diff --git a/Userland/Libraries/LibWeb/HTML/AttributeNames.h b/Userland/Libraries/LibWeb/HTML/AttributeNames.h
index daf9bac4ad..15ff739eb0 100644
--- a/Userland/Libraries/LibWeb/HTML/AttributeNames.h
+++ b/Userland/Libraries/LibWeb/HTML/AttributeNames.h
@@ -63,10 +63,14 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(dirname) \
__ENUMERATE_HTML_ATTRIBUTE(disabled) \
__ENUMERATE_HTML_ATTRIBUTE(download) \
+ __ENUMERATE_HTML_ATTRIBUTE(enctype) \
__ENUMERATE_HTML_ATTRIBUTE(event) \
__ENUMERATE_HTML_ATTRIBUTE(face) \
__ENUMERATE_HTML_ATTRIBUTE(for_) \
__ENUMERATE_HTML_ATTRIBUTE(form) \
+ __ENUMERATE_HTML_ATTRIBUTE(formaction) \
+ __ENUMERATE_HTML_ATTRIBUTE(formenctype) \
+ __ENUMERATE_HTML_ATTRIBUTE(formmethod) \
__ENUMERATE_HTML_ATTRIBUTE(formnovalidate) \
__ENUMERATE_HTML_ATTRIBUTE(formtarget) \
__ENUMERATE_HTML_ATTRIBUTE(frame) \
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLButtonElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLButtonElement.cpp
index 0055f2f93b..d6673d2ef1 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLButtonElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLButtonElement.cpp
@@ -32,7 +32,7 @@ HTMLButtonElement::HTMLButtonElement(DOM::Document& document, DOM::QualifiedName
case TypeAttributeState::Submit:
// Submit Button
// Submit element's form owner from element.
- form()->submit_form(this).release_value_but_fixme_should_propagate_errors();
+ form()->submit_form(*this).release_value_but_fixme_should_propagate_errors();
break;
case TypeAttributeState::Reset:
// Reset Button
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp
index aa81e2a713..c3a619af44 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.cpp
@@ -1,15 +1,19 @@
/*
* Copyright (c) 2018-2021, Andreas Kling
* Copyright (c) 2023, Kenneth Myhra
+ * Copyright (c) 2023, Luke Wilde
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include
+#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -19,6 +23,8 @@
#include
#include
#include
+#include
+#include
#include
#include
@@ -47,107 +53,205 @@ void HTMLFormElement::visit_edges(Cell::Visitor& visitor)
visitor.visit(element.ptr());
}
-ErrorOr HTMLFormElement::submit_form(JS::GCPtr submitter, bool from_submit_binding)
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit
+WebIDL::ExceptionOr HTMLFormElement::submit_form(JS::NonnullGCPtr submitter, bool from_submit_binding)
{
+ auto& vm = this->vm();
+ auto& realm = this->realm();
+
+ // 1. If form cannot navigate, then return.
if (cannot_navigate())
return {};
- if (action().is_null()) {
- dbgln("Unsupported form action ''");
+ // 2. If form's constructing entry list is true, then return.
+ if (m_constructing_entry_list)
return {};
- }
- auto effective_method = method().to_lowercase();
+ // 3. Let form document be form's node document.
+ JS::NonnullGCPtr form_document = this->document();
- if (effective_method == "dialog") {
- dbgln("Failed to submit form: Unsupported form method '{}'", method());
+ // FIXME: This is not in the navigable version.
+ // Let form browsing context be the browsing context of form document.
+ auto* form_browsing_context = form_document->browsing_context();
+
+ // 4. If form document's active sandboxing flag set has its sandboxed forms browsing context flag set, then return.
+ if (form_document->active_sandboxing_flag_set().flags & HTML::SandboxingFlagSet::Flag::SandboxedForms)
return {};
- }
-
- if (effective_method != "get" && effective_method != "post") {
- effective_method = "get";
- }
+ // 5. If the submitted from submit() method flag is not set, then:
if (!from_submit_binding) {
+ // 1. If form's firing submission events is true, then return.
if (m_firing_submission_events)
return {};
+ // 2. Set form's firing submission events to true.
m_firing_submission_events = true;
- // FIXME: If the submitter element's no-validate state is false...
+ // FIXME: 3. If the submitter element's no-validate state is false, then interactively validate the constraints
+ // of form and examine the result. If the result is negative (i.e., the constraint validation concluded
+ // that there were invalid fields and probably informed the user of this), then:
+ // 1. Set form's firing submission events to false.
+ // 2. Return.
+ // 4. Let submitterButton be null if submitter is form. Otherwise, let submitterButton be submitter.
JS::GCPtr submitter_button;
-
if (submitter != this)
submitter_button = submitter;
+ // 5. Let shouldContinue be the result of firing an event named submit at form using SubmitEvent, with the
+ // submitter attribute initialized to submitterButton, the bubbles attribute initialized to true, and the
+ // cancelable attribute initialized to true.
SubmitEventInit event_init {};
event_init.submitter = submitter_button;
- auto submit_event = SubmitEvent::create(realm(), EventNames::submit, event_init).release_value_but_fixme_should_propagate_errors();
+ auto submit_event = SubmitEvent::create(realm, EventNames::submit, event_init).release_value_but_fixme_should_propagate_errors();
submit_event->set_bubbles(true);
submit_event->set_cancelable(true);
- bool continue_ = dispatch_event(*submit_event);
+ bool should_continue = dispatch_event(*submit_event);
+ // 6. Set form's firing submission events to false.
m_firing_submission_events = false;
- if (!continue_)
+ // 7. If shouldContinue is false, then return.
+ if (!should_continue)
return {};
- // This is checked again because arbitrary JS may have run when handling submit,
- // which may have changed the result.
+ // 8. If form cannot navigate, then return.
+ // Spec Note: Cannot navigate is run again as dispatching the submit event could have changed the outcome.
if (cannot_navigate())
return {};
}
- AK::URL url(document().parse_url(action()));
-
- if (!url.is_valid()) {
- dbgln("Failed to submit form: Invalid URL: {}", action());
+ // 6. Let encoding be the result of picking an encoding for the form.
+ auto encoding = TRY_OR_THROW_OOM(vm, pick_an_encoding());
+ if (encoding != "UTF-8"sv) {
+ dbgln("FIXME: Support encodings other than UTF-8 in form submission. Returning from form submission.");
return {};
}
- if (url.scheme() == "file") {
- if (document().url().scheme() != "file") {
- dbgln("Failed to submit form: Security violation: {} may not submit to {}", document().url(), url);
- return {};
- }
- if (effective_method != "get") {
- dbgln("Failed to submit form: Unsupported form method '{}' for URL: {}", method(), url);
- return {};
- }
- } else if (url.scheme() != "http" && url.scheme() != "https") {
- dbgln("Failed to submit form: Unsupported protocol for URL: {}", url);
+ // 7. Let entry list be the result of constructing the entry list with form, submitter, and encoding.
+ auto entry_list_or_null = TRY(construct_entry_list(realm, *this, submitter, encoding));
+
+ // 8. Assert: entry list is not null.
+ VERIFY(entry_list_or_null.has_value());
+ auto entry_list = entry_list_or_null.release_value();
+
+ // 9. If form cannot navigate, then return.
+ // Spec Note: Cannot navigate is run again as dispatching the formdata event in constructing the entry list could
+ // have changed the outcome.
+ if (cannot_navigate())
+ return {};
+
+ // 10. Let method be the submitter element's method.
+ auto method = method_state_from_form_element(submitter);
+
+ // 11. If method is dialog, then:
+ if (method == MethodAttributeState::Dialog) {
+ // FIXME: 1. If form does not have an ancestor dialog element, then return.
+ // FIXME: 2. Let subject be form's nearest ancestor dialog element.
+ // FIXME: 3. Let result be null.
+ // FIXME: 4. If submitter is an input element whose type attribute is in the Image Button state, then:
+ // 1. Let (x, y) be the selected coordinate.
+ // 2. Set result to the concatenation of x, ",", and y.
+ // FIXME: 5. Otherwise, if submitter has a value, then set result to that value.
+ // FIXME: 6. Close the dialog subject with result.
+ // FIXME: 7. Return.
+
+ dbgln("FIXME: Implement form submission with `dialog` action. Returning from form submission.");
return {};
}
- Vector parameters;
+ // 12. Let action be the submitter element's action.
+ auto action = action_from_form_element(submitter);
- for_each_in_inclusive_subtree_of_type([&](auto& input) {
- if (!input.name().is_null() && (input.type() != "submit" || &input == submitter)) {
- auto name = String::from_deprecated_string(input.name()).release_value_but_fixme_should_propagate_errors();
- auto value = String::from_deprecated_string(input.value()).release_value_but_fixme_should_propagate_errors();
- parameters.append({ move(name), move(value) });
- }
- return IterationDecision::Continue;
- });
+ // 13. If action is the empty string, let action be the URL of the form document.
+ if (action.is_empty())
+ action = form_document->url_string();
- if (effective_method == "get") {
- auto url_encoded_parameters = TRY(url_encode(parameters, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded)).to_deprecated_string();
- url.set_query(move(url_encoded_parameters));
+ // 14. Parse a URL given action, relative to the submitter element's node document. If this fails, return.
+ // 15. Let parsed action be the resulting URL record.
+ auto parsed_action = document().parse_url(action);
+ if (!parsed_action.is_valid()) {
+ dbgln("Failed to submit form: Invalid URL: {}", action);
+ return {};
}
- LoadRequest request = LoadRequest::create_for_url_on_page(url, document().page());
+ // 16. Let scheme be the scheme of parsed action.
+ auto const& scheme = parsed_action.scheme();
- if (effective_method == "post") {
- auto url_encoded_parameters = TRY(url_encode(parameters, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
- auto body = TRY(ByteBuffer::copy(url_encoded_parameters.bytes()));
- request.set_method("POST");
- request.set_header("Content-Type", "application/x-www-form-urlencoded");
- request.set_body(move(body));
+ // 17. Let enctype be the submitter element's enctype.
+ auto encoding_type = encoding_type_state_from_form_element(submitter);
+
+ // 18. Let target be the submitter element's formtarget attribute value, if the element is a submit button and has
+ // such an attribute. Otherwise, let it be the result of getting an element's target given submitter's form
+ // owner.
+ DeprecatedString target;
+ if (submitter->has_attribute(AttributeNames::formtarget))
+ target = submitter->attribute(AttributeNames::formtarget);
+ else
+ target = get_an_elements_target();
+
+ // 19. Let noopener be the result of getting an element's noopener with form and target.
+ auto no_opener = get_an_elements_noopener(target);
+
+ // FIXME: Update these steps for navigables.
+ // 20. Let targetNavigable be the first return value of applying the rules for choosing a navigable given target, form's node navigable, and noopener.
+ auto target_navigable = form_browsing_context->choose_a_browsing_context(target, no_opener).browsing_context;
+
+ // 21. If targetNavigable is null, then return.
+ if (!target_navigable) {
+ dbgln("Failed to submit form: choose_a_browsing_context returning a null browsing context");
+ return {};
}
- if (auto* page = document().page())
- page->load(request);
+ // 22. Let historyHandling be "push".
+ // NOTE: This is `Default` in the old spec.
+ auto history_handling = HistoryHandlingBehavior::Default;
+
+ // 23. If form document has not yet completely loaded, then set historyHandling to "replace".
+ if (!form_document->is_completely_loaded())
+ history_handling = HistoryHandlingBehavior::Replace;
+
+ // 24. Select the appropriate row in the table below based on scheme as given by the first cell of each row.
+ // Then, select the appropriate cell on that row based on method as given in the first cell of each column.
+ // Then, jump to the steps named in that cell and defined below the table.
+
+ // | GET | POST
+ // ------------------------------------------------------
+ // http | Mutate action URL | Submit as entity body
+ // https | Mutate action URL | Submit as entity body
+ // ftp | Get action URL | Get action URL
+ // javascript | Get action URL | Get action URL
+ // data | Mutate action URL | Get action URL
+ // mailto | Mail with headers | Mail as body
+
+ // If scheme is not one of those listed in this table, then the behavior is not defined by this specification.
+ // User agents should, in the absence of another specification defining this, act in a manner analogous to that defined
+ // in this specification for similar schemes.
+
+ // This should have been handled above.
+ VERIFY(method != MethodAttributeState::Dialog);
+
+ if (scheme.is_one_of("http"sv, "https"sv)) {
+ if (method == MethodAttributeState::GET)
+ TRY_OR_THROW_OOM(vm, mutate_action_url(move(parsed_action), move(entry_list), *target_navigable, history_handling));
+ else
+ TRY_OR_THROW_OOM(vm, submit_as_entity_body(move(parsed_action), move(entry_list), encoding_type, move(encoding), *target_navigable, history_handling));
+ } else if (scheme.is_one_of("ftp"sv, "javascript"sv)) {
+ get_action_url(move(parsed_action), *target_navigable, history_handling);
+ } else if (scheme == "data"sv) {
+ if (method == MethodAttributeState::GET)
+ TRY_OR_THROW_OOM(vm, mutate_action_url(move(parsed_action), move(entry_list), *target_navigable, history_handling));
+ else
+ get_action_url(move(parsed_action), *target_navigable, history_handling);
+ } else if (scheme == "mailto"sv) {
+ if (method == MethodAttributeState::GET)
+ TRY_OR_THROW_OOM(vm, mail_with_headers(move(parsed_action), move(entry_list), move(encoding), *target_navigable, history_handling));
+ else
+ TRY_OR_THROW_OOM(vm, mail_as_body(move(parsed_action), move(entry_list), encoding_type, move(encoding), *target_navigable, history_handling));
+ } else {
+ dbgln("Failed to submit form: Unknown scheme: {}", scheme);
+ return {};
+ }
return {};
}
@@ -175,10 +279,7 @@ void HTMLFormElement::reset_form()
WebIDL::ExceptionOr HTMLFormElement::submit()
{
- auto& vm = realm().vm();
-
- TRY_OR_THROW_OOM(vm, submit_form(this, true));
- return {};
+ return submit_form(*this, true);
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-form-reset
@@ -208,17 +309,83 @@ void HTMLFormElement::remove_associated_element(Badge, HT
m_associated_elements.remove_first_matching([&](auto& entry) { return entry.ptr() == &element; });
}
-// https://html.spec.whatwg.org/#dom-fs-action
-DeprecatedString HTMLFormElement::action() const
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fs-action
+DeprecatedString HTMLFormElement::action_from_form_element(JS::NonnullGCPtr element) const
{
- auto value = attribute(HTML::AttributeNames::action);
+ // The action of an element is the value of the element's formaction attribute, if the element is a submit button
+ // and has such an attribute, or the value of its form owner's action attribute, if it has one, or else the empty
+ // string.
+ if (auto const* form_associated_element = dynamic_cast(element.ptr());
+ form_associated_element && form_associated_element->is_submit_button() && element->has_attribute(AttributeNames::formaction))
+ return attribute(AttributeNames::formaction);
- // Return the current URL if the action attribute is null or an empty string
- if (value.is_null() || value.is_empty()) {
- return document().url().to_deprecated_string();
+ if (this->has_attribute(AttributeNames::action))
+ return attribute(AttributeNames::action);
+
+ return DeprecatedString::empty();
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-attributes:attr-fs-method-2
+static HTMLFormElement::MethodAttributeState method_attribute_to_method_state(StringView method)
+{
+#define __ENUMERATE_FORM_METHOD_ATTRIBUTE(keyword, state) \
+ if (Infra::is_ascii_case_insensitive_match(#keyword##sv, method)) \
+ return HTMLFormElement::MethodAttributeState::state;
+ ENUMERATE_FORM_METHOD_ATTRIBUTES
+#undef __ENUMERATE_FORM_METHOD_ATTRIBUTE
+
+ // The method attribute's invalid value default and missing value default are both the GET state.
+ return HTMLFormElement::MethodAttributeState::GET;
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fs-method
+HTMLFormElement::MethodAttributeState HTMLFormElement::method_state_from_form_element(JS::NonnullGCPtr element) const
+{
+ // If the element is a submit button and has a formmethod attribute, then the element's method is that attribute's state;
+ // otherwise, it is the form owner's method attribute's state.
+ if (auto const* form_associated_element = dynamic_cast(element.ptr());
+ form_associated_element && form_associated_element->is_submit_button() && element->has_attribute(AttributeNames::formmethod)) {
+ // NOTE: `formmethod` is the same as `method`, except that it has no missing value default.
+ // This is handled by not calling `method_attribute_to_method_state` in the first place if there is no `formmethod` attribute.
+ return method_attribute_to_method_state(element->attribute(AttributeNames::formmethod));
}
- return value;
+ if (!this->has_attribute(AttributeNames::method))
+ return MethodAttributeState::GET;
+
+ return method_attribute_to_method_state(this->attribute(AttributeNames::method));
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-attributes:attr-fs-enctype-2
+static HTMLFormElement::EncodingTypeAttributeState encoding_type_attribute_to_encoding_type_state(StringView encoding_type)
+{
+#define __ENUMERATE_FORM_METHOD_ENCODING_TYPE(keyword, state) \
+ if (Infra::is_ascii_case_insensitive_match(keyword##sv, encoding_type)) \
+ return HTMLFormElement::EncodingTypeAttributeState::state;
+ ENUMERATE_FORM_METHOD_ENCODING_TYPES
+#undef __ENUMERATE_FORM_METHOD_ENCODING_TYPE
+
+ // The enctype attribute's invalid value default and missing value default are both the application/x-www-form-urlencoded state.
+ return HTMLFormElement::EncodingTypeAttributeState::FormUrlEncoded;
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fs-enctype
+HTMLFormElement::EncodingTypeAttributeState HTMLFormElement::encoding_type_state_from_form_element(JS::NonnullGCPtr element) const
+{
+ // If the element is a submit button and has a formenctype attribute, then the element's enctype is that attribute's state;
+ // otherwise, it is the form owner's enctype attribute's state.
+ if (auto const* form_associated_element = dynamic_cast(element.ptr());
+ form_associated_element && form_associated_element->is_submit_button() && element->has_attribute(AttributeNames::formenctype)) {
+ // NOTE: `formenctype` is the same as `enctype`, except that it has no missing value default.
+ // This is handled by not calling `encoding_type_attribute_to_encoding_type_state` in the first place if there is no
+ // `formenctype` attribute.
+ return encoding_type_attribute_to_encoding_type_state(element->attribute(AttributeNames::formenctype));
+ }
+
+ if (!this->has_attribute(AttributeNames::enctype))
+ return EncodingTypeAttributeState::FormUrlEncoded;
+
+ return encoding_type_attribute_to_encoding_type_state(this->attribute(AttributeNames::enctype));
}
static bool is_form_control(DOM::Element const& element)
@@ -298,4 +465,378 @@ ErrorOr HTMLFormElement::populate_vector_with_submittable_elements_in_tree
return {};
}
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-method
+DeprecatedString HTMLFormElement::method() const
+{
+ // The method and enctype IDL attributes must reflect the respective content attributes of the same name, limited to only known values.
+ auto method_state = method_state_from_form_element(*this);
+ switch (method_state) {
+ case MethodAttributeState::GET:
+ return "get"sv;
+ case MethodAttributeState::POST:
+ return "post"sv;
+ case MethodAttributeState::Dialog:
+ return "dialog"sv;
+ default:
+ VERIFY_NOT_REACHED();
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-method
+WebIDL::ExceptionOr HTMLFormElement::set_method(DeprecatedString const& method)
+{
+ // The method and enctype IDL attributes must reflect the respective content attributes of the same name, limited to only known values.
+ return set_attribute(AttributeNames::method, method);
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-action
+DeprecatedString HTMLFormElement::action() const
+{
+ // The action IDL attribute must reflect the content attribute of the same name, except that on getting, when the
+ // content attribute is missing or its value is the empty string, the element's node document's URL must be returned
+ // instead.
+ if (!has_attribute(AttributeNames::action))
+ return document().url_string();
+
+ auto action_attribute = attribute(AttributeNames::action);
+ if (action_attribute.is_empty())
+ return document().url_string();
+
+ return action_attribute;
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-action
+WebIDL::ExceptionOr HTMLFormElement::set_action(DeprecatedString const& value)
+{
+ return set_attribute(AttributeNames::action, value);
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#picking-an-encoding-for-the-form
+ErrorOr HTMLFormElement::pick_an_encoding() const
+{
+ // 1. Let encoding be the document's character encoding.
+ auto encoding = document().encoding_or_default();
+
+ // 2. If the form element has an accept-charset attribute, set encoding to the return value of running these substeps:
+ if (has_attribute(AttributeNames::accept_charset)) {
+ // 1. Let input be the value of the form element's accept-charset attribute.
+ auto input = attribute(AttributeNames::accept_charset);
+
+ // 2. Let candidate encoding labels be the result of splitting input on ASCII whitespace.
+ auto candidate_encoding_labels = input.split_view(Infra::is_ascii_whitespace);
+
+ // 3. Let candidate encodings be an empty list of character encodings.
+ Vector candidate_encodings;
+
+ // 4. For each token in candidate encoding labels in turn (in the order in which they were found in input),
+ // get an encoding for the token and, if this does not result in failure, append the encoding to candidate
+ // encodings.
+ for (auto const& token : candidate_encoding_labels) {
+ auto candidate_encoding = TextCodec::get_standardized_encoding(token);
+ if (candidate_encoding.has_value())
+ TRY(candidate_encodings.try_append(candidate_encoding.value()));
+ }
+
+ // 5. If candidate encodings is empty, return UTF-8.
+ if (candidate_encodings.is_empty())
+ return "UTF-8"_string;
+
+ // 6. Return the first encoding in candidate encodings.
+ return String::from_utf8(candidate_encodings.first());
+ }
+
+ // 3. Return the result of getting an output encoding from encoding.
+ return String::from_utf8(encoding.view());
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#convert-to-a-list-of-name-value-pairs
+static ErrorOr> convert_to_list_of_name_value_pairs(Vector const& entry_list)
+{
+ // 1. Let list be an empty list of name-value pairs.
+ Vector list;
+
+ // 2. For each entry of entry list:
+ for (auto const& entry : entry_list) {
+ // 1. Let name be entry's name, with every occurrence of U+000D (CR) not followed by U+000A (LF), and every occurrence of U+000A (LF)
+ // not preceded by U+000D (CR), replaced by a string consisting of U+000D (CR) and U+000A (LF).
+ auto name = TRY(normalize_line_breaks(entry.name));
+
+ // 2. If entry's value is a File object, then let value be entry's value's name. Otherwise, let value be entry's value.
+ String value;
+ entry.value.visit(
+ [&value](JS::Handle const& file) {
+ value = file->name();
+ },
+ [&value](String const& string) {
+ value = string;
+ });
+
+ // 3. Replace every occurrence of U+000D (CR) not followed by U+000A (LF), and every occurrence of
+ // U+000A (LF) not preceded by U+000D (CR), in value, by a string consisting of U+000D (CR) and U+000A (LF).
+ auto normalized_value = TRY(normalize_line_breaks(value));
+
+ // 4. Append to list a new name-value pair whose name is name and whose value is value.
+ TRY(list.try_append(URL::QueryParam { .name = move(name), .value = move(normalized_value) }));
+ }
+
+ // 3. Return list.
+ return list;
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#text/plain-encoding-algorithm
+static ErrorOr plain_text_encode(Vector const& pairs)
+{
+ // 1. Let result be the empty string.
+ StringBuilder result;
+
+ // 2. For each pair in pairs:
+ for (auto const& pair : pairs) {
+ // 1. Append pair's name to result.
+ TRY(result.try_append(pair.name));
+
+ // 2. Append a single U+003D EQUALS SIGN character (=) to result.
+ TRY(result.try_append('='));
+
+ // 3. Append pair's value to result.
+ TRY(result.try_append(pair.value));
+
+ // 4. Append a U+000D CARRIAGE RETURN (CR) U+000A LINE FEED (LF) character pair to result.
+ TRY(result.try_append("\r\n"sv));
+ }
+
+ // 3. Return result.
+ return result.to_string();
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
+ErrorOr HTMLFormElement::mutate_action_url(AK::URL parsed_action, Vector entry_list, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling)
+{
+ // 1. Let pairs be the result of converting to a list of name-value pairs with entry list.
+ auto pairs = TRY(convert_to_list_of_name_value_pairs(entry_list));
+
+ // 2. Let query be the result of running the application/x-www-form-urlencoded serializer with pairs and encoding.
+ // FIXME: Pass in encoding.
+ auto query = TRY(url_encode(pairs, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
+
+ // 3. Set parsed action's query component to query.
+ parsed_action.set_query(query.to_deprecated_string());
+
+ // 4. Plan to navigate to parsed action.
+ plan_to_navigate_to(move(parsed_action), target_navigable, history_handling);
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body
+ErrorOr HTMLFormElement::submit_as_entity_body(AK::URL parsed_action, Vector entry_list, EncodingTypeAttributeState encoding_type, [[maybe_unused]] String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling)
+{
+ // 1. Assert: method is POST.
+
+ ByteBuffer mime_type;
+ ByteBuffer body;
+
+ // 2. Switch on enctype:
+ switch (encoding_type) {
+ case EncodingTypeAttributeState::FormUrlEncoded: {
+ // -> application/x-www-form-urlencoded
+ // 1. Let pairs be the result of converting to a list of name-value pairs with entry list.
+ auto pairs = TRY(convert_to_list_of_name_value_pairs(entry_list));
+
+ // 2. Let body be the result of running the application/x-www-form-urlencoded serializer with pairs and encoding.
+ // FIXME: Pass in encoding.
+ body = TRY(ByteBuffer::copy(TRY(url_encode(pairs, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded)).bytes()));
+
+ // 3. Set body to the result of encoding body.
+ // NOTE: `encoding` refers to `UTF-8 encode`, which body already is encoded as because it uses AK::String.
+
+ // 4. Let mimeType be `application/x-www-form-urlencoded`.
+ mime_type = TRY(ByteBuffer::copy("application/x-www-form-urlencoded"sv.bytes()));
+ break;
+ }
+ case EncodingTypeAttributeState::FormData: {
+ // -> multipart/form-data
+ // 1. Let body be the result of running the multipart/form-data encoding algorithm with entry list and encoding.
+ auto body_and_mime_type = TRY(serialize_to_multipart_form_data(entry_list));
+ body = move(body_and_mime_type.serialized_data);
+
+ // 2. Let mimeType be the isomorphic encoding of the concatenation of "multipart/form-data; boundary=" and the multipart/form-data
+ // boundary string generated by the multipart/form-data encoding algorithm.
+ mime_type = TRY(ByteBuffer::copy(TRY(String::formatted("multipart/form-data; boundary={}", body_and_mime_type.boundary)).bytes()));
+ return {};
+ }
+ case EncodingTypeAttributeState::PlainText: {
+ // -> text/plain
+ // 1. Let pairs be the result of converting to a list of name-value pairs with entry list.
+ auto pairs = TRY(convert_to_list_of_name_value_pairs(entry_list));
+
+ // 2. Let body be the result of running the text/plain encoding algorithm with pairs.
+ body = TRY(ByteBuffer::copy(TRY(plain_text_encode(pairs)).bytes()));
+
+ // FIXME: 3. Set body to the result of encoding body using encoding.
+
+ // 4. Let mimeType be `text/plain`.
+ mime_type = TRY(ByteBuffer::copy("text/plain"sv.bytes()));
+ break;
+ }
+ default:
+ VERIFY_NOT_REACHED();
+ }
+
+ // FIXME: Update this to the navigable version.
+ // 3. Plan to navigate to a new request whose url is parsed action, method is method, header list consists of `Content-Type`/MIME type,
+ // and body is body.
+ auto request = Fetch::Infrastructure::Request::create(vm());
+ request->set_url(move(parsed_action));
+ request->set_method(TRY(ByteBuffer::copy("POST"sv.bytes())));
+ request->set_body(move(body));
+
+ auto temp_header = Fetch::Infrastructure::Header {
+ .name = TRY(ByteBuffer::copy("Content-Type"sv.bytes())),
+ .value = move(mime_type),
+ };
+ TRY(request->header_list()->append(move(temp_header)));
+ plan_to_navigate_to(request, target_navigable, history_handling);
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-get-action
+void HTMLFormElement::get_action_url(AK::URL parsed_action, JS::NonnullGCPtr target_navigable, Web::HTML::HistoryHandlingBehavior history_handling)
+{
+ // 1. Plan to navigate to parsed action.
+ // Spec Note: entry list is discarded.
+ plan_to_navigate_to(move(parsed_action), target_navigable, history_handling);
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mailto-headers
+ErrorOr HTMLFormElement::mail_with_headers(AK::URL parsed_action, Vector entry_list, [[maybe_unused]] String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling)
+{
+ // 1. Let pairs be the result of converting to a list of name-value pairs with entry list.
+ auto pairs = TRY(convert_to_list_of_name_value_pairs(entry_list));
+
+ // 2. Let headers be the result of running the application/x-www-form-urlencoded serializer with pairs and encoding.
+ // FIXME: Pass in encoding.
+ auto headers = TRY(url_encode(pairs, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
+
+ // 3. Replace occurrences of U+002B PLUS SIGN characters (+) in headers with the string "%20".
+ TRY(headers.replace("+"sv, "%20"sv, ReplaceMode::All));
+
+ // 4. Set parsed action's query to headers.
+ parsed_action.set_query(headers.to_deprecated_string());
+
+ // 5. Plan to navigate to parsed action.
+ plan_to_navigate_to(move(parsed_action), target_navigable, history_handling);
+ return {};
+}
+
+ErrorOr HTMLFormElement::mail_as_body(AK::URL parsed_action, Vector entry_list, EncodingTypeAttributeState encoding_type, [[maybe_unused]] String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling)
+{
+ // 1. Let pairs be the result of converting to a list of name-value pairs with entry list.
+ auto pairs = TRY(convert_to_list_of_name_value_pairs(entry_list));
+
+ String body;
+
+ // 2. Switch on enctype:
+ switch (encoding_type) {
+ case EncodingTypeAttributeState::PlainText: {
+ // -> text/plain
+ // 1. Let body be the result of running the text/plain encoding algorithm with pairs.
+ body = TRY(plain_text_encode(pairs));
+
+ // 2. Set body to the result of running UTF-8 percent-encode on body using the default encode set. [URL]
+ // NOTE: body is already UTF-8 encoded due to using AK::String, so we only have to do the percent encoding.
+ // NOTE: "default encode set" links to "path percent-encode-set": https://url.spec.whatwg.org/#default-encode-set
+ auto percent_encoded_body = AK::URL::percent_encode(body, AK::URL::PercentEncodeSet::Path);
+ body = TRY(String::from_utf8(percent_encoded_body.view()));
+ break;
+ }
+ default:
+ // -> Otherwise
+ // Let body be the result of running the application/x-www-form-urlencoded serializer with pairs and encoding.
+ // FIXME: Pass in encoding.
+ body = TRY(url_encode(pairs, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
+ break;
+ }
+
+ // 3. If parsed action's query is null, then set it to the empty string.
+ if (parsed_action.query().is_null())
+ parsed_action.set_query(DeprecatedString::empty());
+
+ StringBuilder query_builder;
+
+ TRY(query_builder.try_append(parsed_action.query()));
+
+ // 4. If parsed action's query is not the empty string, then append a single U+0026 AMPERSAND character (&) to it.
+ if (!parsed_action.query().is_empty())
+ TRY(query_builder.try_append('&'));
+
+ // 5. Append "body=" to parsed action's query.
+ TRY(query_builder.try_append("body="sv));
+
+ // 6. Append body to parsed action's query.
+ TRY(query_builder.try_append(body));
+
+ parsed_action.set_query(query_builder.to_deprecated_string());
+
+ // 7. Plan to navigate to parsed action.
+ plan_to_navigate_to(move(parsed_action), target_navigable, history_handling);
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plan-to-navigate
+void HTMLFormElement::plan_to_navigate_to(Variant> resource, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling)
+{
+ // FIXME: Update this to the navigable version.
+
+ // 1. Let referrerPolicy be the empty string.
+ Optional referrer_policy;
+
+ // 2. If the form element's link types include the noreferrer keyword, then set referrerPolicy to "no-referrer".
+ auto rel = attribute(HTML::AttributeNames::rel).to_lowercase();
+ auto link_types = rel.view().split_view_if(Infra::is_ascii_whitespace);
+ if (link_types.contains_slow("noreferrer"sv))
+ referrer_policy = ReferrerPolicy::ReferrerPolicy::NoReferrer;
+
+ // 3. If the form has a non-null planned navigation, remove it from its task queue.
+ if (m_planned_navigation) {
+ HTML::main_thread_event_loop().task_queue().remove_tasks_matching([this](Task const& task) {
+ return &task == m_planned_navigation;
+ });
+ }
+
+ auto actual_resource = resource.visit(
+ [this](AK::URL url) {
+ // NOTE: BrowsingContext::navigate is supposed to do this for us, however it currently doesn't.
+ // This will eventually be replaced with URL + POST-resource when updated to the navigable version however,
+ // so it's not worth changing BrowsingContext::navigate.
+ auto request = Fetch::Infrastructure::Request::create(vm());
+ request->set_url(url);
+ return request;
+ },
+ [](JS::NonnullGCPtr request) {
+ return request;
+ });
+
+ actual_resource->set_referrer_policy(move(referrer_policy));
+
+ // 4. Queue an element task on the DOM manipulation task source given the form element and the following steps:
+ // NOTE: `this`, `actual_resource` and `target_navigable` are protected by JS::SafeFunction.
+ queue_an_element_task(Task::Source::DOMManipulation, [this, actual_resource, target_navigable, history_handling]() {
+ // 1. Set the form's planned navigation to null.
+ m_planned_navigation = nullptr;
+
+ // FIXME: 2. Navigate targetNavigable to url using the form element's node document, with historyHandling set to historyHandling,
+ // referrerPolicy set to referrerPolicy, documentResource set to postResource, and cspNavigationType set to "form-submission".
+ // Browsing Context version:
+ // Navigate target browsing context to destination. If replace is true, then target browsing context must be navigated with
+ // replacement enabled.
+ // NOTE: This uses the current node document's browsing context, as the submission events or any code run after planning the navigation
+ // could have adopted the node to a different document.
+ VERIFY(document().browsing_context());
+ MUST(target_navigable->navigate(actual_resource, *document().browsing_context(), false, history_handling));
+ });
+
+ // 5. Set the form's planned navigation to the just-queued task.
+ m_planned_navigation = HTML::main_thread_event_loop().task_queue().last_added_task();
+ VERIFY(m_planned_navigation);
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h
index a2ca9f1985..9ac43a4608 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLFormElement.h
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2018-2021, Andreas Kling
* Copyright (c) 2023, Kenneth Myhra
+ * Copyright (c) 2023, Luke Wilde
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -8,21 +9,50 @@
#pragma once
#include
+#include
#include
#include
+#include
namespace Web::HTML {
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method
+#define ENUMERATE_FORM_METHOD_ATTRIBUTES \
+ __ENUMERATE_FORM_METHOD_ATTRIBUTE(get, GET) \
+ __ENUMERATE_FORM_METHOD_ATTRIBUTE(post, POST) \
+ __ENUMERATE_FORM_METHOD_ATTRIBUTE(dialog, Dialog)
+
+// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-enctype
+#define ENUMERATE_FORM_METHOD_ENCODING_TYPES \
+ __ENUMERATE_FORM_METHOD_ENCODING_TYPE("application/x-www-form-urlencoded", FormUrlEncoded) \
+ __ENUMERATE_FORM_METHOD_ENCODING_TYPE("multipart/form-data", FormData) \
+ __ENUMERATE_FORM_METHOD_ENCODING_TYPE("text/plain", PlainText)
+
class HTMLFormElement final : public HTMLElement {
WEB_PLATFORM_OBJECT(HTMLFormElement, HTMLElement);
public:
virtual ~HTMLFormElement() override;
- DeprecatedString action() const;
- DeprecatedString method() const { return attribute(HTML::AttributeNames::method); }
+ DeprecatedString action_from_form_element(JS::NonnullGCPtr element) const;
- ErrorOr submit_form(JS::GCPtr submitter, bool from_submit_binding = false);
+ enum class MethodAttributeState {
+#define __ENUMERATE_FORM_METHOD_ATTRIBUTE(_, state) state,
+ ENUMERATE_FORM_METHOD_ATTRIBUTES
+#undef __ENUMERATE_FORM_METHOD_ATTRIBUTE
+ };
+
+ MethodAttributeState method_state_from_form_element(JS::NonnullGCPtr element) const;
+
+ enum class EncodingTypeAttributeState {
+#define __ENUMERATE_FORM_METHOD_ENCODING_TYPE(_, state) state,
+ ENUMERATE_FORM_METHOD_ENCODING_TYPES
+#undef __ENUMERATE_FORM_METHOD_ENCODING_TYPE
+ };
+
+ EncodingTypeAttributeState encoding_type_state_from_form_element(JS::NonnullGCPtr element) const;
+
+ WebIDL::ExceptionOr submit_form(JS::NonnullGCPtr submitter, bool from_submit_binding = false);
void reset_form();
@@ -50,6 +80,12 @@ public:
bool constructing_entry_list() const { return m_constructing_entry_list; }
void set_constructing_entry_list(bool value) { m_constructing_entry_list = value; }
+ DeprecatedString method() const;
+ WebIDL::ExceptionOr set_method(DeprecatedString const&);
+
+ DeprecatedString action() const;
+ WebIDL::ExceptionOr set_action(DeprecatedString const&);
+
private:
HTMLFormElement(DOM::Document&, DOM::QualifiedName);
@@ -58,6 +94,15 @@ private:
ErrorOr populate_vector_with_submittable_elements_in_tree_order(JS::NonnullGCPtr element, Vector>& elements);
+ ErrorOr pick_an_encoding() const;
+
+ ErrorOr mutate_action_url(AK::URL parsed_action, Vector entry_list, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+ ErrorOr submit_as_entity_body(AK::URL parsed_action, Vector entry_list, EncodingTypeAttributeState encoding_type, String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+ void get_action_url(AK::URL parsed_action, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+ ErrorOr mail_with_headers(AK::URL parsed_action, Vector entry_list, String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+ ErrorOr mail_as_body(AK::URL parsed_action, Vector entry_list, EncodingTypeAttributeState encoding_type, String encoding, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+ void plan_to_navigate_to(Variant> resource, JS::NonnullGCPtr target_navigable, HistoryHandlingBehavior history_handling);
+
bool m_firing_submission_events { false };
// https://html.spec.whatwg.org/multipage/forms.html#locked-for-reset
@@ -68,6 +113,11 @@ private:
JS::GCPtr mutable m_elements;
bool m_constructing_entry_list { false };
+
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#planned-navigation
+ // Each form element has a planned navigation, which is either null or a task; when the form is first created,
+ // its planned navigation must be set to null.
+ Task const* m_planned_navigation { nullptr };
};
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
index 636e58631f..5515d2368b 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
@@ -244,7 +244,7 @@ WebIDL::ExceptionOr HTMLInputElement::show_picker()
}
// https://html.spec.whatwg.org/multipage/input.html#input-activation-behavior
-ErrorOr HTMLInputElement::run_input_activation_behavior()
+WebIDL::ExceptionOr HTMLInputElement::run_input_activation_behavior()
{
if (type_state() == TypeAttributeState::Checkbox || type_state() == TypeAttributeState::RadioButton) {
// 1. If the element is not connected, then return.
@@ -272,7 +272,7 @@ ErrorOr HTMLInputElement::run_input_activation_behavior()
return {};
// 3. Submit the form owner from the element.
- TRY(form->submit_form(this));
+ TRY(form->submit_form(*this));
} else if (type_state() == TypeAttributeState::FileUpload) {
show_the_picker_if_applicable(*this);
} else {
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
index 94dc226a45..818c684e9c 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h
@@ -160,7 +160,7 @@ private:
static TypeAttributeState parse_type_attribute(StringView);
void create_shadow_tree_if_needed();
- ErrorOr run_input_activation_behavior();
+ WebIDL::ExceptionOr run_input_activation_behavior();
void set_checked_within_group();
// https://html.spec.whatwg.org/multipage/input.html#value-sanitization-algorithm