LibWeb+LibWebView+WebContent: Introduce a WebUI framework

When we build internal pages (e.g. about:settings), there is currently
quite a lot of boilerplate needed to communicate between the browser and
the page. This includes creating IDL for the page and the IPC for every
message sent between the processes.

These internal pages are also special in that they have privileged
access to and control over the browser process.

The framework introduced here serves to ease the setup of new internal
pages and to reduce the access that WebContent processes have to the
browser process. WebUI pages can send requests to the browser process
via a `ladybird.sendMessage` API. Responses from the browser are passed
through a WebUIMessage event. So, for example, an internal page may:

    ladybird.sendMessage("getDataFor", { id: 123 });

    document.addEventListener("WebUIMessage", event => {
        if (event.name === "gotData") {
            console.assert(event.data.id === 123);
        }
    });

To handle these messages, we set up a new IPC connection between the
browser and WebContent processes. This connection is torn down when
the user navigates away from the internal page.
This commit is contained in:
Timothy Flynn 2025-03-24 09:27:36 -04:00 committed by Tim Flynn
parent f72d87931f
commit 41aeb9e63a
Notes: github-actions[bot] 2025-03-28 11:32:17 +00:00
25 changed files with 416 additions and 3 deletions

View file

@ -568,6 +568,7 @@ set(SOURCES
Internals/InternalsBase.cpp
Internals/Processes.cpp
Internals/Settings.cpp
Internals/WebUI.cpp
IntersectionObserver/IntersectionObserver.cpp
IntersectionObserver/IntersectionObserverEntry.cpp
Layout/AudioBox.cpp

View file

@ -628,6 +628,7 @@ class RequestList;
namespace Web::Internals {
class Internals;
class Processes;
class WebUI;
}
namespace Web::IntersectionObserver {

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/WebUIPrototype.h>
#include <LibWeb/Internals/WebUI.h>
#include <LibWeb/Page/Page.h>
namespace Web::Internals {
GC_DEFINE_ALLOCATOR(WebUI);
WebUI::WebUI(JS::Realm& realm)
: InternalsBase(realm)
{
}
WebUI::~WebUI() = default;
void WebUI::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(WebUI);
}
void WebUI::send_message(String const& name, JS::Value data)
{
page().client().received_message_from_web_ui(name, data);
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/Value.h>
#include <LibWeb/Internals/InternalsBase.h>
namespace Web::Internals {
class WebUI final : public InternalsBase {
WEB_PLATFORM_OBJECT(WebUI, InternalsBase);
GC_DECLARE_ALLOCATOR(WebUI);
public:
virtual ~WebUI() override;
void send_message(String const& name, JS::Value data);
private:
explicit WebUI(JS::Realm&);
virtual void initialize(JS::Realm&) override;
};
}

View file

@ -0,0 +1,4 @@
[Exposed=Nobody]
interface WebUI {
undefined sendMessage(DOMString name, optional any data = null);
};

View file

@ -400,6 +400,8 @@ public:
virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr<DOM::Node> previous_sibling, [[maybe_unused]] GC::Ptr<DOM::Node> next_sibling, [[maybe_unused]] Optional<String> const& attribute_name) { }
virtual void received_message_from_web_ui([[maybe_unused]] String const& name, [[maybe_unused]] JS::Value data) { }
virtual void update_process_statistics() { }
virtual void request_current_settings() { }

View file

@ -268,6 +268,7 @@ libweb_js_bindings(Internals/InternalAnimationTimeline)
libweb_js_bindings(Internals/Internals)
libweb_js_bindings(Internals/Processes)
libweb_js_bindings(Internals/Settings)
libweb_js_bindings(Internals/WebUI)
libweb_js_bindings(IntersectionObserver/IntersectionObserver)
libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry)
libweb_js_bindings(MathML/MathMLElement)

View file

@ -24,6 +24,7 @@ set(SOURCES
Utilities.cpp
ViewImplementation.cpp
WebContentClient.cpp
WebUI.cpp
)
if (APPLE)
@ -69,6 +70,8 @@ set(GENERATED_SOURCES
../../Services/WebContent/WebContentServerEndpoint.h
../../Services/WebContent/WebDriverClientEndpoint.h
../../Services/WebContent/WebDriverServerEndpoint.h
../../Services/WebContent/WebUIClientEndpoint.h
../../Services/WebContent/WebUIServerEndpoint.h
NativeStyleSheetSource.cpp
UIProcessClientEndpoint.h
UIProcessServerEndpoint.h

View file

@ -18,6 +18,7 @@ class ProcessManager;
class Settings;
class ViewImplementation;
class WebContentClient;
class WebUI;
struct Attribute;
struct ConsoleOutput;

View file

@ -4,12 +4,13 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "WebContentClient.h"
#include "Application.h"
#include "ViewImplementation.h"
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/HelperProcess.h>
#include <LibWebView/ViewImplementation.h>
#include <LibWebView/WebContentClient.h>
#include <LibWebView/WebUI.h>
namespace WebView {
@ -68,6 +69,11 @@ void WebContentClient::unregister_view(u64 page_id)
}
}
void WebContentClient::web_ui_disconnected(Badge<WebUI>)
{
m_web_ui.clear();
}
void WebContentClient::did_paint(u64 page_id, Gfx::IntRect rect, i32 bitmap_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
@ -95,6 +101,13 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi
void WebContentClient::did_finish_loading(u64 page_id, URL::URL url)
{
if (url.scheme() == "about"sv && url.paths().size() == 1) {
if (auto web_ui = WebUI::create(*this, url.paths().first()); web_ui.is_error())
warnln("Could not create WebUI for {}: {}", url, web_ui.error());
else
m_web_ui = web_ui.release_value();
}
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->set_url({}, url);

View file

@ -47,6 +47,8 @@ public:
void register_view(u64 page_id, ViewImplementation&);
void unregister_view(u64 page_id);
void web_ui_disconnected(Badge<WebUI>);
Function<void()> on_web_content_process_crash;
pid_t pid() const { return m_process_handle.pid; }
@ -142,6 +144,8 @@ private:
ProcessHandle m_process_handle;
RefPtr<WebUI> m_web_ui;
static HashTable<WebContentClient*> s_clients;
};

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/Socket.h>
#include <LibCore/System.h>
#include <LibWebView/WebContentClient.h>
#include <LibWebView/WebUI.h>
namespace WebView {
template<typename WebUIType>
static ErrorOr<NonnullRefPtr<WebUIType>> create_web_ui(WebContentClient& client, String host)
{
Array<int, 2> socket_fds { 0, 0 };
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds.data()));
auto client_socket = Core::LocalSocket::adopt_fd(socket_fds[0]);
if (client_socket.is_error()) {
close(socket_fds[0]);
close(socket_fds[1]);
return client_socket.release_error();
}
auto web_ui = WebUIType::create(client, IPC::Transport { client_socket.release_value() }, move(host));
client.async_connect_to_web_ui(0, IPC::File::adopt_fd(socket_fds[1]));
return web_ui;
}
ErrorOr<RefPtr<WebUI>> WebUI::create(WebContentClient&, String)
{
RefPtr<WebUI> web_ui;
if (web_ui)
web_ui->register_interfaces();
return web_ui;
}
WebUI::WebUI(WebContentClient& client, IPC::Transport transport, String host)
: IPC::ConnectionToServer<WebUIClientEndpoint, WebUIServerEndpoint>(*this, move(transport))
, m_client(client)
, m_host(move(host))
{
}
WebUI::~WebUI() = default;
void WebUI::die()
{
m_client.web_ui_disconnected({});
}
void WebUI::register_interface(StringView name, Interface interface)
{
auto result = m_interfaces.set(name, move(interface));
VERIFY(result == HashSetResult::InsertedNewEntry);
}
void WebUI::received_message(String name, JsonValue data)
{
auto interface = m_interfaces.get(name);
if (!interface.has_value()) {
warnln("Received message from WebUI for unrecognized interface: {}", name);
return;
}
interface.value()(move(data));
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/JsonValue.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibIPC/Transport.h>
#include <LibWebView/Forward.h>
#include <WebContent/WebUIClientEndpoint.h>
#include <WebContent/WebUIServerEndpoint.h>
namespace WebView {
class WebUI
: public IPC::ConnectionToServer<WebUIClientEndpoint, WebUIServerEndpoint>
, public WebUIClientEndpoint {
public:
static ErrorOr<RefPtr<WebUI>> create(WebContentClient&, String host);
virtual ~WebUI();
String const& host() const { return m_host; }
protected:
WebUI(WebContentClient&, IPC::Transport, String host);
using Interface = Function<void(JsonValue)>;
virtual void register_interfaces() { }
void register_interface(StringView name, Interface);
private:
virtual void die() override;
virtual void received_message(String name, JsonValue data) override;
WebContentClient& m_client;
String m_host;
HashMap<StringView, Interface> m_interfaces;
};
#define WEB_UI(WebUIType) \
public: \
static NonnullRefPtr<WebUIType> create(WebContentClient& client, IPC::Transport transport, String host) \
{ \
return adopt_ref(*new WebUIType(client, move(transport), move(host))); \
} \
\
private: \
WebUIType(WebContentClient& client, IPC::Transport transport, String host) \
: WebView::WebUI(client, move(transport), move(host)) \
{ \
}
}

View file

@ -433,6 +433,8 @@ compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebContentServer.ipc Se
compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebContentClient.ipc Services/WebContent/WebContentClientEndpoint.h)
compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebDriverClient.ipc Services/WebContent/WebDriverClientEndpoint.h)
compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebDriverServer.ipc Services/WebContent/WebDriverServerEndpoint.h)
compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebUIClient.ipc Services/WebContent/WebUIClientEndpoint.h)
compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebUIServer.ipc Services/WebContent/WebUIServerEndpoint.h)
foreach(lib IN LISTS lagom_standard_libraries)
add_serenity_subdirectory("Libraries/Lib${lib}")

