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