diff options
author | Kenneth Myhra <kennethmyhra@gmail.com> | 2023-02-03 21:50:05 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2023-02-12 00:18:09 +0000 |
commit | d5b5b94a3569036d93726f25b778ea8c8a6d1af2 (patch) | |
tree | 910b8385e430fd43eb7b923297b56dc4b3bb3a41 /Userland/Libraries | |
parent | b74d5a423deeb0a9e504f6f3dece6b3472152bc4 (diff) | |
download | serenity-d5b5b94a3569036d93726f25b778ea8c8a6d1af2.zip |
LibWeb: Introduce the FormData interface from the XHR specification
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/LibWeb/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Forward.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.cpp | 44 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.h | 20 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/FormData.cpp | 172 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/FormData.h | 50 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/FormData.idl | 23 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/idl_files.cmake | 1 |
8 files changed, 313 insertions, 0 deletions
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index a1bd191deb..88f8a7cf01 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -185,6 +185,7 @@ set(SOURCES HTML/EventNames.cpp HTML/Focus.cpp HTML/FormAssociatedElement.cpp + HTML/FormControlInfrastructure.cpp HTML/GlobalEventHandlers.cpp HTML/History.cpp HTML/HTMLAnchorElement.cpp @@ -466,6 +467,7 @@ set(SOURCES WebIDL/Promise.cpp WebSockets/WebSocket.cpp XHR/EventNames.cpp + XHR/FormData.cpp XHR/ProgressEvent.cpp XHR/XMLHttpRequest.cpp XHR/XMLHttpRequestEventTarget.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index e528172c86..85f5af61b2 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -480,6 +480,7 @@ class WebGLRenderingContextBase; } namespace Web::XHR { +class FormData; class ProgressEvent; class XMLHttpRequest; class XMLHttpRequestEventTarget; diff --git a/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.cpp b/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.cpp new file mode 100644 index 0000000000..4718215da0 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, Kenneth Myhra <kennethmyhra@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibWeb/HTML/FormControlInfrastructure.h> +#include <LibWeb/Infra/Strings.h> + +namespace Web::HTML { + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry +WebIDL::ExceptionOr<Entry> create_entry(JS::Realm& realm, String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename) +{ + auto& vm = realm.vm(); + + // 1. Set name to the result of converting name into a scalar value string. + auto entry_name = TRY_OR_THROW_OOM(realm.vm(), Infra::convert_to_scalar_value_string(name)); + + auto entry_value = TRY(value.visit( + // 2. If value is a string, then set value to the result of converting value into a scalar value string. + [&](String const& string) -> WebIDL::ExceptionOr<Variant<JS::NonnullGCPtr<FileAPI::File>, String>> { + return TRY_OR_THROW_OOM(vm, Infra::convert_to_scalar_value_string(string)); + }, + // 3. Otherwise: + [&](JS::NonnullGCPtr<FileAPI::Blob> const& blob) -> WebIDL::ExceptionOr<Variant<JS::NonnullGCPtr<FileAPI::File>, String>> { + // 1. If value is not a File object, then set value to a new File object, representing the same bytes, whose name attribute value is "blob". + // 2. If filename is given, then set value to a new File object, representing the same bytes, whose name attribute is filename. + String name_attribute; + if (filename.has_value()) + name_attribute = filename.value(); + else + name_attribute = TRY_OR_THROW_OOM(vm, String::from_utf8("blob"sv)); + return TRY(FileAPI::File::create(realm, { JS::make_handle(*blob) }, name_attribute.to_deprecated_string(), {})); + })); + + // 4. Return an entry whose name is name and whose value is value. + return Entry { + .name = move(entry_name), + .value = move(entry_value), + }; +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.h b/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.h new file mode 100644 index 0000000000..8e247c967c --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/FormControlInfrastructure.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, Kenneth Myhra <kennethmyhra@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibWeb/XHR/FormData.h> + +namespace Web::HTML { + +struct Entry { + String name; + Variant<JS::NonnullGCPtr<FileAPI::File>, String> value; +}; + +WebIDL::ExceptionOr<Entry> create_entry(JS::Realm& realm, String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename = {}); + +} diff --git a/Userland/Libraries/LibWeb/XHR/FormData.cpp b/Userland/Libraries/LibWeb/XHR/FormData.cpp new file mode 100644 index 0000000000..3cf994acce --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/FormData.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023, Kenneth Myhra <kennethmyhra@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/TypeCasts.h> +#include <LibJS/Runtime/Completion.h> +#include <LibWeb/Bindings/Intrinsics.h> +#include <LibWeb/FileAPI/Blob.h> +#include <LibWeb/FileAPI/File.h> +#include <LibWeb/HTML/FormControlInfrastructure.h> +#include <LibWeb/WebIDL/DOMException.h> +#include <LibWeb/XHR/FormData.h> + +namespace Web::XHR { + +// https://xhr.spec.whatwg.org/#dom-formdata +WebIDL::ExceptionOr<JS::NonnullGCPtr<FormData>> FormData::construct_impl(JS::Realm& realm, Optional<JS::NonnullGCPtr<HTML::HTMLFormElement>>) +{ + // FIXME: 1. If form is given, then: + // FIXME: 1. Let list be the result of constructing the entry list for form. + // FIXME: 2. If list is null, then throw an "InvalidStateError" DOMException. + // FIXME: 3. Set this’s entry list to list. + + return construct_impl(realm); +} + +WebIDL::ExceptionOr<JS::NonnullGCPtr<FormData>> FormData::construct_impl(JS::Realm& realm, HashMap<DeprecatedString, Vector<FormDataEntryValue>> entry_list) +{ + return MUST_OR_THROW_OOM(realm.heap().allocate<FormData>(realm, realm, move(entry_list))); +} + +FormData::FormData(JS::Realm& realm, HashMap<DeprecatedString, Vector<FormDataEntryValue>> entry_list) + : PlatformObject(realm) + , m_entry_list(move(entry_list)) +{ + set_prototype(&Bindings::ensure_web_prototype<Bindings::FormDataPrototype>(realm, "FormData")); +} + +FormData::~FormData() = default; + +void FormData::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + for (auto const& entry : m_entry_list) { + for (auto const& value : entry.value) { + if (auto* file = value.get_pointer<JS::NonnullGCPtr<FileAPI::File>>()) + visitor.visit(*file); + } + } +} + +// https://xhr.spec.whatwg.org/#dom-formdata-append +WebIDL::ExceptionOr<void> FormData::append(DeprecatedString const& name, DeprecatedString const& value) +{ + auto& vm = realm().vm(); + return append_impl(TRY_OR_THROW_OOM(vm, String::from_deprecated_string(name)), TRY_OR_THROW_OOM(vm, String::from_deprecated_string(value))); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-append-blob +WebIDL::ExceptionOr<void> FormData::append(DeprecatedString const& name, JS::NonnullGCPtr<FileAPI::Blob> const& blob_value, Optional<DeprecatedString> const& filename) +{ + auto& vm = realm().vm(); + auto inner_filename = filename.has_value() ? TRY_OR_THROW_OOM(vm, String::from_deprecated_string(filename.value())) : Optional<String> {}; + return append_impl(TRY_OR_THROW_OOM(vm, String::from_deprecated_string(name)), blob_value, inner_filename); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-append +// https://xhr.spec.whatwg.org/#dom-formdata-append-blob +WebIDL::ExceptionOr<void> FormData::append_impl(String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename) +{ + auto& realm = this->realm(); + auto& vm = realm.vm(); + + // 1. Let value be value if given; otherwise blobValue. + // 2. Let entry be the result of creating an entry with name, value, and filename if given. + auto entry = TRY(HTML::create_entry(realm, name, value, filename)); + + // FIXME: Remove this when our binding generator supports "new string". + auto form_data_entry_value = entry.value.has<String>() + ? FormDataEntryValue { entry.value.get<String>().to_deprecated_string() } + : FormDataEntryValue { entry.value.get<JS::NonnullGCPtr<FileAPI::File>>() }; + + // 3. Append entry to this’s entry list. + if (auto entries = m_entry_list.get(entry.name.to_deprecated_string()); entries.has_value() && !entries->is_empty()) + TRY_OR_THROW_OOM(vm, entries->try_append(form_data_entry_value)); + else + TRY_OR_THROW_OOM(vm, m_entry_list.try_set(entry.name.to_deprecated_string(), { form_data_entry_value })); + + return {}; +} + +// https://xhr.spec.whatwg.org/#dom-formdata-delete +void FormData::delete_(DeprecatedString const& name) +{ + // The delete(name) method steps are to remove all entries whose name is name from this’s entry list. + m_entry_list.remove(name); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-get +Variant<JS::NonnullGCPtr<FileAPI::File>, DeprecatedString, Empty> FormData::get(DeprecatedString const& name) +{ + // 1. If there is no entry whose name is name in this’s entry list, then return null. + if (!m_entry_list.contains(name)) + return Empty {}; + // 2. Return the value of the first entry whose name is name from this’s entry list. + return m_entry_list.get(name)->at(0); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-getall +Vector<FormDataEntryValue> FormData::get_all(DeprecatedString const& name) +{ + // 1. If there is no entry whose name is name in this’s entry list, then return the empty list. + if (!m_entry_list.contains(name)) + return {}; + // 2. Return the values of all entries whose name is name, in order, from this’s entry list. + return *m_entry_list.get(name); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-has +bool FormData::has(DeprecatedString const& name) +{ + // The has(name) method steps are to return true if there is an entry whose name is name in this’s entry list; otherwise false. + return m_entry_list.contains(name); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-set +WebIDL::ExceptionOr<void> FormData::set(DeprecatedString const& name, DeprecatedString const& value) +{ + auto& vm = realm().vm(); + return set_impl(TRY_OR_THROW_OOM(vm, String::from_deprecated_string(name)), TRY_OR_THROW_OOM(vm, String::from_deprecated_string(value))); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-set-blob +WebIDL::ExceptionOr<void> FormData::set(DeprecatedString const& name, JS::NonnullGCPtr<FileAPI::Blob> const& blob_value, Optional<DeprecatedString> const& filename) +{ + auto& vm = realm().vm(); + auto inner_filename = filename.has_value() ? TRY_OR_THROW_OOM(vm, String::from_deprecated_string(filename.value())) : Optional<String> {}; + return set_impl(TRY_OR_THROW_OOM(vm, String::from_deprecated_string(name)), blob_value, inner_filename); +} + +// https://xhr.spec.whatwg.org/#dom-formdata-set +// https://xhr.spec.whatwg.org/#dom-formdata-set-blob +WebIDL::ExceptionOr<void> FormData::set_impl(String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename) +{ + auto& realm = this->realm(); + auto& vm = realm.vm(); + + // 1. Let value be value if given; otherwise blobValue. + // 2. Let entry be the result of creating an entry with name, value, and filename if given. + auto entry = TRY(HTML::create_entry(realm, name, value, filename)); + + // FIXME: Remove this when our binding generator supports "new string". + auto form_data_entry_value = entry.value.has<String>() + ? FormDataEntryValue { entry.value.get<String>().to_deprecated_string() } + : FormDataEntryValue { entry.value.get<JS::NonnullGCPtr<FileAPI::File>>() }; + + // 3. If there are entries in this’s entry list whose name is name, then replace the first such entry with entry and remove the others. + if (auto entries = m_entry_list.get(entry.name.to_deprecated_string()); entries.has_value() && !entries->is_empty()) { + entries->remove(0, entries->size()); + TRY_OR_THROW_OOM(vm, entries->try_append(form_data_entry_value)); + } + // 4. Otherwise, append entry to this’s entry list. + else { + TRY_OR_THROW_OOM(vm, m_entry_list.try_set(entry.name.to_deprecated_string(), { form_data_entry_value })); + } + + return {}; +} + +} diff --git a/Userland/Libraries/LibWeb/XHR/FormData.h b/Userland/Libraries/LibWeb/XHR/FormData.h new file mode 100644 index 0000000000..3156de0057 --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/FormData.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023, Kenneth Myhra <kennethmyhra@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibWeb/Bindings/FormDataPrototype.h> +#include <LibWeb/Bindings/PlatformObject.h> +#include <LibWeb/Forward.h> +#include <LibWeb/HTML/HTMLFormElement.h> +#include <LibWeb/WebIDL/ExceptionOr.h> + +namespace Web::XHR { + +// https://xhr.spec.whatwg.org/#formdataentryvalue +using FormDataEntryValue = Variant<JS::NonnullGCPtr<FileAPI::File>, DeprecatedString>; + +// https://xhr.spec.whatwg.org/#interface-formdata +class FormData : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(FormData, Bindings::PlatformObject); + +public: + virtual ~FormData() override; + + static WebIDL::ExceptionOr<JS::NonnullGCPtr<FormData>> construct_impl(JS::Realm&, Optional<JS::NonnullGCPtr<HTML::HTMLFormElement>> form = {}); + static WebIDL::ExceptionOr<JS::NonnullGCPtr<FormData>> construct_impl(JS::Realm&, HashMap<DeprecatedString, Vector<FormDataEntryValue>> entry_list); + + WebIDL::ExceptionOr<void> append(DeprecatedString const& name, DeprecatedString const& value); + WebIDL::ExceptionOr<void> append(DeprecatedString const& name, JS::NonnullGCPtr<FileAPI::Blob> const& blob_value, Optional<DeprecatedString> const& filename = {}); + void delete_(DeprecatedString const& name); + Variant<JS::NonnullGCPtr<FileAPI::File>, DeprecatedString, Empty> get(DeprecatedString const& name); + Vector<FormDataEntryValue> get_all(DeprecatedString const& name); + bool has(DeprecatedString const& name); + WebIDL::ExceptionOr<void> set(DeprecatedString const& name, DeprecatedString const& value); + WebIDL::ExceptionOr<void> set(DeprecatedString const& name, JS::NonnullGCPtr<FileAPI::Blob> const& blob_value, Optional<DeprecatedString> const& filename = {}); + +private: + explicit FormData(JS::Realm&, HashMap<DeprecatedString, Vector<FormDataEntryValue>> entry_list = {}); + + virtual void visit_edges(Cell::Visitor&) override; + + WebIDL::ExceptionOr<void> append_impl(String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename = {}); + WebIDL::ExceptionOr<void> set_impl(String const& name, Variant<JS::NonnullGCPtr<FileAPI::Blob>, String> const& value, Optional<String> const& filename = {}); + + HashMap<DeprecatedString, Vector<FormDataEntryValue>> m_entry_list; +}; + +} diff --git a/Userland/Libraries/LibWeb/XHR/FormData.idl b/Userland/Libraries/LibWeb/XHR/FormData.idl new file mode 100644 index 0000000000..ca5b170026 --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/FormData.idl @@ -0,0 +1,23 @@ +#import <FileAPI/Blob.idl> +#import <FileAPI/File.idl> +#import <HTML/HTMLFormElement.idl> + +typedef (File or USVString) FormDataEntryValue; + +// https://xhr.spec.whatwg.org/#interface-formdata +[Exposed=Window] +interface FormData { + constructor(optional HTMLFormElement form); + + undefined append(USVString name, USVString value); + undefined append(USVString name, Blob blobValue, optional USVString filename); + undefined delete(USVString name); + // FIXME: The BindingsGenerator is not able to resolve the Variant's visit for FormDataEntryValue when + // the return value for one function returns an optional FormDataEntryValue while the others does not. + (File or USVString)? get(USVString name); + sequence<FormDataEntryValue> getAll(USVString name); + boolean has(USVString name); + undefined set(USVString name, USVString value); + undefined set(USVString name, Blob blobValue, optional USVString filename); + // FIXME: iterable<USVString, FormDataEntryValue>; +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 0148066b3e..e28ec3f418 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -195,6 +195,7 @@ libweb_js_bindings(WebGL/WebGLContextEvent) libweb_js_bindings(WebGL/WebGLRenderingContext) libweb_js_bindings(WebIDL/DOMException) libweb_js_bindings(WebSockets/WebSocket) +libweb_js_bindings(XHR/FormData) libweb_js_bindings(XHR/ProgressEvent) libweb_js_bindings(XHR/XMLHttpRequest) libweb_js_bindings(XHR/XMLHttpRequestEventTarget) |