LibWeb/HTML: Sanitize email input with multiple attribute
Some checks are pending
CI / macOS, arm64, Sanitizer, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer, GNU (push) Waiting to run
CI / Linux, x86_64, Fuzzers, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer, Clang (push) Waiting to run
Package the js repl as a binary artifact / Linux, arm64 (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

This implements the missing part of the value sanitization algorithm
for email inputs with the multiple attribute.
This commit is contained in:
Glenn Skrzypczak 2025-07-22 00:58:28 +02:00 committed by Tim Ledbetter
commit 6e6507c8c5
Notes: github-actions[bot] 2025-07-22 22:03:58 +00:00
17 changed files with 66 additions and 43 deletions

View file

@ -56,7 +56,7 @@ private:
{ \
ElementBaseClass::attribute_changed(name, old_value, value, namespace_); \
form_node_attribute_changed(name, value); \
form_associated_element_attribute_changed(name, value, namespace_); \
form_associated_element_attribute_changed(name, old_value, value, namespace_); \
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#selection-direction
@ -152,7 +152,7 @@ protected:
virtual void form_associated_element_was_inserted() { }
virtual void form_associated_element_was_removed(DOM::Node*) { }
virtual void form_associated_element_was_moved(GC::Ptr<DOM::Node>) { }
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) { }
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<String> const&, Optional<FlyString> const&) { }
void form_node_was_inserted();
void form_node_was_removed();

View file

@ -80,7 +80,7 @@ WebIDL::ExceptionOr<void> HTMLButtonElement::set_type_for_bindings(String const&
return set_attribute(HTML::AttributeNames::type, type);
}
void HTMLButtonElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const& namespace_)
void HTMLButtonElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
PopoverInvokerElement::associated_attribute_changed(name, value, namespace_);
}

View file

@ -42,7 +42,7 @@ public:
String type_for_bindings() const;
WebIDL::ExceptionOr<void> set_type_for_bindings(String const&);
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
bool will_validate();

View file

