blob: 52be833b101f4f6c5b1a01ad011a3c126ee20f67 [file] [log] [blame]
/*
* This file is part of the WebKit project.
*
* Copyright (C) 2009 Michelangelo De Simone <micdesim@gmail.com>
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "core/html/forms/EmailInputType.h"
#include <unicode/idna.h>
#include <unicode/unistr.h>
#include <unicode/uvernum.h>
#include "bindings/core/v8/ScriptRegexp.h"
#include "core/InputTypeNames.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/parser/HTMLParserIdioms.h"
#include "core/page/ChromeClient.h"
#include "platform/text/PlatformLocale.h"
#include "public/platform/Platform.h"
#include "wtf/text/StringBuilder.h"
#if U_ICU_VERSION_MAJOR_NUM >= 59
#include <unicode/char16ptr.h>
#endif
namespace blink {
using blink::WebLocalizedString;
// http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
static const char localPartCharacters[] =
"abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+/=?^_`{|}~.-";
static const char emailPattern[] =
"[a-z0-9!#$%&'*+/=?^_`{|}~.-]+" // local part
"@"
"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?" // domain part
"(?:\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*";
// RFC5321 says the maximum total length of a domain name is 255 octets.
static const int32_t maximumDomainNameLength = 255;
// Use the same option as in url/url_canon_icu.cc
static const int32_t idnaConversionOption = UIDNA_CHECK_BIDI;
std::unique_ptr<ScriptRegexp> EmailInputType::createEmailRegexp() {
return std::unique_ptr<ScriptRegexp>(
new ScriptRegexp(emailPattern, TextCaseUnicodeInsensitive));
}
String EmailInputType::convertEmailAddressToASCII(const ScriptRegexp& regexp,
const String& address) {
if (address.containsOnlyASCII())
return address;
size_t atPosition = address.find('@');
if (atPosition == kNotFound)
return address;
String host = address.substring(atPosition + 1);
// UnicodeString ctor for copy-on-write does not work reliably (in debug
// build.) TODO(jshin): In an unlikely case this is a perf-issue, treat
// 8bit and non-8bit strings separately.
host.ensure16Bit();
icu::UnicodeString idnDomainName(host.characters16(), host.length());
icu::UnicodeString domainName;
// Leak |idna| at the end.
UErrorCode errorCode = U_ZERO_ERROR;
static icu::IDNA* idna =
icu::IDNA::createUTS46Instance(idnaConversionOption, errorCode);
DCHECK(idna);
icu::IDNAInfo idnaInfo;
idna->nameToASCII(idnDomainName, domainName, idnaInfo, errorCode);
if (U_FAILURE(errorCode) || idnaInfo.hasErrors() ||
domainName.length() > maximumDomainNameLength)
return address;
StringBuilder builder;
builder.append(address, 0, atPosition + 1);
#if U_ICU_VERSION_MAJOR_NUM >= 59
builder.append(icu::toUCharPtr(domainName.getBuffer()), domainName.length());
#else
builder.append(domainName.getBuffer(), domainName.length());
#endif
String asciiEmail = builder.toString();
return isValidEmailAddress(regexp, asciiEmail) ? asciiEmail : address;
}
String EmailInputType::convertEmailAddressToUnicode(
const String& address) const {
if (!address.containsOnlyASCII())
return address;
size_t atPosition = address.find('@');
if (atPosition == kNotFound)
return address;
if (address.find("xn--", atPosition + 1) == kNotFound)
return address;
String unicodeHost = Platform::current()->convertIDNToUnicode(
address.substring(atPosition + 1));
StringBuilder builder;
builder.append(address, 0, atPosition + 1);
builder.append(unicodeHost);
return builder.toString();
}
static bool isInvalidLocalPartCharacter(UChar ch) {
if (!isASCII(ch))
return true;
DEFINE_STATIC_LOCAL(const String, validCharacters, (localPartCharacters));
return validCharacters.find(toASCIILower(ch)) == kNotFound;
}
static bool isInvalidDomainCharacter(UChar ch) {
if (!isASCII(ch))
return true;
return !isASCIILower(ch) && !isASCIIUpper(ch) && !isASCIIDigit(ch) &&
ch != '.' && ch != '-';
}
static bool checkValidDotUsage(const String& domain) {
if (domain.isEmpty())
return true;
if (domain[0] == '.' || domain[domain.length() - 1] == '.')
return false;
return domain.find("..") == kNotFound;
}
bool EmailInputType::isValidEmailAddress(const ScriptRegexp& regexp,
const String& address) {
int addressLength = address.length();
if (!addressLength)
return false;
int matchLength;
int matchOffset = regexp.match(address, 0, &matchLength);
return !matchOffset && matchLength == addressLength;
}
EmailInputType::EmailInputType(HTMLInputElement& element)
: BaseTextInputType(element) {}
InputType* EmailInputType::create(HTMLInputElement& element) {
return new EmailInputType(element);
}
void EmailInputType::countUsage() {
countUsageIfVisible(UseCounter::InputTypeEmail);
bool hasMaxLength = element().fastHasAttribute(HTMLNames::maxlengthAttr);
if (hasMaxLength)
countUsageIfVisible(UseCounter::InputTypeEmailMaxLength);
if (element().multiple()) {
countUsageIfVisible(UseCounter::InputTypeEmailMultiple);
if (hasMaxLength)
countUsageIfVisible(UseCounter::InputTypeEmailMultipleMaxLength);
}
}
const AtomicString& EmailInputType::formControlType() const {
return InputTypeNames::email;
}
ScriptRegexp& EmailInputType::ensureEmailRegexp() const {
if (!m_emailRegexp)
m_emailRegexp = createEmailRegexp();
return *m_emailRegexp;
}
// The return value is an invalid email address string if the specified string
// contains an invalid email address. Otherwise, null string is returned.
// If an empty string is returned, it means empty address is specified.
// e.g. "foo@example.com,,bar@example.com" for multiple case.
String EmailInputType::findInvalidAddress(const String& value) const {
if (value.isEmpty())
return String();
if (!element().multiple())
return isValidEmailAddress(ensureEmailRegexp(), value) ? String() : value;
Vector<String> addresses;
value.split(',', true, addresses);
for (const auto& address : addresses) {
String stripped = stripLeadingAndTrailingHTMLSpaces(address);
if (!isValidEmailAddress(ensureEmailRegexp(), stripped))
return stripped;
}
return String();
}
bool EmailInputType::typeMismatchFor(const String& value) const {
return !findInvalidAddress(value).isNull();
}
bool EmailInputType::typeMismatch() const {
return typeMismatchFor(element().value());
}
String EmailInputType::typeMismatchText() const {
String invalidAddress = findInvalidAddress(element().value());
DCHECK(!invalidAddress.isNull());
if (invalidAddress.isEmpty())
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailEmpty);
String atSign = String("@");
size_t atIndex = invalidAddress.find('@');
if (atIndex == kNotFound)
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailNoAtSign, atSign,
invalidAddress);
// We check validity against an ASCII value because of difficulty to check
// invalid characters. However we should show Unicode value.
String unicodeAddress = convertEmailAddressToUnicode(invalidAddress);
String localPart = invalidAddress.left(atIndex);
String domain = invalidAddress.substring(atIndex + 1);
if (localPart.isEmpty())
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailEmptyLocal, atSign,
unicodeAddress);
if (domain.isEmpty())
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailEmptyDomain, atSign,
unicodeAddress);
size_t invalidCharIndex = localPart.find(isInvalidLocalPartCharacter);
if (invalidCharIndex != kNotFound) {
unsigned charLength = U_IS_LEAD(localPart[invalidCharIndex]) ? 2 : 1;
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailInvalidLocal, atSign,
localPart.substring(invalidCharIndex, charLength));
}
invalidCharIndex = domain.find(isInvalidDomainCharacter);
if (invalidCharIndex != kNotFound) {
unsigned charLength = U_IS_LEAD(domain[invalidCharIndex]) ? 2 : 1;
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailInvalidDomain, atSign,
domain.substring(invalidCharIndex, charLength));
}
if (!checkValidDotUsage(domain)) {
size_t atIndexInUnicode = unicodeAddress.find('@');
DCHECK_NE(atIndexInUnicode, kNotFound);
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmailInvalidDots,
String("."), unicodeAddress.substring(atIndexInUnicode + 1));
}
if (element().multiple())
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForMultipleEmail);
return locale().queryString(
WebLocalizedString::ValidationTypeMismatchForEmail);
}
bool EmailInputType::supportsSelectionAPI() const {
return false;
}
String EmailInputType::sanitizeValue(const String& proposedValue) const {
String noLineBreakValue = proposedValue.removeCharacters(isHTMLLineBreak);
if (!element().multiple())
return stripLeadingAndTrailingHTMLSpaces(noLineBreakValue);
Vector<String> addresses;
noLineBreakValue.split(',', true, addresses);
StringBuilder strippedValue;
for (size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
strippedValue.append(',');
strippedValue.append(stripLeadingAndTrailingHTMLSpaces(addresses[i]));
}
return strippedValue.toString();
}
String EmailInputType::convertFromVisibleValue(
const String& visibleValue) const {
String sanitizedValue = sanitizeValue(visibleValue);
if (!element().multiple())
return convertEmailAddressToASCII(ensureEmailRegexp(), sanitizedValue);
Vector<String> addresses;
sanitizedValue.split(',', true, addresses);
StringBuilder builder;
builder.reserveCapacity(sanitizedValue.length());
for (size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
builder.append(',');
builder.append(
convertEmailAddressToASCII(ensureEmailRegexp(), addresses[i]));
}
return builder.toString();
}
String EmailInputType::visibleValue() const {
String value = element().value();
if (!element().multiple())
return convertEmailAddressToUnicode(value);
Vector<String> addresses;
value.split(',', true, addresses);
StringBuilder builder;
builder.reserveCapacity(value.length());
for (size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
builder.append(',');
builder.append(convertEmailAddressToUnicode(addresses[i]));
}
return builder.toString();
}
} // namespace blink