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

@ -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)) \
{ \
}
}