Home | History | Annotate | Download | only in forms
      1 /*
      2  * This file is part of the WebKit project.
      3  *
      4  * Copyright (C) 2009 Michelangelo De Simone <micdesim (at) gmail.com>
      5  * Copyright (C) 2010 Google Inc. All rights reserved.
      6  *
      7  * This library is free software; you can redistribute it and/or
      8  * modify it under the terms of the GNU Library General Public
      9  * License as published by the Free Software Foundation; either
     10  * version 2 of the License, or (at your option) any later version.
     11  *
     12  * This library is distributed in the hope that it will be useful,
     13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     15  * Library General Public License for more details.
     16  *
     17  * You should have received a copy of the GNU Library General Public License
     18  * along with this library; see the file COPYING.LIB.  If not, write to
     19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     20  * Boston, MA 02110-1301, USA.
     21  *
     22  */
     23 
     24 #include "config.h"
     25 #include "core/html/forms/EmailInputType.h"
     26 
     27 #include "bindings/v8/ScriptRegexp.h"
     28 #include "core/InputTypeNames.h"
     29 #include "core/html/HTMLInputElement.h"
     30 #include "core/html/parser/HTMLParserIdioms.h"
     31 #include "core/page/Chrome.h"
     32 #include "core/page/ChromeClient.h"
     33 #include "platform/text/PlatformLocale.h"
     34 #include "public/platform/Platform.h"
     35 #include "wtf/PassOwnPtr.h"
     36 #include "wtf/text/StringBuilder.h"
     37 #include <unicode/idna.h>
     38 #include <unicode/unistr.h>
     39 
     40 namespace WebCore {
     41 
     42 using blink::WebLocalizedString;
     43 
     44 // http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
     45 static const char localPartCharacters[] = "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+/=?^_`{|}~.-";
     46 static const char emailPattern[] =
     47     "[a-z0-9!#$%&'*+/=?^_`{|}~.-]+" // local part
     48     "@"
     49     "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?" // domain part
     50     "(?:\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*";
     51 
     52 // RFC5321 says the maximum total length of a domain name is 255 octets.
     53 static const int32_t maximumDomainNameLength = 255;
     54 // Use the same option as in url/url_canon_icu.cc
     55 static const int32_t idnaConversionOption = UIDNA_CHECK_BIDI;
     56 
     57 static String convertEmailAddressToASCII(const String& address)
     58 {
     59     if (address.containsOnlyASCII())
     60         return address;
     61 
     62     size_t atPosition = address.find('@');
     63     if (atPosition == kNotFound)
     64         return address;
     65 
     66     // UnicodeString ctor for copy-on-write does not work reliably (in debug
     67     // build.) TODO(jshin): In an unlikely case this is a perf-issue, treat
     68     // 8bit and non-8bit strings separately.
     69     icu::UnicodeString idnDomainName(address.charactersWithNullTermination().data() + atPosition + 1, address.length() - atPosition - 1);
     70     icu::UnicodeString domainName;
     71 
     72     // Leak |idna| at the end.
     73     UErrorCode errorCode = U_ZERO_ERROR;
     74     static icu::IDNA *idna = icu::IDNA::createUTS46Instance(idnaConversionOption, errorCode);
     75     ASSERT(idna);
     76     icu::IDNAInfo idnaInfo;
     77     idna->nameToASCII(idnDomainName, domainName, idnaInfo, errorCode);
     78     if (U_FAILURE(errorCode) || idnaInfo.hasErrors() || domainName.length() > maximumDomainNameLength)
     79         return address;
     80 
     81     StringBuilder builder;
     82     builder.append(address, 0, atPosition + 1);
     83     builder.append(domainName.getBuffer(), domainName.length());
     84     return builder.toString();
     85 }
     86 
     87 String EmailInputType::convertEmailAddressToUnicode(const String& address) const
     88 {
     89     if (!address.containsOnlyASCII())
     90         return address;
     91 
     92     size_t atPosition = address.find('@');
     93     if (atPosition == kNotFound)
     94         return address;
     95 
     96     if (address.find("xn--", atPosition + 1) == kNotFound)
     97         return address;
     98 
     99     if (!chrome())
    100         return address;
    101 
    102     String languages = chrome()->client().acceptLanguages();
    103     String unicodeHost = blink::Platform::current()->convertIDNToUnicode(address.substring(atPosition + 1), languages);
    104     StringBuilder builder;
    105     builder.append(address, 0, atPosition + 1);
    106     builder.append(unicodeHost);
    107     return builder.toString();
    108 }
    109 
    110 static bool isInvalidLocalPartCharacter(UChar ch)
    111 {
    112     if (!isASCII(ch))
    113         return true;
    114     DEFINE_STATIC_LOCAL(const String, validCharacters, (localPartCharacters));
    115     return validCharacters.find(toASCIILower(ch)) == kNotFound;
    116 }
    117 
    118 static bool isInvalidDomainCharacter(UChar ch)
    119 {
    120     if (!isASCII(ch))
    121         return true;
    122     return !isASCIILower(ch) && !isASCIIUpper(ch) && !isASCIIDigit(ch) && ch != '.' && ch != '-';
    123 }
    124 
    125 static bool checkValidDotUsage(const String& domain)
    126 {
    127     if (domain.isEmpty())
    128         return true;
    129     if (domain[0] == '.' || domain[domain.length() - 1] == '.')
    130         return false;
    131     return domain.find("..") == kNotFound;
    132 }
    133 
    134 static bool isValidEmailAddress(const String& address)
    135 {
    136     int addressLength = address.length();
    137     if (!addressLength)
    138         return false;
    139 
    140     DEFINE_STATIC_LOCAL(const ScriptRegexp, regExp, (emailPattern, TextCaseInsensitive));
    141 
    142     int matchLength;
    143     int matchOffset = regExp.match(address, 0, &matchLength);
    144 
    145     return !matchOffset && matchLength == addressLength;
    146 }
    147 
    148 PassRefPtrWillBeRawPtr<InputType> EmailInputType::create(HTMLInputElement& element)
    149 {
    150     return adoptRefWillBeNoop(new EmailInputType(element));
    151 }
    152 
    153 void EmailInputType::countUsage()
    154 {
    155     countUsageIfVisible(UseCounter::InputTypeEmail);
    156     bool hasMaxLength = element().fastHasAttribute(HTMLNames::maxlengthAttr);
    157     if (hasMaxLength)
    158         countUsageIfVisible(UseCounter::InputTypeEmailMaxLength);
    159     if (element().multiple()) {
    160         countUsageIfVisible(UseCounter::InputTypeEmailMultiple);
    161         if (hasMaxLength)
    162             countUsageIfVisible(UseCounter::InputTypeEmailMultipleMaxLength);
    163     }
    164 }
    165 
    166 const AtomicString& EmailInputType::formControlType() const
    167 {
    168     return InputTypeNames::email;
    169 }
    170 
    171 // The return value is an invalid email address string if the specified string
    172 // contains an invalid email address. Otherwise, null string is returned.
    173 // If an empty string is returned, it means empty address is specified.
    174 // e.g. "foo (at) example.com,,bar (at) example.com" for multiple case.
    175 String EmailInputType::findInvalidAddress(const String& value) const
    176 {
    177     if (value.isEmpty())
    178         return String();
    179     if (!element().multiple())
    180         return isValidEmailAddress(value) ? String() : value;
    181     Vector<String> addresses;
    182     value.split(',', true, addresses);
    183     for (unsigned i = 0; i < addresses.size(); ++i) {
    184         String stripped = stripLeadingAndTrailingHTMLSpaces(addresses[i]);
    185         if (!isValidEmailAddress(stripped))
    186             return stripped;
    187     }
    188     return String();
    189 }
    190 
    191 bool EmailInputType::typeMismatchFor(const String& value) const
    192 {
    193     return !findInvalidAddress(value).isNull();
    194 }
    195 
    196 bool EmailInputType::typeMismatch() const
    197 {
    198     return typeMismatchFor(element().value());
    199 }
    200 
    201 String EmailInputType::typeMismatchText() const
    202 {
    203     String invalidAddress = findInvalidAddress(element().value());
    204     ASSERT(!invalidAddress.isNull());
    205     if (invalidAddress.isEmpty())
    206         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailEmpty);
    207     String atSign = String("@");
    208     size_t atIndex = invalidAddress.find('@');
    209     if (atIndex == kNotFound)
    210         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailNoAtSign, atSign, invalidAddress);
    211     // We check validity against an ASCII value because of difficulty to check
    212     // invalid characters. However we should show Unicode value.
    213     String unicodeAddress = convertEmailAddressToUnicode(invalidAddress);
    214     String localPart = invalidAddress.left(atIndex);
    215     String domain = invalidAddress.substring(atIndex + 1);
    216     if (localPart.isEmpty())
    217         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailEmptyLocal, atSign, unicodeAddress);
    218     if (domain.isEmpty())
    219         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailEmptyDomain, atSign, unicodeAddress);
    220     size_t invalidCharIndex = localPart.find(isInvalidLocalPartCharacter);
    221     if (invalidCharIndex != kNotFound) {
    222         unsigned charLength = U_IS_LEAD(localPart[invalidCharIndex]) ? 2 : 1;
    223         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailInvalidLocal, atSign, localPart.substring(invalidCharIndex, charLength));
    224     }
    225     invalidCharIndex = domain.find(isInvalidDomainCharacter);
    226     if (invalidCharIndex != kNotFound) {
    227         unsigned charLength = U_IS_LEAD(domain[invalidCharIndex]) ? 2 : 1;
    228         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailInvalidDomain, atSign, domain.substring(invalidCharIndex, charLength));
    229     }
    230     if (!checkValidDotUsage(domain)) {
    231         size_t atIndexInUnicode = unicodeAddress.find('@');
    232         ASSERT(atIndexInUnicode != kNotFound);
    233         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmailInvalidDots, String("."), unicodeAddress.substring(atIndexInUnicode + 1));
    234     }
    235     if (element().multiple())
    236         return locale().queryString(WebLocalizedString::ValidationTypeMismatchForMultipleEmail);
    237     return locale().queryString(WebLocalizedString::ValidationTypeMismatchForEmail);
    238 }
    239 
    240 bool EmailInputType::isEmailField() const
    241 {
    242     return true;
    243 }
    244 
    245 bool EmailInputType::supportsSelectionAPI() const
    246 {
    247     return false;
    248 }
    249 
    250 String EmailInputType::sanitizeValue(const String& proposedValue) const
    251 {
    252     String noLineBreakValue = proposedValue.removeCharacters(isHTMLLineBreak);
    253     if (!element().multiple())
    254         return stripLeadingAndTrailingHTMLSpaces(noLineBreakValue);
    255     Vector<String> addresses;
    256     noLineBreakValue.split(',', true, addresses);
    257     StringBuilder strippedValue;
    258     for (size_t i = 0; i < addresses.size(); ++i) {
    259         if (i > 0)
    260             strippedValue.append(",");
    261         strippedValue.append(stripLeadingAndTrailingHTMLSpaces(addresses[i]));
    262     }
    263     return strippedValue.toString();
    264 }
    265 
    266 String EmailInputType::convertFromVisibleValue(const String& visibleValue) const
    267 {
    268     String sanitizedValue = sanitizeValue(visibleValue);
    269     if (!element().multiple())
    270         return convertEmailAddressToASCII(sanitizedValue);
    271     Vector<String> addresses;
    272     sanitizedValue.split(',', true, addresses);
    273     StringBuilder builder;
    274     builder.reserveCapacity(sanitizedValue.length());
    275     for (size_t i = 0; i < addresses.size(); ++i) {
    276         if (i > 0)
    277             builder.append(",");
    278         builder.append(convertEmailAddressToASCII(addresses[i]));
    279     }
    280     return builder.toString();
    281 }
    282 
    283 String EmailInputType::visibleValue() const
    284 {
    285     String value = element().value();
    286     if (!element().multiple())
    287         return convertEmailAddressToUnicode(value);
    288 
    289     Vector<String> addresses;
    290     value.split(',', true, addresses);
    291     StringBuilder builder;
    292     builder.reserveCapacity(value.length());
    293     for (size_t i = 0; i < addresses.size(); ++i) {
    294         if (i > 0)
    295             builder.append(",");
    296         builder.append(convertEmailAddressToUnicode(addresses[i]));
    297     }
    298     return builder.toString();
    299 }
    300 
    301 } // namespace WebCore
    302