blob: 4e574dade6d93282036981c07144541c1a10f3ab [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 "third_party/blink/renderer/core/html/forms/email_input_type.h"
#include <unicode/idna.h>
#include <unicode/unistr.h>
#include <unicode/uvernum.h>
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/bindings/core/v8/script_regexp.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#if U_ICU_VERSION_MAJOR_NUM >= 59
#include <unicode/char16ptr.h>
#endif
namespace blink {
// http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
static const char kLocalPartCharacters[] =
"abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+/=?^_`{|}~.-";
static const char kEmailPattern[] =
"[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 kMaximumDomainNameLength = 255;
// Use the same option as in url/url_canon_icu.cc
static const int32_t kIdnaConversionOption = UIDNA_CHECK_BIDI;
ScriptRegexp* EmailInputType::CreateEmailRegexp() {
return MakeGarbageCollected<ScriptRegexp>(kEmailPattern,
kTextCaseUnicodeInsensitive);
}
String EmailInputType::ConvertEmailAddressToASCII(const ScriptRegexp& regexp,
const String& address) {
if (address.ContainsOnlyASCIIOrEmpty())
return address;
wtf_size_t at_position = address.find('@');
if (at_position == kNotFound)
return address;
String host = address.Substring(at_position + 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 idn_domain_name(host.Characters16(), host.length());
icu::UnicodeString domain_name;
// Leak |idna| at the end.
UErrorCode error_code = U_ZERO_ERROR;
static const icu::IDNA* const idna =
icu::IDNA::createUTS46Instance(kIdnaConversionOption, error_code);
DCHECK(idna);
icu::IDNAInfo idna_info;
idna->nameToASCII(idn_domain_name, domain_name, idna_info, error_code);
if (U_FAILURE(error_code) || idna_info.hasErrors() ||
domain_name.length() > kMaximumDomainNameLength)
return address;
StringBuilder builder;
builder.Append(address, 0, at_position + 1);
#if U_ICU_VERSION_MAJOR_NUM >= 59
builder.Append(icu::toUCharPtr(domain_name.getBuffer()), domain_name.length());
#else
builder.Append(domain_name.getBuffer(), domain_name.length());
#endif
String ascii_email = builder.ToString();
return IsValidEmailAddress(regexp, ascii_email) ? ascii_email : address;
}
String EmailInputType::ConvertEmailAddressToUnicode(
const String& address) const {
if (!address.ContainsOnlyASCIIOrEmpty())
return address;
wtf_size_t at_position = address.find('@');
if (at_position == kNotFound)
return address;
if (address.Find("xn--", at_position + 1) == kNotFound)
return address;
String unicode_host = Platform::Current()->ConvertIDNToUnicode(
address.Substring(at_position + 1));
StringBuilder builder;
builder.Append(address, 0, at_position + 1);
builder.Append(unicode_host);
return builder.ToString();
}
static bool IsInvalidLocalPartCharacter(UChar ch) {
if (!IsASCII(ch))
return true;
DEFINE_STATIC_LOCAL(const String, valid_characters, (kLocalPartCharacters));
return valid_characters.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 address_length = address.length();
if (!address_length)
return false;
int match_length;
int match_offset = regexp.Match(address, 0, &match_length);
return !match_offset && match_length == address_length;
}
EmailInputType::EmailInputType(HTMLInputElement& element)
: BaseTextInputType(element) {}
void EmailInputType::CountUsage() {
CountUsageIfVisible(WebFeature::kInputTypeEmail);
bool has_max_length =
GetElement().FastHasAttribute(html_names::kMaxlengthAttr);
if (has_max_length)
CountUsageIfVisible(WebFeature::kInputTypeEmailMaxLength);
if (GetElement().Multiple()) {
CountUsageIfVisible(WebFeature::kInputTypeEmailMultiple);
if (has_max_length)
CountUsageIfVisible(WebFeature::kInputTypeEmailMultipleMaxLength);
}
}
const AtomicString& EmailInputType::FormControlType() const {
return input_type_names::kEmail;
}
// The return value is an invalid email address string if the specified string
// contains an invalid email address. Otherwise, an empty 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 (!GetElement().Multiple()) {
return IsValidEmailAddress(GetElement().GetDocument().EnsureEmailRegexp(),
value)
? String()
: value;
}
Vector<String> addresses;
value.Split(',', true, addresses);
for (const auto& address : addresses) {
String stripped = StripLeadingAndTrailingHTMLSpaces(address);
if (!IsValidEmailAddress(GetElement().GetDocument().EnsureEmailRegexp(),
stripped))
return stripped;
}
return String();
}
bool EmailInputType::TypeMismatchFor(const String& value) const {
return !FindInvalidAddress(value).IsNull();
}
bool EmailInputType::TypeMismatch() const {
return TypeMismatchFor(GetElement().value());
}
String EmailInputType::TypeMismatchText() const {
String invalid_address = FindInvalidAddress(GetElement().value());
DCHECK(!invalid_address.IsNull());
if (invalid_address.IsEmpty()) {
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_EMPTY);
}
String at_sign = String("@");
wtf_size_t at_index = invalid_address.find('@');
if (at_index == kNotFound)
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_NO_AT_SIGN, at_sign,
invalid_address);
// We check validity against an ASCII value because of difficulty to check
// invalid characters. However we should show Unicode value.
String unicode_address = ConvertEmailAddressToUnicode(invalid_address);
String local_part = invalid_address.Left(at_index);
String domain = invalid_address.Substring(at_index + 1);
if (local_part.IsEmpty())
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_EMPTY_LOCAL, at_sign,
unicode_address);
if (domain.IsEmpty())
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_EMPTY_DOMAIN, at_sign,
unicode_address);
wtf_size_t invalid_char_index = local_part.Find(IsInvalidLocalPartCharacter);
if (invalid_char_index != kNotFound) {
unsigned char_length = U_IS_LEAD(local_part[invalid_char_index]) ? 2 : 1;
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_INVALID_LOCAL, at_sign,
local_part.Substring(invalid_char_index, char_length));
}
invalid_char_index = domain.Find(IsInvalidDomainCharacter);
if (invalid_char_index != kNotFound) {
unsigned char_length = U_IS_LEAD(domain[invalid_char_index]) ? 2 : 1;
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_INVALID_DOMAIN, at_sign,
domain.Substring(invalid_char_index, char_length));
}
if (!CheckValidDotUsage(domain)) {
wtf_size_t at_index_in_unicode = unicode_address.find('@');
DCHECK_NE(at_index_in_unicode, kNotFound);
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL_INVALID_DOTS, String("."),
unicode_address.Substring(at_index_in_unicode + 1));
}
if (GetElement().Multiple()) {
return GetLocale().QueryString(
IDS_FORM_VALIDATION_TYPE_MISMATCH_MULTIPLE_EMAIL);
}
return GetLocale().QueryString(IDS_FORM_VALIDATION_TYPE_MISMATCH_EMAIL);
}
bool EmailInputType::SupportsSelectionAPI() const {
return false;
}
String EmailInputType::SanitizeValue(const String& proposed_value) const {
String no_line_break_value = proposed_value.RemoveCharacters(IsHTMLLineBreak);
if (!GetElement().Multiple())
return StripLeadingAndTrailingHTMLSpaces(no_line_break_value);
Vector<String> addresses;
no_line_break_value.Split(',', true, addresses);
StringBuilder stripped_value;
for (wtf_size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
stripped_value.Append(',');
stripped_value.Append(StripLeadingAndTrailingHTMLSpaces(addresses[i]));
}
return stripped_value.ToString();
}
String EmailInputType::ConvertFromVisibleValue(
const String& visible_value) const {
String sanitized_value = SanitizeValue(visible_value);
if (!GetElement().Multiple()) {
return ConvertEmailAddressToASCII(
GetElement().GetDocument().EnsureEmailRegexp(), sanitized_value);
}
Vector<String> addresses;
sanitized_value.Split(',', true, addresses);
StringBuilder builder;
builder.ReserveCapacity(sanitized_value.length());
for (wtf_size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
builder.Append(',');
builder.Append(ConvertEmailAddressToASCII(
GetElement().GetDocument().EnsureEmailRegexp(), addresses[i]));
}
return builder.ToString();
}
String EmailInputType::VisibleValue() const {
String value = GetElement().value();
if (!GetElement().Multiple())
return ConvertEmailAddressToUnicode(value);
Vector<String> addresses;
value.Split(',', true, addresses);
StringBuilder builder;
builder.ReserveCapacity(value.length());
for (wtf_size_t i = 0; i < addresses.size(); ++i) {
if (i > 0)
builder.Append(',');
builder.Append(ConvertEmailAddressToUnicode(addresses[i]));
}
return builder.ToString();
}
} // namespace blink