diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm index 71bf91ec61..22b5e668c6 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.mm +++ b/Ladybird/AppKit/UI/LadybirdWebView.mm @@ -626,7 +626,7 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_ [panel makeKeyAndOrderFront:nil]; }; - m_web_view_bridge->on_request_file_picker = [self](auto allow_multiple_files) { + m_web_view_bridge->on_request_file_picker = [self](auto const&, auto allow_multiple_files) { auto* panel = [NSOpenPanel openPanel]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; diff --git a/Ladybird/Qt/Tab.cpp b/Ladybird/Qt/Tab.cpp index 92c13c2cf0..76c0b99bde 100644 --- a/Ladybird/Qt/Tab.cpp +++ b/Ladybird/Qt/Tab.cpp @@ -232,7 +232,7 @@ Tab::Tab(BrowserWindow* window, WebContentOptions const& web_content_options, St m_dialog = nullptr; }; - view().on_request_file_picker = [this](auto allow_multiple_files) { + view().on_request_file_picker = [this](auto const&, auto allow_multiple_files) { Vector selected_files; auto create_selected_file = [&](auto const& qfile_path) { diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn index 428cd033e9..a8d809f569 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn @@ -31,6 +31,7 @@ source_set("HTML") { "ErrorEvent.cpp", "EventHandler.cpp", "EventNames.cpp", + "FileFilter.cpp", "Focus.cpp", "FormAssociatedElement.cpp", "FormControlInfrastructure.cpp", diff --git a/Tests/LibWeb/Text/expected/input-file-accept.txt b/Tests/LibWeb/Text/expected/input-file-accept.txt new file mode 100644 index 0000000000..dbe01d5d6d --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-file-accept.txt @@ -0,0 +1,12 @@ +Select file...file1 Select files...4 files selected. Select file...file1.cpp Select files...2 files selected. input1: +file1: text/plain: Contents for file1 +input2: +file1: text/plain: Contents for file1 +file2: text/plain: Contents for file2 +file3: text/plain: Contents for file3 +file4: text/plain: Contents for file4 +input3: +file1.cpp: text/plain: int main() {{ return 1; }} +input4: +file1.cpp: text/plain: int main() {{ return 1; }} +file2.cpp: text/plain: int main() {{ return 2; }} diff --git a/Tests/LibWeb/Text/input/input-file-accept.html b/Tests/LibWeb/Text/input/input-file-accept.html new file mode 100644 index 0000000000..eb1a98781e --- /dev/null +++ b/Tests/LibWeb/Text/input/input-file-accept.html @@ -0,0 +1,34 @@ + + + + + + diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp index 5c93903571..6db7c37851 100644 --- a/Userland/Applications/Browser/Tab.cpp +++ b/Userland/Applications/Browser/Tab.cpp @@ -556,7 +556,7 @@ Tab::Tab(BrowserWindow& window) m_dialog = nullptr; }; - view().on_request_file_picker = [this](auto allow_multiple_files) { + view().on_request_file_picker = [this](auto const&, auto allow_multiple_files) { // FIXME: GUI::FilePicker does not allow selecting multiple files at once. (void)allow_multiple_files; diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index f510fa752e..5af193b81d 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -268,6 +268,7 @@ set(SOURCES HTML/EventLoop/Task.cpp HTML/EventLoop/TaskQueue.cpp HTML/EventNames.cpp + HTML/FileFilter.cpp HTML/Focus.cpp HTML/FormAssociatedElement.cpp HTML/FormControlInfrastructure.cpp diff --git a/Userland/Libraries/LibWeb/HTML/FileFilter.cpp b/Userland/Libraries/LibWeb/HTML/FileFilter.cpp new file mode 100644 index 0000000000..0ccafea5eb --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/FileFilter.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::HTML { + +void FileFilter::add_filter(FilterType filter) +{ + if (!filters.contains_slow(filter)) + filters.append(move(filter)); +} + +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::FileFilter::MimeType const& mime_type) +{ + TRY(encoder.encode(mime_type.value)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto value = TRY(decoder.decode()); + return Web::HTML::FileFilter::MimeType { move(value) }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::FileFilter::Extension const& extension) +{ + TRY(encoder.encode(extension.value)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto value = TRY(decoder.decode()); + return Web::HTML::FileFilter::Extension { move(value) }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::FileFilter const& filter) +{ + TRY(encoder.encode(filter.filters)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto filters = TRY(decoder.decode>()); + return Web::HTML::FileFilter { move(filters) }; +} diff --git a/Userland/Libraries/LibWeb/HTML/FileFilter.h b/Userland/Libraries/LibWeb/HTML/FileFilter.h new file mode 100644 index 0000000000..8576bc1844 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/FileFilter.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::HTML { + +struct FileFilter { + enum class FileType { + Audio, + Image, + Video, + }; + + struct MimeType { + bool operator==(MimeType const&) const = default; + String value; + }; + + struct Extension { + bool operator==(Extension const&) const = default; + String value; + }; + + using FilterType = Variant; + + void add_filter(FilterType); + + Vector filters; +}; + +} + +namespace IPC { + +template<> +ErrorOr encode(Encoder&, Web::HTML::FileFilter::MimeType const&); + +template<> +ErrorOr decode(Decoder&); + +template<> +ErrorOr encode(Encoder&, Web::HTML::FileFilter::Extension const&); + +template<> +ErrorOr decode(Decoder&); + +template<> +ErrorOr encode(Encoder&, Web::HTML::FileFilter const&); + +template<> +ErrorOr decode(Decoder&); + +} diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index bd499a25bd..61c62e2541 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -181,6 +182,43 @@ void HTMLInputElement::set_files(JS::GCPtr files) m_selected_files = files; } +// https://html.spec.whatwg.org/multipage/input.html#attr-input-accept +FileFilter HTMLInputElement::parse_accept_attribute() const +{ + FileFilter filter; + + // If specified, the attribute must consist of a set of comma-separated tokens, each of which must be an ASCII + // case-insensitive match for one of the following: + get_attribute_value(HTML::AttributeNames::accept).bytes_as_string_view().for_each_split_view(',', SplitBehavior::Nothing, [&](StringView value) { + // The string "audio/*" + // Indicates that sound files are accepted. + if (value.equals_ignoring_ascii_case("audio/*"sv)) + filter.add_filter(FileFilter::FileType::Audio); + + // The string "video/*" + // Indicates that video files are accepted. + if (value.equals_ignoring_ascii_case("video/*"sv)) + filter.add_filter(FileFilter::FileType::Video); + + // The string "image/*" + // Indicates that image files are accepted. + if (value.equals_ignoring_ascii_case("image/*"sv)) + filter.add_filter(FileFilter::FileType::Image); + + // A valid MIME type string with no parameters + // Indicates that files of the specified type are accepted. + else if (auto mime_type = MUST(MimeSniff::MimeType::parse(value)); mime_type.has_value() && mime_type->parameters().is_empty()) + filter.add_filter(FileFilter::MimeType { mime_type->essence() }); + + // A string whose first character is a U+002E FULL STOP character (.) + // Indicates that files with the specified file extension are accepted. + else if (value.starts_with('.')) + filter.add_filter(FileFilter::Extension { MUST(String::from_utf8(value.substring_view(1))) }); + }); + + return filter; +} + // https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection void HTMLInputElement::update_the_file_selection(JS::NonnullGCPtr files) { @@ -227,12 +265,11 @@ static void show_the_picker_if_applicable(HTMLInputElement& element) // with the bubbles attribute initialized to true. // 5. Otherwise, update the file selection for element. + auto accepted_file_types = element.parse_accept_attribute(); auto allow_multiple_files = element.has_attribute(HTML::AttributeNames::multiple) ? AllowMultipleFiles::Yes : AllowMultipleFiles::No; auto weak_element = element.make_weak_ptr(); - // FIXME: Pass along accept attribute information https://html.spec.whatwg.org/multipage/input.html#attr-input-accept - // The accept attribute may be specified to provide user agents with a hint of what file types will be accepted. - element.document().browsing_context()->top_level_browsing_context()->page().did_request_file_picker(weak_element, allow_multiple_files); + element.document().browsing_context()->top_level_browsing_context()->page().did_request_file_picker(weak_element, move(accepted_file_types), allow_multiple_files); return; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index c7bf365b3b..b3a39429ff 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,8 @@ public: JS::GCPtr files(); void set_files(JS::GCPtr); + FileFilter parse_accept_attribute() const; + // NOTE: User interaction // https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection void update_the_file_selection(JS::NonnullGCPtr); diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp index c5062703fc..da1f36908f 100644 --- a/Userland/Libraries/LibWeb/Page/Page.cpp +++ b/Userland/Libraries/LibWeb/Page/Page.cpp @@ -344,13 +344,13 @@ void Page::color_picker_update(Optional picked_color, HTML::ColorPickerUp } } -void Page::did_request_file_picker(WeakPtr target, HTML::AllowMultipleFiles allow_multiple_files) +void Page::did_request_file_picker(WeakPtr target, HTML::FileFilter accepted_file_types, HTML::AllowMultipleFiles allow_multiple_files) { if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { m_pending_non_blocking_dialog = PendingNonBlockingDialog::FilePicker; m_pending_non_blocking_dialog_target = move(target); - m_client->page_did_request_file_picker(allow_multiple_files); + m_client->page_did_request_file_picker(move(accepted_file_types), allow_multiple_files); } } diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index 595a29ea8e..bc949cbf40 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -134,7 +135,7 @@ public: void did_request_color_picker(WeakPtr target, Color current_color); void color_picker_update(Optional picked_color, HTML::ColorPickerUpdateState state); - void did_request_file_picker(WeakPtr target, HTML::AllowMultipleFiles); + void did_request_file_picker(WeakPtr target, HTML::FileFilter accepted_file_types, HTML::AllowMultipleFiles); void file_picker_closed(Span selected_files); void did_request_select_dropdown(WeakPtr target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items); @@ -285,7 +286,7 @@ public: // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable virtual void page_did_request_color_picker([[maybe_unused]] Color current_color) {}; - virtual void page_did_request_file_picker(Web::HTML::AllowMultipleFiles) {}; + virtual void page_did_request_file_picker([[maybe_unused]] HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles) {}; virtual void page_did_request_select_dropdown([[maybe_unused]] Web::CSSPixelPoint content_position, [[maybe_unused]] Web::CSSPixels minimum_width, [[maybe_unused]] Vector items) {}; virtual void page_did_finish_text_test() {}; diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h index b3f25c320c..d620651c35 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.h +++ b/Userland/Libraries/LibWebView/ViewImplementation.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -170,7 +171,7 @@ public: Function on_minimize_window; Function on_fullscreen_window; Function on_request_color_picker; - Function on_request_file_picker; + Function on_request_file_picker; Function items)> on_request_select_dropdown; Function on_finish_handling_key_event; Function on_text_test_finish; diff --git a/Userland/Libraries/LibWebView/WebContentClient.cpp b/Userland/Libraries/LibWebView/WebContentClient.cpp index 55cbad80c9..ffead277b7 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.cpp +++ b/Userland/Libraries/LibWebView/WebContentClient.cpp @@ -806,7 +806,7 @@ void WebContentClient::did_request_color_picker(u64 page_id, Color const& curren view.on_request_color_picker(current_color); } -void WebContentClient::did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles allow_multiple_files) +void WebContentClient::did_request_file_picker(u64 page_id, Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple_files) { auto maybe_view = m_views.get(page_id); if (!maybe_view.has_value()) { @@ -816,7 +816,7 @@ void WebContentClient::did_request_file_picker(u64 page_id, Web::HTML::AllowMult auto& view = *maybe_view.value(); if (view.on_request_file_picker) - view.on_request_file_picker(allow_multiple_files); + view.on_request_file_picker(accepted_file_types, allow_multiple_files); } void WebContentClient::did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) diff --git a/Userland/Libraries/LibWebView/WebContentClient.h b/Userland/Libraries/LibWebView/WebContentClient.h index f55b1f5124..e183c055e3 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.h +++ b/Userland/Libraries/LibWebView/WebContentClient.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -89,7 +90,7 @@ private: virtual Messages::WebContentClient::DidRequestFullscreenWindowResponse did_request_fullscreen_window(u64 page_id) override; virtual void did_request_file(u64 page_id, ByteString const& path, i32) override; virtual void did_request_color_picker(u64 page_id, Color const& current_color) override; - virtual void did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles) override; + virtual void did_request_file_picker(u64 page_id, Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles) override; virtual void did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) override; virtual void did_finish_handling_input_event(u64 page_id, bool event_was_accepted) override; virtual void did_finish_text_test(u64 page_id) override; diff --git a/Userland/Services/WebContent/PageClient.cpp b/Userland/Services/WebContent/PageClient.cpp index 3d66bf2fdf..cc7d5ccdf2 100644 --- a/Userland/Services/WebContent/PageClient.cpp +++ b/Userland/Services/WebContent/PageClient.cpp @@ -549,9 +549,9 @@ void PageClient::page_did_request_color_picker(Color current_color) client().async_did_request_color_picker(m_id, current_color); } -void PageClient::page_did_request_file_picker(Web::HTML::AllowMultipleFiles allow_multiple_files) +void PageClient::page_did_request_file_picker(Web::HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple_files) { - client().async_did_request_file_picker(m_id, allow_multiple_files); + client().async_did_request_file_picker(m_id, move(accepted_file_types), allow_multiple_files); } void PageClient::page_did_request_select_dropdown(Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items) diff --git a/Userland/Services/WebContent/PageClient.h b/Userland/Services/WebContent/PageClient.h index 8936cfdf23..2faffb90b7 100644 --- a/Userland/Services/WebContent/PageClient.h +++ b/Userland/Services/WebContent/PageClient.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -133,7 +134,7 @@ private: virtual void page_did_close_top_level_traversable() override; virtual void request_file(Web::FileRequest) override; virtual void page_did_request_color_picker(Color current_color) override; - virtual void page_did_request_file_picker(Web::HTML::AllowMultipleFiles) override; + virtual void page_did_request_file_picker(Web::HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles) override; virtual void page_did_request_select_dropdown(Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items) override; virtual void page_did_finish_text_test() override; virtual void page_did_change_theme_color(Gfx::Color color) override; diff --git a/Userland/Services/WebContent/WebContentClient.ipc b/Userland/Services/WebContent/WebContentClient.ipc index 0abce74f54..853ecbd4fa 100644 --- a/Userland/Services/WebContent/WebContentClient.ipc +++ b/Userland/Services/WebContent/WebContentClient.ipc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -71,7 +72,7 @@ endpoint WebContentClient did_request_fullscreen_window(u64 page_id) => (Gfx::IntRect window_rect) did_request_file(u64 page_id, ByteString path, i32 request_id) =| did_request_color_picker(u64 page_id, Color current_color) =| - did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles allow_multiple_files) =| + did_request_file_picker(u64 page_id, Web::HTML::FileFilter accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple_files) =| did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector items) =| did_finish_handling_input_event(u64 page_id, bool event_was_accepted) =| did_change_theme_color(u64 page_id, Gfx::Color color) =| diff --git a/Userland/Utilities/headless-browser.cpp b/Userland/Utilities/headless-browser.cpp index 433e53aafc..b5a86de144 100644 --- a/Userland/Utilities/headless-browser.cpp +++ b/Userland/Utilities/headless-browser.cpp @@ -436,15 +436,42 @@ static ErrorOr run_test(HeadlessWebContentView& view, StringView inp }; view.on_text_test_finish = {}; - view.on_request_file_picker = [&](auto allow_multiple_files) { + view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) { // Create some dummy files for tests. Vector selected_files; - selected_files.empend("file1"sv, MUST(ByteBuffer::copy("Contents for file1"sv.bytes()))); - if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) { - selected_files.empend("file2"sv, MUST(ByteBuffer::copy("Contents for file2"sv.bytes()))); - selected_files.empend("file3"sv, MUST(ByteBuffer::copy("Contents for file3"sv.bytes()))); - selected_files.empend("file4"sv, MUST(ByteBuffer::copy("Contents for file4"sv.bytes()))); + bool add_txt_files = accepted_file_types.filters.is_empty(); + bool add_cpp_files = false; + + for (auto const& filter : accepted_file_types.filters) { + filter.visit( + [](Web::HTML::FileFilter::FileType) {}, + [&](Web::HTML::FileFilter::MimeType const& mime_type) { + if (mime_type.value == "text/plain"sv) + add_txt_files = true; + }, + [&](Web::HTML::FileFilter::Extension const& extension) { + if (extension.value == "cpp"sv) + add_cpp_files = true; + }); + } + + if (add_txt_files) { + selected_files.empend("file1"sv, MUST(ByteBuffer::copy("Contents for file1"sv.bytes()))); + + if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) { + selected_files.empend("file2"sv, MUST(ByteBuffer::copy("Contents for file2"sv.bytes()))); + selected_files.empend("file3"sv, MUST(ByteBuffer::copy("Contents for file3"sv.bytes()))); + selected_files.empend("file4"sv, MUST(ByteBuffer::copy("Contents for file4"sv.bytes()))); + } + } + + if (add_cpp_files) { + selected_files.empend("file1.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 1; }}"sv.bytes()))); + + if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) { + selected_files.empend("file2.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 2; }}"sv.bytes()))); + } } view.file_picker_closed(move(selected_files));