LibWeb+LibWebView+WebContent: Implement more <input type=file> behavior

We had previous implemented some plumbing for file input elements in
commit 636602a54e.

This implements the return path for chromes to inform WebContent of the
file(s) the user selected. This patch includes a dummy implementation
for headless-browser to enable testing.
This commit is contained in:
Timothy Flynn 2024-02-25 13:02:47 -05:00 committed by Andreas Kling
parent 435c2c24d1
commit 108521a566
Notes: sideshowbarker 2024-07-17 04:10:16 +09:00
23 changed files with 307 additions and 5 deletions

View file

@ -70,7 +70,7 @@ static bool is_primitive_type(ByteString const& type)
static bool is_simple_type(ByteString const& type)
{
// Small types that it makes sense just to pass by value.
return type.is_one_of("Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source");
return type.is_one_of("Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source", "Web::HTML::AllowMultipleFiles");
}
static bool is_primitive_or_simple_type(ByteString const& type)

View file

@ -140,6 +140,7 @@ source_set("HTML") {
"PotentialCORSRequest.cpp",
"PromiseRejectionEvent.cpp",
"SelectItem.cpp",
"SelectedFile.cpp",
"SessionHistoryEntry.cpp",
"SharedImageRequest.cpp",
"SourceSet.cpp",

View file

@ -0,0 +1,7 @@
Select file...file1 Select files...4 files selected. input1:
file1: Contents for file1
input2:
file1: Contents for file1
file2: Contents for file2
file3: Contents for file3
file4: Contents for file4

View file

@ -0,0 +1,32 @@
<input id="input1" type="file" />
<input id="input2" type="file" 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 i = 0; i < input.files.length; ++i) {
const file = input.files.item(i);
const text = await file.text();
println(`${file.name}: ${text}`);
}
resolve();
});
internals.dispatchUserActivatedEvent(input, new Event("mousedown"));
input.showPicker();
});
};
asyncTest(async done => {
await runTest("input1");
await runTest("input2");
done();
});
</script>

View file

