From f04d1d493d2cece48ae1d4ae6c9ff867778f2585 Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Sun, 18 Jun 2023 16:30:23 +0100 Subject: [PATCH] LibWeb: Bring form submission more up to spec The main missing things are: - Dialog submission - Form validation - Encoding URLs in the form element's encoding - Navigables --- .../Libraries/LibWeb/HTML/AttributeNames.h | 4 + .../LibWeb/HTML/HTMLButtonElement.cpp | 2 +- .../Libraries/LibWeb/HTML/HTMLFormElement.cpp | 673 ++++++++++++++++-- .../Libraries/LibWeb/HTML/HTMLFormElement.h | 56 +- .../LibWeb/HTML/HTMLInputElement.cpp | 4 +- .../Libraries/LibWeb/HTML/HTMLInputElement.h | 2 +- 6 files changed, 668 insertions(+), 73 deletions(-) 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