View file

@ -9,6 +9,7 @@ set(SOURCES
PageHost.cpp
WebContentConsoleClient.cpp
WebDriverConnection.cpp
WebUIConnection.cpp
)
if (ANDROID)

View file

@ -124,6 +124,15 @@ void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdrive
}
}
void ConnectionFromClient::connect_to_web_ui(u64 page_id, IPC::File web_ui_socket)
{
if (auto page = this->page(page_id); page.has_value()) {
// FIXME: Propagate this error back to the browser.
if (auto result = page->connect_to_web_ui(move(web_ui_socket)); result.is_error())
dbgln("Unable to connect to the WebUI host: {}", result.error());
}
}
void ConnectionFromClient::connect_to_image_decoder(IPC::File image_decoder_socket)
{
if (on_image_decoder_connection)

View file

@ -61,6 +61,7 @@ private:
virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override;
virtual void set_window_handle(u64 page_id, String handle) override;
virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) override;
virtual void connect_to_web_ui(u64 page_id, IPC::File web_ui_socket) override;
virtual void connect_to_image_decoder(IPC::File image_decoder_socket) override;
virtual void update_system_theme(u64 page_id, Core::AnonymousBuffer) override;
virtual void update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect>, u32) override;

View file

@ -15,5 +15,6 @@ class PageHost;
class PageClient;
class WebContentConsoleClient;
class WebDriverConnection;
class WebUIConnection;
}