@ -389,6 +389,7 @@ set(SOURCES
HTML/Scripting/TemporaryExecutionContext.cpp
HTML/Scripting/WindowEnvironmentSettingsObject.cpp
HTML/Scripting/WorkerEnvironmentSettingsObject.cpp
HTML/SelectedFile.cpp
HTML/SelectItem.cpp
HTML/SessionHistoryEntry.cpp
HTML/SharedImageRequest.cpp

View file

@ -445,6 +445,7 @@ class Path2D;
class Plugin;
class PluginArray;
class PromiseRejectionEvent;
class SelectedFile;
class SharedImageRequest;
class Storage;
class SubmitEvent;
@ -468,6 +469,7 @@ class WorkerGlobalScope;
class WorkerLocation;
class WorkerNavigator;
enum class AllowMultipleFiles;
enum class MediaSeekMode;
enum class SandboxingFlagSet;

View file

@ -28,6 +28,7 @@
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/SharedImageRequest.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
@ -37,6 +38,7 @@
#include <LibWeb/Layout/CheckBox.h>
#include <LibWeb/Layout/ImageBox.h>
#include <LibWeb/Layout/RadioButton.h>
#include <LibWeb/MimeSniff/Resource.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/UIEvents/EventNames.h>
@ -213,12 +215,12 @@ static void show_the_picker_if_applicable(HTMLInputElement& element)
// with the bubbles attribute initialized to true.
// 5. Otherwise, update the file selection for element.
bool const multiple = element.has_attribute(HTML::AttributeNames::multiple);
auto weak_element = element.make_weak_ptr<DOM::EventTarget>();
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().client().page_did_request_file_picker(weak_element, multiple);
element.document().browsing_context()->top_level_browsing_context()->page().did_request_file_picker(weak_element, allow_multiple_files);
return;
}
@ -380,6 +382,51 @@ void HTMLInputElement::did_pick_color(Optional<Color> picked_color)
}
}
void HTMLInputElement::did_select_files(Span<SelectedFile> selected_files)
{
// https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable
// 4. If the user dismissed the prompt without changing their selection, then queue an element task on the user
// interaction task source given element to fire an event named cancel at element, with the bubbles attribute
// initialized to true.
if (selected_files.is_empty()) {
queue_an_element_task(HTML::Task::Source::UserInteraction, [this]() {
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .bubbles = true }));
});
return;
}
Vector<JS::NonnullGCPtr<FileAPI::File>> files;
files.ensure_capacity(selected_files.size());
for (auto& selected_file : selected_files) {
auto contents = selected_file.take_contents();
auto mime_type = MUST(MimeSniff::Resource::sniff(contents));
auto blob = FileAPI::Blob::create(realm(), move(contents), mime_type.essence());
// FIXME: The FileAPI should use ByteString for file names.
auto file_name = MUST(String::from_byte_string(selected_file.name()));
auto file = MUST(FileAPI::File::create(realm(), { JS::make_handle(blob) }, file_name));
files.unchecked_append(file);
}
// https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection
// 1. Queue an element task on the user interaction task source given element and the following steps:
queue_an_element_task(HTML::Task::Source::UserInteraction, [this, files = move(files)]() mutable {
// 1. Update element's selected files so that it represents the user's selection.
m_selected_files = FileAPI::FileList::create(realm(), move(files));
update_file_input_shadow_tree();
// 2. Fire an event named input at the input element, with the bubbles and composed attributes initialized to true.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::input, { .bubbles = true, .composed = true }));
// 3. Fire an event named change at the input element, with the bubbles attribute initialized to true.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::change, { .bubbles = true }));
});
}
String HTMLInputElement::value() const
{
switch (value_attribute_mode()) {

View file

@ -94,6 +94,8 @@ public:
void did_pick_color(Optional<Color> picked_color);
void did_select_files(Span<SelectedFile> selected_files);
JS::GCPtr<FileAPI::FileList> files();
void set_files(JS::GCPtr<FileAPI::FileList>);

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <LibCore/File.h>
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/HTML/SelectedFile.h>
namespace Web::HTML {
ErrorOr<SelectedFile> SelectedFile::from_file_path(ByteString const& file_path)
{
// https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file):concept-input-file-path
// Filenames must not contain path components, even in the case that a user has selected an entire directory
// hierarchy or multiple files with the same name from different directories.
auto name = LexicalPath::basename(file_path);
auto file = TRY(Core::File::open(file_path, Core::File::OpenMode::Read));
return SelectedFile { move(name), IPC::File { *file } };
}
SelectedFile::SelectedFile(ByteString name, ByteBuffer contents)
: m_name(move(name))
, m_file_or_contents(move(contents))
{
}
SelectedFile::SelectedFile(ByteString name, IPC::File file)
: m_name(move(name))
, m_file_or_contents(move(file))
{
}
ByteBuffer SelectedFile::take_contents()
{
VERIFY(m_file_or_contents.has<ByteBuffer>());
return move(m_file_or_contents.get<ByteBuffer>());
}
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, Web::HTML::SelectedFile const& file)
{
TRY(encoder.encode(file.name()));
TRY(encoder.encode(file.file_or_contents()));
return {};
}
template<>
ErrorOr<Web::HTML::SelectedFile> IPC::decode(Decoder& decoder)
{
auto name = TRY(decoder.decode<ByteString>());
auto file_or_contents = TRY((decoder.decode<Variant<IPC::File, ByteBuffer>>()));
ByteBuffer contents;
if (file_or_contents.has<IPC::File>()) {
auto file = TRY(Core::File::adopt_fd(file_or_contents.get<IPC::File>().take_fd(), Core::File::OpenMode::Read));
contents = TRY(file->read_until_eof());
} else {
contents = move(file_or_contents.get<ByteBuffer>());
}
return Web::HTML::SelectedFile { move(name), move(contents) };
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Variant.h>
#include <LibIPC/File.h>
#include <LibIPC/Forward.h>
namespace Web::HTML {
enum class AllowMultipleFiles {
No,
Yes,
};
class SelectedFile {
public:
static ErrorOr<SelectedFile> from_file_path(ByteString const& file_path);
SelectedFile(ByteString name, ByteBuffer contents);
SelectedFile(ByteString name, IPC::File file);
ByteString const& name() const { return m_name; }
auto const& file_or_contents() const { return m_file_or_contents; }
ByteBuffer take_contents();
private:
ByteString m_name;
Variant<IPC::File, ByteBuffer> m_file_or_contents;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, Web::HTML::SelectedFile const&);
template<>
ErrorOr<Web::HTML::SelectedFile> decode(Decoder&);
}

View file

@ -18,6 +18,7 @@
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
@ -343,6 +344,30 @@ 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)
{
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);
}
}
void Page::file_picker_closed(Span<HTML::SelectedFile> selected_files)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::FilePicker) {
m_pending_non_blocking_dialog = PendingNonBlockingDialog::None;
if (m_pending_non_blocking_dialog_target) {
auto& input_element = verify_cast<HTML::HTMLInputElement>(*m_pending_non_blocking_dialog_target);
input_element.did_select_files(selected_files);
m_pending_non_blocking_dialog_target.clear();
}
}
}
void Page::did_request_select_dropdown(WeakPtr<HTML::HTMLSelectElement> target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector<Web::HTML::SelectItem> items)
{
if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) {

View file

@ -134,12 +134,16 @@ 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 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);
void select_dropdown_closed(Optional<String> value);
enum class PendingNonBlockingDialog {
None,
ColorPicker,
FilePicker,
Select,
};
@ -280,8 +284,8 @@ public:
virtual void request_file(FileRequest) = 0;
// https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable
virtual void page_did_request_file_picker(WeakPtr<DOM::EventTarget>, [[maybe_unused]] bool multiple) {};
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_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