@ -134,7 +134,7 @@ void HTMLImageElement::apply_presentational_hints(GC::Ref<CSS::CascadedPropertie
});
}
void HTMLImageElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&)
void HTMLImageElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const& value, Optional<FlyString> const&)
{
if (name == HTML::AttributeNames::crossorigin) {
m_cors_setting = cors_setting_attribute_from_keyword(value);

View file

@ -38,7 +38,7 @@ class HTMLImageElement final
public:
virtual ~HTMLImageElement() override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
Optional<String> alternative_text() const override
{

View file

@ -1380,7 +1380,7 @@ void HTMLInputElement::did_lose_focus()
commit_pending_changes();
}
void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const& namespace_)
void HTMLInputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
PopoverInvokerElement::associated_attribute_changed(name, value, namespace_);
@ -1434,6 +1434,9 @@ void HTMLInputElement::form_associated_element_attribute_changed(FlyString const
} else if (name == HTML::AttributeNames::maxlength) {
handle_maxlength_attribute();
} else if (name == HTML::AttributeNames::multiple) {
if (type_state() == TypeAttributeState::Email && old_value.has_value() != value.has_value()) {
m_value = value_sanitization_algorithm(m_value);
}
update_shadow_tree();
}
}
@ -1657,18 +1660,34 @@ String HTMLInputElement::value_sanitization_algorithm(String const& value) const
}
return MUST(value.trim(Infra::ASCII_WHITESPACE));
} else if (type_state() == HTMLInputElement::TypeAttributeState::Email) {
// https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email):value-sanitization-algorithm
// FIXME: handle the `multiple` attribute
// Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value.
if (value.bytes_as_string_view().contains('\r') || value.bytes_as_string_view().contains('\n')) {
StringBuilder builder;
for (auto c : value.bytes_as_string_view()) {
if (c != '\r' && c != '\n')
builder.append(c);
if (!has_attribute(AttributeNames::multiple)) {
// https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email):value-sanitization-algorithm
// Strip newlines from the value, then strip leading and trailing ASCII whitespace from the value.
if (value.bytes_as_string_view().contains('\r') || value.bytes_as_string_view().contains('\n')) {
StringBuilder builder;
for (auto c : value.bytes_as_string_view()) {
if (c != '\r' && c != '\n')
builder.append(c);
}
return MUST(String::from_utf8(builder.string_view().trim_whitespace()));
}
return MUST(String::from_utf8(builder.string_view().trim(Infra::ASCII_WHITESPACE)));
return MUST(value.trim_ascii_whitespace());
}
return MUST(value.trim(Infra::ASCII_WHITESPACE));
// https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email):value-sanitization-algorithm-2
// 1. Split on commas the element's value, strip leading and trailing ASCII whitespace from each resulting token, if any,
// and let the element's values be the (possibly empty) resulting list of (possibly empty) tokens, maintaining the original order.
Vector<String> values {};
for (auto const& token : MUST(value.split(',', SplitBehavior::KeepEmpty))) {
values.append(MUST(token.trim_ascii_whitespace()));
}
// 2. Set the element's value to the result of concatenating the element's values, separating each value
// from the next by a single U+002C COMMA character (,), maintaining the list's order.
StringBuilder builder;
builder.join(',', values);
return MUST(builder.to_string());
} else if (type_state() == HTMLInputElement::TypeAttributeState::Number) {
// https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):value-sanitization-algorithm
// If the value of the element is not a valid floating-point number, then set it
@ -3320,9 +3339,9 @@ bool HTMLInputElement::suffering_from_a_pattern_mismatch() const
// type attribute's current state, and the element has a compiled pattern regular expression but that regular expression does not match the element's value, then the element is
// suffering from a pattern mismatch.
// FIXME: If the element's value is not the empty string, and the element's multiple attribute is specified and applies to the input element,
// and the element has a compiled pattern regular expression but that regular expression does not match each of the element's values,
// then the element is suffering from a pattern mismatch.
// If the element's value is not the empty string, and the element's multiple attribute is specified and applies to the input element,
// and the element has a compiled pattern regular expression but that regular expression does not match each of the element's values,
// then the element is suffering from a pattern mismatch.
if (!pattern_applies())
return false;
@ -3331,13 +3350,18 @@ bool HTMLInputElement::suffering_from_a_pattern_mismatch() const
if (value.is_empty())
return false;
if (has_attribute(HTML::AttributeNames::multiple) && multiple_applies())
return false;
auto regexp_object = compiled_pattern_regular_expression();
if (!regexp_object.has_value())
return false;
if (has_attribute(HTML::AttributeNames::multiple) && multiple_applies()) {
VERIFY(type_state() == HTMLInputElement::TypeAttributeState::Email);
return AK::any_of(MUST(value.split(',')), [&regexp_object](auto const& value) {
return !regexp_object->match(value).success;
});
}
return !regexp_object->match(value).success;
}

View file

@ -195,7 +195,7 @@ public:
virtual void clear_algorithm() override;
virtual void form_associated_element_was_inserted() override;
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual WebIDL::ExceptionOr<void> cloned(Node&, bool) const override;

View file

@ -91,7 +91,7 @@ bool HTMLObjectElement::will_validate()
return false;
}
void HTMLObjectElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<FlyString> const&)
void HTMLObjectElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const&, Optional<FlyString> const&)
{
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element
// Whenever one of the following conditions occur:

View file

@ -33,7 +33,7 @@ class HTMLObjectElement final
public:
virtual ~HTMLObjectElement() override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual void form_associated_element_was_removed(DOM::Node*) override;
String data() const;

View file

@ -32,7 +32,7 @@ void HTMLOutputElement::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_html_for);
}
void HTMLOutputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&)
void HTMLOutputElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const& value, Optional<FlyString> const&)
{
if (name == HTML::AttributeNames::for_) {
if (m_html_for)

View file

@ -65,7 +65,7 @@ private:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor& visitor) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
GC::Ptr<DOM::DOMTokenList> m_html_for;

View file

@ -574,7 +574,7 @@ void HTMLSelectElement::form_associated_element_was_inserted()
create_shadow_tree_if_needed();
}
void HTMLSelectElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&)
void HTMLSelectElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const& value, Optional<FlyString> const&)
{
if (name == HTML::AttributeNames::multiple) {
// If the multiple attribute is absent then update the selectedness of the option elements.

View file

@ -98,7 +98,7 @@ public:
virtual void activation_behavior(DOM::Event const&) override;
virtual void form_associated_element_was_inserted() override;
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
void did_select_item(Optional<u32> const& id);

View file

@ -438,7 +438,7 @@ void HTMLTextAreaElement::children_changed(ChildrenChangedMetadata const* metada
}
}
void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& value, Optional<FlyString> const&)
void HTMLTextAreaElement::form_associated_element_attribute_changed(FlyString const& name, Optional<String> const&, Optional<String> const& value, Optional<FlyString> const&)
{
if (name == HTML::AttributeNames::placeholder) {
if (m_placeholder_text_node)

View file

@ -70,7 +70,7 @@ public:
virtual WebIDL::ExceptionOr<void> cloned(Node&, bool) const override;
virtual void form_associated_element_was_inserted() override;
virtual void form_associated_element_attribute_changed(FlyString const&, Optional<String> const&, Optional<FlyString> const&) override;
virtual void form_associated_element_attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
virtual void children_changed(ChildrenChangedMetadata const*) override;

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 85 tests
69 Pass
16 Fail
71 Pass
14 Fail
Pass [INPUT in TEXT status] The pattern attribute is not set
Pass [INPUT in TEXT status] The value attibute is empty string
Pass [INPUT in TEXT status] The value attribute matches the pattern attribute
@ -80,12 +80,12 @@ Pass [INPUT in EMAIL status] The pattern attribute is not set, if multiple is pr
Pass [INPUT in EMAIL status] The value attibute is empty string, if multiple is present
Pass [INPUT in EMAIL status] The value attribute matches the pattern attribute, if multiple is present
Pass [INPUT in EMAIL status] The value(ABC) in unicode attribute matches the pattern attribute, if multiple is present
Fail [INPUT in EMAIL status] The value attribute mismatches the pattern attribute, if multiple is present
Fail [INPUT in EMAIL status] The value attribute mismatches the pattern attribute even when a subset matches, if multiple is present
Pass [INPUT in EMAIL status] The value attribute mismatches the pattern attribute, if multiple is present
Pass [INPUT in EMAIL status] The value attribute mismatches the pattern attribute even when a subset matches, if multiple is present
Pass [INPUT in EMAIL status] Invalid regular expression gets ignored, if multiple is present
Pass [INPUT in EMAIL status] Invalid `v` regular expression gets ignored, if multiple is present
Pass [INPUT in EMAIL status] The pattern attribute tries to escape a group, if multiple is present
Pass [INPUT in EMAIL status] The pattern attribute uses Unicode features, if multiple is present
Fail [INPUT in EMAIL status] The pattern attribute uses Unicode features, if multiple is present
Pass [INPUT in EMAIL status] The value attribute matches JavaScript-specific regular expression, if multiple is present
Fail [INPUT in EMAIL status] The value attribute mismatches JavaScript-specific regular expression, if multiple is present
Fail [INPUT in EMAIL status] Commas should be stripped from regex input, if multiple is present
Pass [INPUT in EMAIL status] Commas should be stripped from regex input, if multiple is present

View file

@ -2,13 +2,12 @@ Harness status: OK
Found 8 tests
5 Pass
3 Fail
8 Pass
Pass single_email doesn't have the multiple attribute
Pass value should be sanitized: strip line breaks
Pass Email address validity
Fail When the multiple attribute is removed, the user agent must run the value sanitization algorithm
Pass When the multiple attribute is removed, the user agent must run the value sanitization algorithm
Pass multiple_email has the multiple attribute
Fail run the value sanitization algorithm after setting a new value
Pass run the value sanitization algorithm after setting a new value
Pass valid value is a set of valid email addresses separated by a single ','
Fail When the multiple attribute is set, the user agent must run the value sanitization algorithm
Pass When the multiple attribute is set, the user agent must run the value sanitization algorithm