1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-28 07:27:45 +00:00

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
23 changed files with 307 additions and 5 deletions

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;