View file

@ -32,6 +32,7 @@
#include <WebContent/PageHost.h>
#include <WebContent/WebContentClientEndpoint.h>
#include <WebContent/WebDriverConnection.h>
#include <WebContent/WebUIConnection.h>
namespace WebContent {
@ -95,6 +96,8 @@ void PageClient::visit_edges(JS::Cell::Visitor& visitor)
if (m_webdriver)
m_webdriver->visit_edges(visitor);
if (m_web_ui)
m_web_ui->visit_edges(visitor);
}
ConnectionFromClient& PageClient::client() const
@ -372,6 +375,8 @@ void PageClient::page_did_change_active_document_in_top_level_browsing_context(W
{
auto& realm = document.realm();
m_web_ui.clear();
if (auto console_client = document.console_client()) {
auto& web_content_console_client = as<WebContentConsoleClient>(*console_client);
m_top_level_document_console_client = web_content_console_client;
@ -737,6 +742,24 @@ ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_p
return {};
}
ErrorOr<void> PageClient::connect_to_web_ui(IPC::File web_ui_socket)
{
auto* active_document = page().top_level_browsing_context().active_document();
if (!active_document || !active_document->window())
return {};
VERIFY(!m_web_ui);
m_web_ui = TRY(WebUIConnection::connect(move(web_ui_socket), *active_document));
return {};
}
void PageClient::received_message_from_web_ui(String const& name, JS::Value data)
{
if (m_web_ui)
m_web_ui->received_message_from_web_ui(name, data);
}
void PageClient::initialize_js_console(Web::DOM::Document& document)
{
if (document.is_temporary_document_for_fragment_parsing())
@ -910,4 +933,5 @@ void PageClient::queue_screenshot_task(Optional<Web::UniqueNodeID> node_id)
m_screenshot_tasks.enqueue({ node_id });
page().top_level_traversable()->set_needs_repaint();
}
}

View file

@ -44,6 +44,7 @@ public:
virtual Web::Page const& page() const override { return *m_page; }
ErrorOr<void> connect_to_webdriver(ByteString const& webdriver_ipc_path);
ErrorOr<void> connect_to_web_ui(IPC::File);
virtual void paint_next_frame() override;
virtual void process_screenshot_requests() override;
@ -173,6 +174,7 @@ private:
virtual void page_did_allocate_backing_stores(i32 front_bitmap_id, Gfx::ShareableBitmap front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap back_bitmap) override;
virtual IPC::File request_worker_agent() override;
virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr<Web::DOM::Node> previous_sibling, GC::Ptr<Web::DOM::Node> next_sibling, Optional<String> const& attribute_name) override;
virtual void received_message_from_web_ui(String const& name, JS::Value data) override;
virtual void update_process_statistics() override;
virtual void request_current_settings() override;
virtual void restore_default_settings() override;
@ -211,6 +213,7 @@ private:
Web::CSS::PreferredMotion m_preferred_motion { Web::CSS::PreferredMotion::NoPreference };
RefPtr<WebDriverConnection> m_webdriver;
RefPtr<WebUIConnection> m_web_ui;
BackingStoreManager m_backing_store_manager;

