Ladybird+LibWeb+WebContent: Parse the <input type=file> accept attribute

This parses the accept attribute value for file input types and passes
it along to the browser chromes.
This commit is contained in:
Timothy Flynn 2024-03-14 12:26:00 -04:00 committed by Andreas Kling
parent e4213f5767
commit 6760d236e4
Notes: sideshowbarker 2024-07-17 08:45:34 +09:00
20 changed files with 267 additions and 24 deletions

View File

@ -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];

View File

@ -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<Web::HTML::SelectedFile> selected_files;
auto create_selected_file = [&](auto const& qfile_path) {

View File

@ -31,6 +31,7 @@ source_set("HTML") {
"ErrorEvent.cpp",
"EventHandler.cpp",
"EventNames.cpp",
"FileFilter.cpp",
"Focus.cpp",
"FormAssociatedElement.cpp",
"FormControlInfrastructure.cpp",

View File

@ -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; }}

View File

@ -0,0 +1,34 @@
<input id="input1" type="file" accept="text/plain" />
<input id="input2" type="file" accept="text/plain" multiple />
<input id="input3" type="file" accept=".cpp" />
<input id="input4" type="file" accept=".cpp" multiple />
<script src="./include.js"></script>
<script type="text/javascript">
const runTest = async id => {
let input = document.getElementById(id);
return new Promise(resolve => {
input.addEventListener("input", async () => {
println(`${id}:`);
for (let file of input.files) {
const text = await file.text();
println(`${file.name}: ${file.type}: ${text}`);
}
resolve();
});
internals.dispatchUserActivatedEvent(input, new Event("mousedown"));
input.showPicker();
});
};
asyncTest(async done => {
await runTest("input1");
await runTest("input2");
await runTest("input3");
await runTest("input4");
done();
});
</script>

View File

@ -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;

View File

@ -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

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/HTML/FileFilter.h>
namespace Web::HTML {
void FileFilter::add_filter(FilterType filter)
{
if (!filters.contains_slow(filter))
filters.append(move(filter));
}
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, Web::HTML::FileFilter::MimeType const& mime_type)
{
TRY(encoder.encode(mime_type.value));
return {};
}
template<>
ErrorOr<Web::HTML::FileFilter::MimeType> IPC::decode(Decoder& decoder)
{
auto value = TRY(decoder.decode<String>());
return Web::HTML::FileFilter::MimeType { move(value) };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, Web::HTML::FileFilter::Extension const& extension)
{
TRY(encoder.encode(extension.value));
return {};
}
template<>
ErrorOr<Web::HTML::FileFilter::Extension> IPC::decode(Decoder& decoder)
{
auto value = TRY(decoder.decode<String>());
return Web::HTML::FileFilter::Extension { move(value) };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, Web::HTML::FileFilter const& filter)
{
TRY(encoder.encode(filter.filters));
return {};
}
template<>
ErrorOr<Web::HTML::FileFilter> IPC::decode(Decoder& decoder)
{
auto filters = TRY(decoder.decode<Vector<Web::HTML::FileFilter::FilterType>>());
return Web::HTML::FileFilter { move(filters) };
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibIPC/Forward.h>
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<FileType, MimeType, Extension>;
void add_filter(FilterType);
Vector<FilterType> filters;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, Web::HTML::FileFilter::MimeType const&);
template<>
ErrorOr<Web::HTML::FileFilter::MimeType> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, Web::HTML::FileFilter::Extension const&);
template<>
ErrorOr<Web::HTML::FileFilter::Extension> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, Web::HTML::FileFilter const&);
template<>
ErrorOr<Web::HTML::FileFilter> decode(Decoder&);
}

View File

@ -39,6 +39,7 @@
#include <LibWeb/Layout/CheckBox.h>
#include <LibWeb/Layout/ImageBox.h>
#include <LibWeb/Layout/RadioButton.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/MimeSniff/Resource.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
@ -181,6 +182,43 @@ void HTMLInputElement::set_files(JS::GCPtr<FileAPI::FileList> 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<FileAPI::FileList> 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<HTMLInputElement>();
// 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;
}

View File

@ -12,6 +12,7 @@
#include <LibWeb/DOM/Text.h>
#include <LibWeb/FileAPI/FileList.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/Layout/ImageProvider.h>
@ -102,6 +103,8 @@ public:
JS::GCPtr<FileAPI::FileList> files();
void set_files(JS::GCPtr<FileAPI::FileList>);
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<FileAPI::FileList>);

View File

@ -344,13 +344,13 @@ void Page::color_picker_update(Optional<Color> picked_color, HTML::ColorPickerUp
}
}
void Page::did_request_file_picker(WeakPtr<HTML::HTMLInputElement> target, HTML::AllowMultipleFiles allow_multiple_files)
void Page::did_request_file_picker(WeakPtr<HTML::HTMLInputElement> 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);
}
}

View File

@ -31,6 +31,7 @@
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/TokenizedFeatures.h>
#include <LibWeb/HTML/WebViewHints.h>
@ -134,7 +135,7 @@ public:
void did_request_color_picker(WeakPtr<HTML::HTMLInputElement> target, Color current_color);
void color_picker_update(Optional<Color> picked_color, HTML::ColorPickerUpdateState state);
void did_request_file_picker(WeakPtr<HTML::HTMLInputElement> target, HTML::AllowMultipleFiles);
void did_request_file_picker(WeakPtr<HTML::HTMLInputElement> target, HTML::FileFilter accepted_file_types, HTML::AllowMultipleFiles);
void file_picker_closed(Span<HTML::SelectedFile> selected_files);
void did_request_select_dropdown(WeakPtr<HTML::HTMLSelectElement> target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector<Web::HTML::SelectItem> 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<Web::HTML::SelectItem> items) {};
virtual void page_did_finish_text_test() {};

View File

@ -18,6 +18,7 @@
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWebView/Forward.h>
@ -170,7 +171,7 @@ public:
Function<Gfx::IntRect()> on_minimize_window;
Function<Gfx::IntRect()> on_fullscreen_window;
Function<void(Color current_color)> on_request_color_picker;
Function<void(Web::HTML::AllowMultipleFiles)> on_request_file_picker;
Function<void(Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles)> on_request_file_picker;
Function<void(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items)> on_request_select_dropdown;
Function<void(Web::KeyEvent const&)> on_finish_handling_key_event;
Function<void()> on_text_test_finish;

View File

@ -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<Web::HTML::SelectItem> const& items)

View File

@ -9,6 +9,7 @@
#include <AK/HashMap.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/WebViewHints.h>
#include <WebContent/WebContentClientEndpoint.h>
@ -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<Web::HTML::SelectItem> 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;

View File

@ -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<Web::HTML::SelectItem> items)

View File

@ -10,6 +10,7 @@
#include <LibAccelGfx/Forward.h>
#include <LibGfx/Rect.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/PixelUnits.h>
#include <WebContent/Forward.h>
@ -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<Web::HTML::SelectItem> items) override;
virtual void page_did_finish_text_test() override;
virtual void page_did_change_theme_color(Gfx::Color color) override;

View File

@ -6,6 +6,7 @@
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/WebViewHints.h>
@ -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<Web::HTML::SelectItem> items) =|
did_finish_handling_input_event(u64 page_id, bool event_was_accepted) =|
did_change_theme_color(u64 page_id, Gfx::Color color) =|

View File

@ -436,15 +436,42 @@ static ErrorOr<TestResult> 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<Web::HTML::SelectedFile> 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));