1 /*
2 * This file is part of the WebKit project.
3 *
4 * Copyright (C) 2009 Michelangelo De Simone <micdesim@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
convertEmailAddressToASCII(const String & address)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
convertEmailAddressToUnicode(const String & address) const87 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
isInvalidLocalPartCharacter(UChar ch)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
isInvalidDomainCharacter(UChar ch)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
checkValidDotUsage(const String & domain)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
isValidEmailAddress(const String & address)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
create(HTMLInputElement & element)148 PassRefPtrWillBeRawPtr<InputType> EmailInputType::create(HTMLInputElement& element)
149 {
150 return adoptRefWillBeNoop(new EmailInputType(element));
151 }
152
countUsage()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
formControlType() const166 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@example.com,,bar@example.com" for multiple case.
findInvalidAddress(const String & value) const175 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
typeMismatchFor(const String & value) const191 bool EmailInputType::typeMismatchFor(const String& value) const
192 {
193 return !findInvalidAddress(value).isNull();
194 }
195
typeMismatch() const196 bool EmailInputType::typeMismatch() const
197 {
198 return typeMismatchFor(element().value());
199 }
200
typeMismatchText() const201 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
isEmailField() const240 bool EmailInputType::isEmailField() const
241 {
242 return true;
243 }
244
supportsSelectionAPI() const245 bool EmailInputType::supportsSelectionAPI() const
246 {
247 return false;
248 }
249
sanitizeValue(const String & proposedValue) const250 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
convertFromVisibleValue(const String & visibleValue) const266 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
visibleValue() const283 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