@ -250,6 +250,11 @@ void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::
client().async_color_picker_update(page_id(), picked_color, state);
}
void ViewImplementation::file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files)
{
client().async_file_picker_closed(page_id(), move(selected_files));
}
void ViewImplementation::select_dropdown_closed(Optional<String> value)
{
client().async_select_dropdown_closed(page_id(), value);

View file

@ -86,6 +86,7 @@ public:
void confirm_closed(bool accepted);
void prompt_closed(Optional<String> response);
void color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state);
void file_picker_closed(Vector<Web::HTML::SelectedFile> selected_files);
void select_dropdown_closed(Optional<String> value);
void toggle_media_play_state();
@ -164,6 +165,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(Gfx::IntPoint content_position, i32 minimum_width, Vector<Web::HTML::SelectItem> items)> on_request_select_dropdown;
Function<void(bool)> on_finish_handling_input_event;
Function<void()> on_text_test_finish;

View file

@ -806,6 +806,19 @@ 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)
{
auto maybe_view = m_views.get(page_id);
if (!maybe_view.has_value()) {
dbgln("Received request file picker for unknown page ID {}", page_id);
return;
}
auto& view = *maybe_view.value();
if (view.on_request_file_picker)
view.on_request_file_picker(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)
{
auto maybe_view = m_views.get(page_id);

View file

@ -89,6 +89,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_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

@ -28,6 +28,8 @@
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
@ -1317,6 +1319,18 @@ void ConnectionFromClient::color_picker_update(u64 page_id, Optional<Color> cons
page.page().color_picker_update(picked_color, state);
}
void ConnectionFromClient::file_picker_closed(u64 page_id, Vector<Web::HTML::SelectedFile> const& selected_files)
{
auto maybe_page = page(page_id);
if (!maybe_page.has_value()) {
dbgln("ConnectionFromClient::color_picker_update: No page with ID {}", page_id);
return;
}
auto& page = maybe_page.release_value();
page.page().file_picker_closed(const_cast<Vector<Web::HTML::SelectedFile>&>(selected_files));
}
void ConnectionFromClient::select_dropdown_closed(u64 page_id, Optional<String> const& value)
{
auto maybe_page = page(page_id);

View file

@ -106,6 +106,7 @@ private:
virtual void confirm_closed(u64 page_id, bool accepted) override;
virtual void prompt_closed(u64 page_id, Optional<String> const& response) override;
virtual void color_picker_update(u64 page_id, Optional<Color> const& picked_color, Web::HTML::ColorPickerUpdateState const& state) override;
virtual void file_picker_closed(u64 page_id, Vector<Web::HTML::SelectedFile> const& selected_files) override;
virtual void select_dropdown_closed(u64 page_id, Optional<String> const& value) override;
virtual void toggle_media_play_state(u64 page_id) override;

View file

@ -549,6 +549,11 @@ 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)
{
client().async_did_request_file_picker(m_id, allow_multiple_files);
}
void PageClient::page_did_request_select_dropdown(Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector<Web::HTML::SelectItem> items)
{
client().async_did_request_select_dropdown(m_id, page().css_to_device_point(content_position).to_type<int>(), minimum_width * device_pixels_per_css_pixel(), items);

View file

@ -133,6 +133,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_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/SelectedFile.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/WebViewHints.h>
#include <LibWeb/Page/Page.h>
@ -70,6 +71,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_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

@ -6,6 +6,7 @@
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/WebDriver/ExecuteScript.h>
#include <LibWebView/Attribute.h>
@ -93,6 +94,7 @@ endpoint WebContentServer
confirm_closed(u64 page_id, bool accepted) =|
prompt_closed(u64 page_id, Optional<String> response) =|
color_picker_update(u64 page_id, Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state) =|
file_picker_closed(u64 page_id, Vector<Web::HTML::SelectedFile> selected_files) =|
select_dropdown_closed(u64 page_id, Optional<String> value) =|
toggle_media_play_state(u64 page_id) =|

View file

@ -8,6 +8,7 @@
*/
#include <AK/Badge.h>
#include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Function.h>
#include <AK/JsonObject.h>
@ -44,6 +45,7 @@
#include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/SelectedFile.h>
#include <LibWeb/Worker/WebWorkerClient.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/Database.h>
@ -433,6 +435,21 @@ static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView inp
promise->resolve({});
};
view.on_text_test_finish = {};
view.on_request_file_picker = [&](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())));
}
view.file_picker_closed(move(selected_files));
};
view.load(URL("about:blank"sv));
MUST(promise->await());