View file

@ -24,6 +24,7 @@ endpoint WebContentServer
set_window_handle(u64 page_id, String handle) =|
connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) =|
connect_to_web_ui(u64 page_id, IPC::File socket_fd) =|
connect_to_image_decoder(IPC::File socket_fd) =|
update_system_theme(u64 page_id, Core::AnonymousBuffer theme_buffer) =|

View file

@ -0,0 +1,3 @@
endpoint WebUIClient {
received_message(String name, JsonValue data) =|
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/FlyString.h>
#include <AK/JsonObject.h>
#include <LibWeb/DOM/CustomEvent.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Internals/WebUI.h>
#include <LibWeb/WebDriver/JSON.h>
#include <WebContent/WebUIConnection.h>
namespace WebContent {
static auto LADYBIRD_PROPERTY = JS::PropertyKey { "ladybird"_fly_string };
static auto WEB_UI_LOADED_EVENT = "WebUILoaded"_fly_string;
static auto WEB_UI_MESSAGE_EVENT = "WebUIMessage"_fly_string;
ErrorOr<NonnullRefPtr<WebUIConnection>> WebUIConnection::connect(IPC::File web_ui_socket, Web::DOM::Document& document)
{
auto socket = TRY(Core::LocalSocket::adopt_fd(web_ui_socket.take_fd()));
TRY(socket->set_blocking(true));
return adopt_ref(*new WebUIConnection(IPC::Transport { move(socket) }, document));
}
WebUIConnection::WebUIConnection(IPC::Transport transport, Web::DOM::Document& document)
: IPC::ConnectionFromClient<WebUIClientEndpoint, WebUIServerEndpoint>(*this, move(transport), 1)
, m_document(document)
{
auto& realm = m_document->realm();
m_document->window()->define_direct_property(LADYBIRD_PROPERTY, realm.create<Web::Internals::WebUI>(realm), JS::default_attributes);
Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, m_document, GC::create_function(realm.heap(), [&document = *m_document]() {
document.dispatch_event(Web::DOM::Event::create(document.realm(), WEB_UI_LOADED_EVENT));
}));
}
WebUIConnection::~WebUIConnection()
{
if (!m_document->window())
return;
(void)m_document->window()->internal_delete(LADYBIRD_PROPERTY);
}
void WebUIConnection::visit_edges(JS::Cell::Visitor& visitor)
{
visitor.visit(m_document);
}
void WebUIConnection::send_message(String name, JsonValue data)
{
if (!m_document->browsing_context())
return;
JsonObject detail;
detail.set("name"sv, move(name));
detail.set("data"sv, move(data));
auto& realm = m_document->realm();
Web::HTML::TemporaryExecutionContext context { realm };
auto serialized_detail = Web::WebDriver::json_deserialize(*m_document->browsing_context(), detail);
if (serialized_detail.is_error()) {
warnln("Unable to serialize JSON data from browser: {}", serialized_detail.error());
return;
}
Web::DOM::CustomEventInit event_init {};
event_init.detail = serialized_detail.value();
m_document->dispatch_event(Web::DOM::CustomEvent::create(realm, WEB_UI_MESSAGE_EVENT, event_init));
}
void WebUIConnection::received_message_from_web_ui(String const& name, JS::Value data)
{
if (!m_document->browsing_context())
return;
auto deserialized_data = Web::WebDriver::json_clone(*m_document->browsing_context(), data);
if (deserialized_data.is_error()) {
warnln("Unable to deserialize JS data from WebUI: {}", deserialized_data.error());
return;
}
async_received_message(name, deserialized_data.value());
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/JsonValue.h>
#include <AK/NonnullRefPtr.h>
#include <LibGC/Ptr.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibIPC/Transport.h>
#include <LibJS/Heap/Cell.h>
#include <LibWeb/Forward.h>
#include <WebContent/WebUIClientEndpoint.h>
#include <WebContent/WebUIServerEndpoint.h>
namespace WebContent {
class WebUIConnection final : public IPC::ConnectionFromClient<WebUIClientEndpoint, WebUIServerEndpoint> {
public:
static ErrorOr<NonnullRefPtr<WebUIConnection>> connect(IPC::File, Web::DOM::Document&);
virtual ~WebUIConnection() override;
void visit_edges(JS::Cell::Visitor&);
void received_message_from_web_ui(String const& name, JS::Value data);
private:
WebUIConnection(IPC::Transport, Web::DOM::Document&);
virtual void die() override { }
virtual void send_message(String name, JsonValue data) override;
GC::Ref<Web::DOM::Document> m_document;
};
}

View file

@ -0,0 +1,3 @@
endpoint WebUIServer {
send_message(String name, JsonValue data) =|
}