Serenity Operating System
1/*
2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2023, Kenneth Myhra <kennethmyhra@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/StringBuilder.h>
9#include <LibWeb/DOM/Document.h>
10#include <LibWeb/DOM/Event.h>
11#include <LibWeb/HTML/BrowsingContext.h>
12#include <LibWeb/HTML/EventNames.h>
13#include <LibWeb/HTML/HTMLButtonElement.h>
14#include <LibWeb/HTML/HTMLFieldSetElement.h>
15#include <LibWeb/HTML/HTMLFormElement.h>
16#include <LibWeb/HTML/HTMLInputElement.h>
17#include <LibWeb/HTML/HTMLObjectElement.h>
18#include <LibWeb/HTML/HTMLOutputElement.h>
19#include <LibWeb/HTML/HTMLSelectElement.h>
20#include <LibWeb/HTML/HTMLTextAreaElement.h>
21#include <LibWeb/HTML/SubmitEvent.h>
22#include <LibWeb/Page/Page.h>
23#include <LibWeb/URL/URL.h>
24
25namespace Web::HTML {
26
27HTMLFormElement::HTMLFormElement(DOM::Document& document, DOM::QualifiedName qualified_name)
28 : HTMLElement(document, move(qualified_name))
29{
30}
31
32HTMLFormElement::~HTMLFormElement() = default;
33
34JS::ThrowCompletionOr<void> HTMLFormElement::initialize(JS::Realm& realm)
35{
36 MUST_OR_THROW_OOM(Base::initialize(realm));
37 set_prototype(&Bindings::ensure_web_prototype<Bindings::HTMLFormElementPrototype>(realm, "HTMLFormElement"));
38
39 return {};
40}
41
42void HTMLFormElement::visit_edges(Cell::Visitor& visitor)
43{
44 Base::visit_edges(visitor);
45 visitor.visit(m_elements);
46 for (auto& element : m_associated_elements)
47 visitor.visit(element.ptr());
48}
49
50ErrorOr<void> HTMLFormElement::submit_form(JS::GCPtr<HTMLElement> submitter, bool from_submit_binding)
51{
52 if (cannot_navigate())
53 return {};
54
55 if (action().is_null()) {
56 dbgln("Unsupported form action ''");
57 return {};
58 }
59
60 auto effective_method = method().to_lowercase();
61
62 if (effective_method == "dialog") {
63 dbgln("Failed to submit form: Unsupported form method '{}'", method());
64 return {};
65 }
66
67 if (effective_method != "get" && effective_method != "post") {
68 effective_method = "get";
69 }
70
71 if (!from_submit_binding) {
72 if (m_firing_submission_events)
73 return {};
74
75 m_firing_submission_events = true;
76
77 // FIXME: If the submitter element's no-validate state is false...
78
79 JS::GCPtr<HTMLElement> submitter_button;
80
81 if (submitter != this)
82 submitter_button = submitter;
83
84 SubmitEventInit event_init {};
85 event_init.submitter = submitter_button;
86 auto submit_event = SubmitEvent::create(realm(), String::from_deprecated_string(EventNames::submit).release_value_but_fixme_should_propagate_errors(), event_init).release_value_but_fixme_should_propagate_errors();
87 submit_event->set_bubbles(true);
88 submit_event->set_cancelable(true);
89 bool continue_ = dispatch_event(*submit_event);
90
91 m_firing_submission_events = false;
92
93 if (!continue_)
94 return {};
95
96 // This is checked again because arbitrary JS may have run when handling submit,
97 // which may have changed the result.
98 if (cannot_navigate())
99 return {};
100 }
101
102 AK::URL url(document().parse_url(action()));
103
104 if (!url.is_valid()) {
105 dbgln("Failed to submit form: Invalid URL: {}", action());
106 return {};
107 }
108
109 if (url.scheme() == "file") {
110 if (document().url().scheme() != "file") {
111 dbgln("Failed to submit form: Security violation: {} may not submit to {}", document().url(), url);
112 return {};
113 }
114 if (effective_method != "get") {
115 dbgln("Failed to submit form: Unsupported form method '{}' for URL: {}", method(), url);
116 return {};
117 }
118 } else if (url.scheme() != "http" && url.scheme() != "https") {
119 dbgln("Failed to submit form: Unsupported protocol for URL: {}", url);
120 return {};
121 }
122
123 Vector<URL::QueryParam> parameters;
124
125 for_each_in_inclusive_subtree_of_type<HTMLInputElement>([&](auto& input) {
126 if (!input.name().is_null() && (input.type() != "submit" || &input == submitter)) {
127 auto name = String::from_deprecated_string(input.name()).release_value_but_fixme_should_propagate_errors();
128 auto value = String::from_deprecated_string(input.value()).release_value_but_fixme_should_propagate_errors();
129 parameters.append({ move(name), move(value) });
130 }
131 return IterationDecision::Continue;
132 });
133
134 if (effective_method == "get") {
135 auto url_encoded_parameters = TRY(url_encode(parameters, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded)).to_deprecated_string();
136 url.set_query(move(url_encoded_parameters));
137 }
138
139 LoadRequest request = LoadRequest::create_for_url_on_page(url, document().page());
140
141 if (effective_method == "post") {
142 auto url_encoded_parameters = TRY(url_encode(parameters, AK::URL::PercentEncodeSet::ApplicationXWWWFormUrlencoded));
143 auto body = TRY(ByteBuffer::copy(url_encoded_parameters.bytes()));
144 request.set_method("POST");
145 request.set_header("Content-Type", "application/x-www-form-urlencoded");
146 request.set_body(move(body));
147 }
148
149 if (auto* page = document().page())
150 page->load(request);
151
152 return {};
153}
154
155// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#resetting-a-form
156void HTMLFormElement::reset_form()
157{
158 // 1. Let reset be the result of firing an event named reset at form, with the bubbles and cancelable attributes initialized to true.
159 auto reset_event = DOM::Event::create(realm(), HTML::EventNames::reset).release_value_but_fixme_should_propagate_errors();
160 reset_event->set_bubbles(true);
161 reset_event->set_cancelable(true);
162
163 bool reset = dispatch_event(reset_event);
164
165 // 2. If reset is true, then invoke the reset algorithm of each resettable element whose form owner is form.
166 if (reset) {
167 for (auto element : m_associated_elements) {
168 VERIFY(is<FormAssociatedElement>(*element));
169 auto& form_associated_element = dynamic_cast<FormAssociatedElement&>(*element);
170 if (form_associated_element.is_resettable())
171 form_associated_element.reset_algorithm();
172 }
173 }
174}
175
176WebIDL::ExceptionOr<void> HTMLFormElement::submit()
177{
178 auto& vm = realm().vm();
179
180 TRY_OR_THROW_OOM(vm, submit_form(this, true));
181 return {};
182}
183
184// https://html.spec.whatwg.org/multipage/forms.html#dom-form-reset
185void HTMLFormElement::reset()
186{
187 // 1. If the form element is marked as locked for reset, then return.
188 if (m_locked_for_reset)
189 return;
190
191 // 2. Mark the form element as locked for reset.
192 m_locked_for_reset = true;
193
194 // 3. Reset the form element.
195 reset_form();
196
197 // 4. Unmark the form element as locked for reset.
198 m_locked_for_reset = false;
199}
200
201void HTMLFormElement::add_associated_element(Badge<FormAssociatedElement>, HTMLElement& element)
202{
203 m_associated_elements.append(element);
204}
205
206void HTMLFormElement::remove_associated_element(Badge<FormAssociatedElement>, HTMLElement& element)
207{
208 m_associated_elements.remove_first_matching([&](auto& entry) { return entry.ptr() == &element; });
209}
210
211// https://html.spec.whatwg.org/#dom-fs-action
212DeprecatedString HTMLFormElement::action() const
213{
214 auto value = attribute(HTML::AttributeNames::action);
215
216 // Return the current URL if the action attribute is null or an empty string
217 if (value.is_null() || value.is_empty()) {
218 return document().url().to_deprecated_string();
219 }
220
221 return value;
222}
223
224static bool is_form_control(DOM::Element const& element)
225{
226 if (is<HTMLButtonElement>(element)
227 || is<HTMLFieldSetElement>(element)
228 || is<HTMLObjectElement>(element)
229 || is<HTMLOutputElement>(element)
230 || is<HTMLSelectElement>(element)
231 || is<HTMLTextAreaElement>(element)) {
232 return true;
233 }
234
235 if (is<HTMLInputElement>(element)
236 && !element.get_attribute(HTML::AttributeNames::type).equals_ignoring_ascii_case("image"sv)) {
237 return true;
238 }
239
240 return false;
241}
242
243// https://html.spec.whatwg.org/multipage/forms.html#dom-form-elements
244JS::NonnullGCPtr<DOM::HTMLCollection> HTMLFormElement::elements() const
245{
246 if (!m_elements) {
247 m_elements = DOM::HTMLCollection::create(const_cast<HTMLFormElement&>(*this), [](Element const& element) {
248 return is_form_control(element);
249 }).release_value_but_fixme_should_propagate_errors();
250 }
251 return *m_elements;
252}
253
254// https://html.spec.whatwg.org/multipage/forms.html#dom-form-length
255unsigned HTMLFormElement::length() const
256{
257 // The length IDL attribute must return the number of nodes represented by the elements collection.
258 return elements()->length();
259}
260
261// https://html.spec.whatwg.org/multipage/forms.html#category-submit
262ErrorOr<Vector<JS::NonnullGCPtr<DOM::Element>>> HTMLFormElement::get_submittable_elements()
263{
264 Vector<JS::NonnullGCPtr<DOM::Element>> submittable_elements = {};
265 for (size_t i = 0; i < elements()->length(); i++) {
266 auto* element = elements()->item(i);
267 TRY(populate_vector_with_submittable_elements_in_tree_order(*element, submittable_elements));
268 }
269 return submittable_elements;
270}
271
272ErrorOr<void> HTMLFormElement::populate_vector_with_submittable_elements_in_tree_order(JS::NonnullGCPtr<DOM::Element> element, Vector<JS::NonnullGCPtr<DOM::Element>>& elements)
273{
274 if (auto* form_associated_element = dynamic_cast<HTML::FormAssociatedElement*>(element.ptr())) {
275 if (form_associated_element->is_submittable())
276 TRY(elements.try_append(element));
277 }
278
279 for (size_t i = 0; i < element->children()->length(); i++) {
280 auto* child = element->children()->item(i);
281 TRY(populate_vector_with_submittable_elements_in_tree_order(*child, elements));
282 }
283
284 return {};
285}
286
287}