Everywhere: Hoist the Libraries folder to the top-level

This commit is contained in:
Timothy Flynn 2024-11-09 12:25:08 -05:00 committed by Andreas Kling
commit 93712b24bf
Notes: github-actions[bot] 2024-11-10 11:51:52 +00:00
4547 changed files with 104 additions and 113 deletions

View file

@ -0,0 +1,258 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Environment.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibCore/TimeZoneWatcher.h>
#include <LibFileSystem/FileSystem.h>
#include <LibImageDecoderClient/Client.h>
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/Database.h>
#include <LibWebView/URL.h>
#include <LibWebView/UserAgent.h>
#include <LibWebView/WebContentClient.h>
namespace WebView {
Application* Application::s_the = nullptr;
Application::Application()
{
VERIFY(!s_the);
s_the = this;
// No need to monitor the system time zone if the TZ environment variable is set, as it overrides system preferences.
if (!Core::Environment::has("TZ"sv)) {
if (auto time_zone_watcher = Core::TimeZoneWatcher::create(); time_zone_watcher.is_error()) {
warnln("Unable to monitor system time zone: {}", time_zone_watcher.error());
} else {
m_time_zone_watcher = time_zone_watcher.release_value();
m_time_zone_watcher->on_time_zone_changed = []() {
WebContentClient::for_each_client([&](WebView::WebContentClient& client) {
client.async_system_time_zone_changed();
return IterationDecision::Continue;
});
};
}
}
m_process_manager.on_process_exited = [this](Process&& process) {
process_did_exit(move(process));
};
}
Application::~Application()
{
s_the = nullptr;
}
void Application::initialize(Main::Arguments const& arguments, URL::URL new_tab_page_url)
{
// Increase the open file limit, as the default limits on Linux cause us to run out of file descriptors with around 15 tabs open.
if (auto result = Core::System::set_resource_limits(RLIMIT_NOFILE, 8192); result.is_error())
warnln("Unable to increase open file limit: {}", result.error());
Vector<ByteString> raw_urls;
Vector<ByteString> certificates;
bool new_window = false;
bool force_new_process = false;
bool allow_popups = false;
bool disable_scripting = false;
bool disable_sql_database = false;
Optional<StringView> debug_process;
Optional<StringView> profile_process;
Optional<StringView> webdriver_content_ipc_path;
Optional<StringView> user_agent_preset;
bool log_all_js_exceptions = false;
bool enable_idl_tracing = false;
bool enable_http_cache = false;
bool enable_autoplay = false;
bool expose_internals_object = false;
bool force_cpu_painting = false;
bool force_fontconfig = false;
bool collect_garbage_on_every_allocation = false;
Core::ArgsParser args_parser;
args_parser.set_general_help("The Ladybird web browser :^)");
args_parser.add_positional_argument(raw_urls, "URLs to open", "url", Core::ArgsParser::Required::No);
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(new_window, "Force opening in a new window", "new-window", 'n');
args_parser.add_option(force_new_process, "Force creation of new browser/chrome process", "force-new-process");
args_parser.add_option(allow_popups, "Disable popup blocking by default", "allow-popups");
args_parser.add_option(disable_scripting, "Disable scripting by default", "disable-scripting");
args_parser.add_option(disable_sql_database, "Disable SQL database", "disable-sql-database");
args_parser.add_option(debug_process, "Wait for a debugger to attach to the given process name (WebContent, RequestServer, etc.)", "debug-process", 0, "process-name");
args_parser.add_option(profile_process, "Enable callgrind profiling of the given process name (WebContent, RequestServer, etc.)", "profile-process", 0, "process-name");
args_parser.add_option(webdriver_content_ipc_path, "Path to WebDriver IPC for WebContent", "webdriver-content-path", 0, "path", Core::ArgsParser::OptionHideMode::CommandLineAndMarkdown);
args_parser.add_option(log_all_js_exceptions, "Log all JavaScript exceptions", "log-all-js-exceptions");
args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing");
args_parser.add_option(enable_http_cache, "Enable HTTP cache", "enable-http-cache");
args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay");
args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting");
args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation", 'g');
args_parser.add_option(Core::ArgsParser::Option {
.argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
.help_string = "Name of the User-Agent preset to use in place of the default User-Agent",
.long_name = "user-agent-preset",
.value_name = "name",
.accept_value = [&](StringView value) {
user_agent_preset = normalize_user_agent_name(value);
return user_agent_preset.has_value();
},
});
create_platform_arguments(args_parser);
args_parser.parse(arguments);
// Our persisted SQL storage assumes it runs in a singleton process. If we have multiple UI processes accessing
// the same underlying database, one of them is likely to fail.
if (force_new_process)
disable_sql_database = true;
Optional<ProcessType> debug_process_type;
Optional<ProcessType> profile_process_type;
if (debug_process.has_value())
debug_process_type = process_type_from_name(*debug_process);
if (profile_process.has_value())
profile_process_type = process_type_from_name(*profile_process);
m_chrome_options = {
.urls = sanitize_urls(raw_urls, new_tab_page_url),
.raw_urls = move(raw_urls),
.new_tab_page_url = move(new_tab_page_url),
.certificates = move(certificates),
.new_window = new_window ? NewWindow::Yes : NewWindow::No,
.force_new_process = force_new_process ? ForceNewProcess::Yes : ForceNewProcess::No,
.allow_popups = allow_popups ? AllowPopups::Yes : AllowPopups::No,
.disable_scripting = disable_scripting ? DisableScripting::Yes : DisableScripting::No,
.disable_sql_database = disable_sql_database ? DisableSQLDatabase::Yes : DisableSQLDatabase::No,
.debug_helper_process = move(debug_process_type),
.profile_helper_process = move(profile_process_type),
};
if (webdriver_content_ipc_path.has_value())
m_chrome_options.webdriver_content_ipc_path = *webdriver_content_ipc_path;
m_web_content_options = {
.command_line = MUST(String::join(' ', arguments.strings)),
.executable_path = MUST(String::from_byte_string(MUST(Core::System::current_executable_path()))),
.user_agent_preset = move(user_agent_preset),
.log_all_js_exceptions = log_all_js_exceptions ? LogAllJSExceptions::Yes : LogAllJSExceptions::No,
.enable_idl_tracing = enable_idl_tracing ? EnableIDLTracing::Yes : EnableIDLTracing::No,
.enable_http_cache = enable_http_cache ? EnableHTTPCache::Yes : EnableHTTPCache::No,
.expose_internals_object = expose_internals_object ? ExposeInternalsObject::Yes : ExposeInternalsObject::No,
.force_cpu_painting = force_cpu_painting ? ForceCPUPainting::Yes : ForceCPUPainting::No,
.force_fontconfig = force_fontconfig ? ForceFontconfig::Yes : ForceFontconfig::No,
.enable_autoplay = enable_autoplay ? EnableAutoplay::Yes : EnableAutoplay::No,
.collect_garbage_on_every_allocation = collect_garbage_on_every_allocation ? CollectGarbageOnEveryAllocation::Yes : CollectGarbageOnEveryAllocation::No,
};
create_platform_options(m_chrome_options, m_web_content_options);
if (m_chrome_options.disable_sql_database == DisableSQLDatabase::No) {
m_database = Database::create().release_value_but_fixme_should_propagate_errors();
m_cookie_jar = CookieJar::create(*m_database).release_value_but_fixme_should_propagate_errors();
} else {
m_cookie_jar = CookieJar::create();
}
}
int Application::execute()
{
int ret = m_event_loop.exec();
m_in_shutdown = true;
return ret;
}
void Application::add_child_process(WebView::Process&& process)
{
m_process_manager.add_process(move(process));
}
#if defined(AK_OS_MACH)
void Application::set_process_mach_port(pid_t pid, Core::MachPort&& port)
{
m_process_manager.set_process_mach_port(pid, move(port));
}
#endif
Optional<Process&> Application::find_process(pid_t pid)
{
return m_process_manager.find_process(pid);
}
void Application::update_process_statistics()
{
m_process_manager.update_all_process_statistics();
}
String Application::generate_process_statistics_html()
{
return m_process_manager.generate_html();
}
void Application::process_did_exit(Process&& process)
{
if (m_in_shutdown)
return;
dbgln_if(WEBVIEW_PROCESS_DEBUG, "Process {} died, type: {}", process.pid(), process_name_from_type(process.type()));
switch (process.type()) {
case ProcessType::ImageDecoder:
if (auto client = process.client<ImageDecoderClient::Client>(); client.has_value()) {
dbgln_if(WEBVIEW_PROCESS_DEBUG, "Restart ImageDecoder process");
if (auto on_death = move(client->on_death)) {
on_death();
}
}
break;
case ProcessType::RequestServer:
dbgln_if(WEBVIEW_PROCESS_DEBUG, "FIXME: Restart request server");
break;
case ProcessType::WebContent:
if (auto client = process.client<WebContentClient>(); client.has_value()) {
dbgln_if(WEBVIEW_PROCESS_DEBUG, "Restart WebContent process");
if (auto on_web_content_process_crash = move(client->on_web_content_process_crash))
on_web_content_process_crash();
}
break;
case ProcessType::WebWorker:
dbgln_if(WEBVIEW_PROCESS_DEBUG, "WebWorker {} died, not sure what to do.", process.pid());
break;
case ProcessType::Chrome:
dbgln("Invalid process type to be dying: Chrome");
VERIFY_NOT_REACHED();
}
}
ErrorOr<LexicalPath> Application::path_for_downloaded_file(StringView file) const
{
auto downloads_directory = Core::StandardPaths::downloads_directory();
if (!FileSystem::is_directory(downloads_directory)) {
auto maybe_downloads_directory = ask_user_for_download_folder();
if (!maybe_downloads_directory.has_value())
return Error::from_errno(ECANCELED);
downloads_directory = maybe_downloads_directory.release_value();
}
if (!FileSystem::is_directory(downloads_directory))
return Error::from_errno(ENOENT);
return LexicalPath::join(downloads_directory, file);
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Badge.h>
#include <AK/ByteString.h>
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <AK/Swift.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Forward.h>
#include <LibMain/Main.h>
#include <LibURL/URL.h>
#include <LibWebView/Options.h>
#include <LibWebView/Process.h>
#include <LibWebView/ProcessManager.h>
namespace WebView {
class Application {
AK_MAKE_NONCOPYABLE(Application);
public:
virtual ~Application();
int execute();
static Application& the() { return *s_the; }
static ChromeOptions const& chrome_options() { return the().m_chrome_options; }
static WebContentOptions const& web_content_options() { return the().m_web_content_options; }
static CookieJar& cookie_jar() { return *the().m_cookie_jar; }
Core::EventLoop& event_loop() { return m_event_loop; }
void add_child_process(Process&&);
// FIXME: Should these methods be part of Application, instead of deferring to ProcessManager?
#if defined(AK_OS_MACH)
void set_process_mach_port(pid_t, Core::MachPort&&);
#endif
Optional<Process&> find_process(pid_t);
// FIXME: Should we just expose the ProcessManager via a getter?
void update_process_statistics();
String generate_process_statistics_html();
ErrorOr<LexicalPath> path_for_downloaded_file(StringView file) const;
protected:
template<DerivedFrom<Application> ApplicationType>
static NonnullOwnPtr<ApplicationType> create(Main::Arguments& arguments, URL::URL new_tab_page_url)
{
auto app = adopt_own(*new ApplicationType { {}, arguments });
app->initialize(arguments, move(new_tab_page_url));
return app;
}
Application();
virtual void process_did_exit(Process&&);
virtual void create_platform_arguments(Core::ArgsParser&) { }
virtual void create_platform_options(ChromeOptions&, WebContentOptions&) { }
virtual Optional<ByteString> ask_user_for_download_folder() const { return {}; }
private:
void initialize(Main::Arguments const& arguments, URL::URL new_tab_page_url);
static Application* s_the;
ChromeOptions m_chrome_options;
WebContentOptions m_web_content_options;
RefPtr<Database> m_database;
OwnPtr<CookieJar> m_cookie_jar;
OwnPtr<Core::TimeZoneWatcher> m_time_zone_watcher;
Core::EventLoop m_event_loop;
ProcessManager m_process_manager;
bool m_in_shutdown { false };
} SWIFT_IMMORTAL_REFERENCE;
}
#define WEB_VIEW_APPLICATION(ApplicationType) \
public: \
static NonnullOwnPtr<ApplicationType> create(Main::Arguments& arguments, URL::URL new_tab_page_url) \
{ \
return WebView::Application::create<ApplicationType>(arguments, move(new_tab_page_url)); \
} \
\
ApplicationType(Badge<WebView::Application>, Main::Arguments&);

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWebView/Attribute.h>
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::Attribute const& attribute)
{
TRY(encoder.encode(attribute.name));
TRY(encoder.encode(attribute.value));
return {};
}
template<>
ErrorOr<WebView::Attribute> IPC::decode(Decoder& decoder)
{
auto name = TRY(decoder.decode<String>());
auto value = TRY(decoder.decode<String>());
return WebView::Attribute { move(name), move(value) };
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibIPC/Forward.h>
namespace WebView {
struct Attribute {
String name;
String value;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, WebView::Attribute const&);
template<>
ErrorOr<WebView::Attribute> decode(Decoder&);
}

View file

@ -0,0 +1,68 @@
include(${SerenityOS_SOURCE_DIR}/Meta/CMake/public_suffix.cmake)
set(SOURCES
Application.cpp
Attribute.cpp
ChromeProcess.cpp
CookieJar.cpp
Database.cpp
InspectorClient.cpp
ProcessHandle.cpp
Process.cpp
ProcessManager.cpp
SearchEngine.cpp
SourceHighlighter.cpp
URL.cpp
UserAgent.cpp
ViewImplementation.cpp
WebContentClient.cpp
${PUBLIC_SUFFIX_SOURCES}
)
set(GENERATED_SOURCES ${CURRENT_LIB_GENERATED})
embed_as_string(
"NativeStyleSheetSource.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Native.css"
"NativeStyleSheetSource.cpp"
"native_stylesheet_source"
NAMESPACE "WebView"
)
compile_ipc(UIProcessServer.ipc UIProcessServerEndpoint.h)
compile_ipc(UIProcessClient.ipc UIProcessClientEndpoint.h)
set(GENERATED_SOURCES
${GENERATED_SOURCES}
../../Services/RequestServer/RequestClientEndpoint.h
../../Services/RequestServer/RequestServerEndpoint.h
../../Services/WebContent/WebContentClientEndpoint.h
../../Services/WebContent/WebContentServerEndpoint.h
../../Services/WebContent/WebDriverClientEndpoint.h
../../Services/WebContent/WebDriverServerEndpoint.h
NativeStyleSheetSource.cpp
UIProcessClientEndpoint.h
UIProcessServerEndpoint.h
)
serenity_lib(LibWebView webview)
target_link_libraries(LibWebView PRIVATE LibCore LibFileSystem LibGfx LibImageDecoderClient LibIPC LibRequests LibJS LibWeb LibUnicode LibURL LibSyntax)
target_compile_definitions(LibWebView PRIVATE ENABLE_PUBLIC_SUFFIX=$<BOOL:${ENABLE_PUBLIC_SUFFIX_DOWNLOAD}>)
# Third-party
find_package(SQLite3 REQUIRED)
target_link_libraries(LibWebView PRIVATE SQLite::SQLite3)
if (ENABLE_INSTALL_HEADERS)
foreach(header ${GENERATED_SOURCES})
get_filename_component(extension ${header} EXT)
if (NOT "${extension}" STREQUAL ".h")
continue()
endif()
get_filename_component(subdirectory ${header} DIRECTORY)
string(REGEX REPLACE "^\\.\\./\\.\\./" "" subdirectory "${subdirectory}")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${header}" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${subdirectory}")
endforeach()
endif()

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <LibCore/Process.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibWebView/Application.h>
#include <LibWebView/ChromeProcess.h>
#include <LibWebView/URL.h>
namespace WebView {
static HashMap<int, RefPtr<UIProcessConnectionFromClient>> s_connections;
class UIProcessClient final
: public IPC::ConnectionToServer<UIProcessClientEndpoint, UIProcessServerEndpoint> {
C_OBJECT(UIProcessClient);
private:
explicit UIProcessClient(IPC::Transport);
};
ErrorOr<ChromeProcess::ProcessDisposition> ChromeProcess::connect(Vector<ByteString> const& raw_urls, NewWindow new_window)
{
static constexpr auto process_name = "Ladybird"sv;
auto [socket_path, pid_path] = TRY(Process::paths_for_process(process_name));
if (auto pid = TRY(Process::get_process_pid(process_name, pid_path)); pid.has_value()) {
TRY(connect_as_client(socket_path, raw_urls, new_window));
return ProcessDisposition::ExitProcess;
}
TRY(connect_as_server(socket_path));
m_pid_path = pid_path;
m_pid_file = TRY(Core::File::open(pid_path, Core::File::OpenMode::Write));
TRY(m_pid_file->write_until_depleted(ByteString::number(::getpid())));
return ProcessDisposition::ContinueMainProcess;
}
ErrorOr<void> ChromeProcess::connect_as_client(ByteString const& socket_path, Vector<ByteString> const& raw_urls, NewWindow new_window)
{
auto socket = TRY(Core::LocalSocket::connect(socket_path));
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
auto client = UIProcessClient::construct(IPC::Transport(move(socket)));
if (new_window == NewWindow::Yes) {
if (!client->send_sync_but_allow_failure<Messages::UIProcessServer::CreateNewWindow>(raw_urls))
dbgln("Failed to send CreateNewWindow message to UIProcess");
} else {
if (!client->send_sync_but_allow_failure<Messages::UIProcessServer::CreateNewTab>(raw_urls))
dbgln("Failed to send CreateNewTab message to UIProcess");
}
return {};
}
ErrorOr<void> ChromeProcess::connect_as_server(ByteString const& socket_path)
{
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
auto socket_fd = TRY(Process::create_ipc_socket(socket_path));
m_socket_path = socket_path;
auto local_server = TRY(Core::LocalServer::try_create());
TRY(local_server->take_over_fd(socket_fd));
m_server_connection = TRY(IPC::MultiServer<UIProcessConnectionFromClient>::try_create(move(local_server)));
m_server_connection->on_new_client = [this](auto& client) {
client.on_new_tab = [this](auto raw_urls) {
if (this->on_new_tab)
this->on_new_tab(raw_urls);
};
client.on_new_window = [this](auto raw_urls) {
if (this->on_new_window)
this->on_new_window(raw_urls);
};
};
return {};
}
ChromeProcess::~ChromeProcess()
{
if (m_pid_file) {
MUST(m_pid_file->truncate(0));
MUST(Core::System::unlink(m_pid_path));
}
if (!m_socket_path.is_empty())
MUST(Core::System::unlink(m_socket_path));
}
UIProcessClient::UIProcessClient(IPC::Transport transport)
: IPC::ConnectionToServer<UIProcessClientEndpoint, UIProcessServerEndpoint>(*this, move(transport))
{
}
UIProcessConnectionFromClient::UIProcessConnectionFromClient(IPC::Transport transport, int client_id)
: IPC::ConnectionFromClient<UIProcessClientEndpoint, UIProcessServerEndpoint>(*this, move(transport), client_id)
{
s_connections.set(client_id, *this);
}
void UIProcessConnectionFromClient::die()
{
s_connections.remove(client_id());
}
void UIProcessConnectionFromClient::create_new_tab(Vector<ByteString> const& urls)
{
if (on_new_tab)
on_new_tab(sanitize_urls(urls, Application::chrome_options().new_tab_page_url));
}
void UIProcessConnectionFromClient::create_new_window(Vector<ByteString> const& urls)
{
if (on_new_window)
on_new_window(sanitize_urls(urls, Application::chrome_options().new_tab_page_url));
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/Function.h>
#include <AK/OwnPtr.h>
#include <AK/Types.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibIPC/Forward.h>
#include <LibIPC/MultiServer.h>
#include <LibWebView/Options.h>
#include <LibWebView/UIProcessClientEndpoint.h>
#include <LibWebView/UIProcessServerEndpoint.h>
namespace WebView {
class UIProcessConnectionFromClient final
: public IPC::ConnectionFromClient<UIProcessClientEndpoint, UIProcessServerEndpoint> {
C_OBJECT(UIProcessConnectionFromClient);
public:
virtual ~UIProcessConnectionFromClient() override = default;
virtual void die() override;
Function<void(Vector<URL::URL> const&)> on_new_tab;
Function<void(Vector<URL::URL> const&)> on_new_window;
private:
UIProcessConnectionFromClient(IPC::Transport, int client_id);
virtual void create_new_tab(Vector<ByteString> const& urls) override;
virtual void create_new_window(Vector<ByteString> const& urls) override;
};
class ChromeProcess {
AK_MAKE_NONCOPYABLE(ChromeProcess);
AK_MAKE_DEFAULT_MOVABLE(ChromeProcess);
public:
enum class ProcessDisposition : u8 {
ContinueMainProcess,
ExitProcess,
};
ChromeProcess() = default;
~ChromeProcess();
ErrorOr<ProcessDisposition> connect(Vector<ByteString> const& raw_urls, NewWindow new_window);
Function<void(Vector<URL::URL> const&)> on_new_tab;
Function<void(Vector<URL::URL> const&)> on_new_window;
private:
ErrorOr<void> connect_as_client(ByteString const& socket_path, Vector<ByteString> const& raw_urls, NewWindow new_window);
ErrorOr<void> connect_as_server(ByteString const& socket_path);
OwnPtr<IPC::MultiServer<UIProcessConnectionFromClient>> m_server_connection;
OwnPtr<Core::File> m_pid_file;
ByteString m_pid_path;
ByteString m_socket_path;
};
}

View file

@ -0,0 +1,707 @@
/*
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2023, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/IPv4Address.h>
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibURL/URL.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/URL.h>
namespace WebView {
static constexpr auto DATABASE_SYNCHRONIZATION_TIMER = AK::Duration::from_seconds(30);
ErrorOr<NonnullOwnPtr<CookieJar>> CookieJar::create(Database& database)
{
Statements statements {};
auto create_table = TRY(database.prepare_statement(MUST(String::formatted(R"#(
CREATE TABLE IF NOT EXISTS Cookies (
name TEXT,
value TEXT,
same_site INTEGER CHECK (same_site >= 0 AND same_site <= {}),
creation_time INTEGER,
last_access_time INTEGER,
expiry_time INTEGER,
domain TEXT,
path TEXT,
secure BOOLEAN,
http_only BOOLEAN,
host_only BOOLEAN,
persistent BOOLEAN,
PRIMARY KEY(name, domain, path)
);)#",
to_underlying(Web::Cookie::SameSite::Lax)))));
database.execute_statement(create_table, {});
statements.insert_cookie = TRY(database.prepare_statement("INSERT OR REPLACE INTO Cookies VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"sv));
statements.expire_cookie = TRY(database.prepare_statement("DELETE FROM Cookies WHERE (expiry_time < ?);"sv));
statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv));
return adopt_own(*new CookieJar { PersistedStorage { database, statements } });
}
NonnullOwnPtr<CookieJar> CookieJar::create()
{
return adopt_own(*new CookieJar { OptionalNone {} });
}
CookieJar::CookieJar(Optional<PersistedStorage> persisted_storage)
: m_persisted_storage(move(persisted_storage))
{
if (!m_persisted_storage.has_value())
return;
// FIXME: Make cookie retrieval lazy so we don't need to retrieve all cookies up front.
auto cookies = m_persisted_storage->select_all_cookies();
m_transient_storage.set_cookies(move(cookies));
m_persisted_storage->synchronization_timer = Core::Timer::create_repeating(
static_cast<int>(DATABASE_SYNCHRONIZATION_TIMER.to_milliseconds()),
[this]() {
for (auto const& it : m_transient_storage.take_dirty_cookies())
m_persisted_storage->insert_cookie(it.value);
auto now = m_transient_storage.purge_expired_cookies();
m_persisted_storage->database.execute_statement(m_persisted_storage->statements.expire_cookie, {}, now);
});
m_persisted_storage->synchronization_timer->start();
}
CookieJar::~CookieJar()
{
if (!m_persisted_storage.has_value())
return;
m_persisted_storage->synchronization_timer->stop();
m_persisted_storage->synchronization_timer->on_timeout();
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3
String CookieJar::get_cookie(const URL::URL& url, Web::Cookie::Source source)
{
m_transient_storage.purge_expired_cookies();
auto domain = canonicalize_domain(url);
if (!domain.has_value())
return {};
auto cookie_list = get_matching_cookies(url, domain.value(), source);
// 4. Serialize the cookie-list into a cookie-string by processing each cookie in the cookie-list in order:
StringBuilder builder;
for (auto const& cookie : cookie_list) {
if (!builder.is_empty())
builder.append("; "sv);
// 1. If the cookies' name is not empty, output the cookie's name followed by the %x3D ("=") character.
if (!cookie.name.is_empty())
builder.appendff("{}=", cookie.name);
// 2. If the cookies' value is not empty, output the cookie's value.
if (!cookie.value.is_empty())
builder.append(cookie.value);
// 3. If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ").
}
return MUST(builder.to_string());
}
void CookieJar::set_cookie(const URL::URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source)
{
auto domain = canonicalize_domain(url);
if (!domain.has_value())
return;
store_cookie(parsed_cookie, url, domain.release_value(), source);
}
// This is based on store_cookie() below, however the whole ParsedCookie->Cookie conversion is skipped.
void CookieJar::update_cookie(Web::Cookie::Cookie cookie)
{
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
// 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie:
if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) {
// 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie.
cookie.creation_time = old_cookie->creation_time;
// 4. Remove the old-cookie from the cookie store.
// NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
}
// 24. Insert the newly-created cookie into the cookie store.
m_transient_storage.set_cookie(move(key), move(cookie));
m_transient_storage.purge_expired_cookies();
}
void CookieJar::dump_cookies()
{
StringBuilder builder;
m_transient_storage.for_each_cookie([&](auto const& cookie) {
static constexpr auto key_color = "\033[34;1m"sv;
static constexpr auto attribute_color = "\033[33m"sv;
static constexpr auto no_color = "\033[0m"sv;
builder.appendff("{}{}{} - ", key_color, cookie.name, no_color);
builder.appendff("{}{}{} - ", key_color, cookie.domain, no_color);
builder.appendff("{}{}{}\n", key_color, cookie.path, no_color);
builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value);
builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.creation_time_to_string());
builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.last_access_time_to_string());
builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.expiry_time_to_string());
builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.secure);
builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.http_only);
builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.host_only);
builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.persistent);
builder.appendff("\t{}SameSite{} = {:s}\n", attribute_color, no_color, Web::Cookie::same_site_to_string(cookie.same_site));
});
dbgln("{} cookies stored\n{}", m_transient_storage.size(), builder.string_view());
}
Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies()
{
Vector<Web::Cookie::Cookie> cookies;
cookies.ensure_capacity(m_transient_storage.size());
m_transient_storage.for_each_cookie([&](auto const& cookie) {
cookies.unchecked_append(cookie);
});
return cookies;
}
// https://w3c.github.io/webdriver/#dfn-associated-cookies
Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies(URL::URL const& url)
{
auto domain = canonicalize_domain(url);
if (!domain.has_value())
return {};
return get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver);
}
Optional<Web::Cookie::Cookie> CookieJar::get_named_cookie(URL::URL const& url, StringView name)
{
auto domain = canonicalize_domain(url);
if (!domain.has_value())
return {};
auto cookie_list = get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver);
for (auto const& cookie : cookie_list) {
if (cookie.name == name)
return cookie;
}
return {};
}
void CookieJar::expire_cookies_with_time_offset(AK::Duration offset)
{
m_transient_storage.purge_expired_cookies(offset);
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.2
Optional<String> CookieJar::canonicalize_domain(const URL::URL& url)
{
if (!url.is_valid() || url.host().has<Empty>())
return {};
// 1. Convert the host name to a sequence of individual domain name labels.
// 2. Convert each label that is not a Non-Reserved LDH (NR-LDH) label, to an A-label (see Section 2.3.2.1 of
// [RFC5890] for the former and latter), or to a "punycode label" (a label resulting from the "ToASCII" conversion
// in Section 4 of [RFC3490]), as appropriate (see Section 6.3 of this specification).
// 3. Concatenate the resulting labels, separated by a %x2E (".") character.
// FIXME: Implement the above conversions.
return MUST(MUST(url.serialized_host()).to_lowercase());
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.1.4
bool CookieJar::path_matches(StringView request_path, StringView cookie_path)
{
// A request-path path-matches a given cookie-path if at least one of the following conditions holds:
// * The cookie-path and the request-path are identical.
if (request_path == cookie_path)
return true;
if (request_path.starts_with(cookie_path)) {
// * The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/").
if (cookie_path.ends_with('/'))
return true;
// * The cookie-path is a prefix of the request-path, and the first character of the request-path that is not
// included in the cookie-path is a %x2F ("/") character.
if (request_path[cookie_path.length()] == '/')
return true;
}
return false;
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#name-storage-model
void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL::URL& url, String canonicalized_domain, Web::Cookie::Source source)
{
// 1. A user agent MAY ignore a received cookie in its entirety. See Section 5.3.
// 2. If cookie-name is empty and cookie-value is empty, abort these steps and ignore the cookie entirely.
if (parsed_cookie.name.is_empty() && parsed_cookie.value.is_empty())
return;
// 3. If the cookie-name or the cookie-value contains a %x00-08 / %x0A-1F / %x7F character (CTL characters
// excluding HTAB), abort these steps and ignore the cookie entirely.
if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.name))
return;
if (Web::Cookie::cookie_contains_invalid_control_character(parsed_cookie.value))
return;
// 4. If the sum of the lengths of cookie-name and cookie-value is more than 4096 octets, abort these steps and
// ignore the cookie entirely.
if (parsed_cookie.name.byte_count() + parsed_cookie.value.byte_count() > 4096)
return;
// 5. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time
// to the current date and time.
Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value };
cookie.creation_time = UnixDateTime::now();
cookie.last_access_time = cookie.creation_time;
// 6. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age":
if (parsed_cookie.expiry_time_from_max_age_attribute.has_value()) {
// 1. Set the cookie's persistent-flag to true.
cookie.persistent = true;
// 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with
// an attribute-name of "Max-Age".
cookie.expiry_time = parsed_cookie.expiry_time_from_max_age_attribute.value();
}
// Otherwise, if the cookie-attribute-list contains an attribute with an attribute-name of "Expires" (and does not
// contain an attribute with an attribute-name of "Max-Age"):
else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) {
// 1. Set the cookie's persistent-flag to true.
cookie.persistent = true;
// 2. Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with
// an attribute-name of "Expires".
cookie.expiry_time = parsed_cookie.expiry_time_from_expires_attribute.value();
}
// Otherwise:
else {
// 1. Set the cookie's persistent-flag to false.
cookie.persistent = false;
// 2. Set the cookie's expiry-time to the latest representable date.
cookie.expiry_time = UnixDateTime::from_unix_time_parts(3000, 1, 1, 0, 0, 0, 0);
}
String domain_attribute;
// 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain":
if (parsed_cookie.domain.has_value()) {
// 1. Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with
// both an attribute-name of "Domain" and an attribute-value whose length is no more than 1024 octets. (Note
// that a leading %x2E ("."), if present, is ignored even though that character is not permitted.)
if (parsed_cookie.domain->byte_count() <= 1024)
domain_attribute = parsed_cookie.domain.value();
}
// Otherwise:
else {
// 1. Let the domain-attribute be the empty string.
}
// 8. If the domain-attribute contains a character that is not in the range of [USASCII] characters, abort these
// steps and ignore the cookie entirely.
for (auto code_point : domain_attribute.code_points()) {
if (!is_ascii(code_point))
return;
}
// 9. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix:
if (is_public_suffix(domain_attribute)) {
// 1. If the domain-attribute is identical to the canonicalized request-host:
if (domain_attribute == canonicalized_domain) {
// 1. Let the domain-attribute be the empty string.
domain_attribute = String {};
}
// Otherwise:
else {
// 1. Abort these steps and ignore the cookie entirely.
return;
}
}
// 10. If the domain-attribute is non-empty:
if (!domain_attribute.is_empty()) {
// 1. If the canonicalized request-host does not domain-match the domain-attribute:
if (!Web::Cookie::domain_matches(canonicalized_domain, domain_attribute)) {
// 1. Abort these steps and ignore the cookie entirely.
return;
}
// Otherwise:
else {
// 1. Set the cookie's host-only-flag to false.
cookie.host_only = false;
// 2. Set the cookie's domain to the domain-attribute.
cookie.domain = move(domain_attribute);
}
}
// Otherwise:
else {
// 1. Set the cookie's host-only-flag to true.
cookie.host_only = true;
// 2. Set the cookie's domain to the canonicalized request-host.
cookie.domain = move(canonicalized_domain);
}
// 11. If the cookie-attribute-list contains an attribute with an attribute-name of "Path", set the cookie's path to
// attribute-value of the last attribute in the cookie-attribute-list with both an attribute-name of "Path" and
// an attribute-value whose length is no more than 1024 octets. Otherwise, set the cookie's path to the
// default-path of the request-uri.
if (parsed_cookie.path.has_value()) {
if (parsed_cookie.path->byte_count() <= 1024)
cookie.path = parsed_cookie.path.value();
} else {
cookie.path = Web::Cookie::default_path(url);
}
// 12. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's
// secure-only-flag to true. Otherwise, set the cookie's secure-only-flag to false.
cookie.secure = parsed_cookie.secure_attribute_present;
// 13. If the request-uri does not denote a "secure" connection (as defined by the user agent), and the cookie's
// secure-only-flag is true, then abort these steps and ignore the cookie entirely.
if (cookie.secure && url.scheme() != "https"sv)
return;
// 14. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's
// http-only-flag to true. Otherwise, set the cookie's http-only-flag to false.
cookie.http_only = parsed_cookie.http_only_attribute_present;
// 15. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is true, abort these steps
// and ignore the cookie entirely.
if (source == Web::Cookie::Source::NonHttp && cookie.http_only)
return;
// 16. If the cookie's secure-only-flag is false, and the request-uri does not denote a "secure" connection, then
// abort these steps and ignore the cookie entirely if the cookie store contains one or more cookies that meet
// all of the following criteria:
if (!cookie.secure && url.scheme() != "https"sv) {
auto ignore_cookie = false;
m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie const& old_cookie) {
// 1. Their name matches the name of the newly-created cookie.
if (old_cookie.name != cookie.name)
return IterationDecision::Continue;
// 2. Their secure-only-flag is true.
if (!old_cookie.secure)
return IterationDecision::Continue;
// 3. Their domain domain-matches the domain of the newly-created cookie, or vice-versa.
if (!Web::Cookie::domain_matches(old_cookie.domain, cookie.domain) && !Web::Cookie::domain_matches(cookie.domain, old_cookie.domain))
return IterationDecision::Continue;
// 4. The path of the newly-created cookie path-matches the path of the existing cookie.
if (!path_matches(cookie.path, old_cookie.path))
return IterationDecision::Continue;
ignore_cookie = true;
return IterationDecision::Break;
});
if (ignore_cookie)
return;
}
// 17. If the cookie-attribute-list contains an attribute with an attribute-name of "SameSite", and an
// attribute-value of "Strict", "Lax", or "None", set the cookie's same-site-flag to the attribute-value of the
// last attribute in the cookie-attribute-list with an attribute-name of "SameSite". Otherwise, set the cookie's
// same-site-flag to "Default".
cookie.same_site = parsed_cookie.same_site_attribute;
// 18. If the cookie's same-site-flag is not "None":
if (cookie.same_site != Web::Cookie::SameSite::None) {
// FIXME: 1. If the cookie was received from a "non-HTTP" API, and the API was called from a navigable's active document
// whose "site for cookies" is not same-site with the top-level origin, then abort these steps and ignore the
// newly created cookie entirely.
// FIXME: 2. If the cookie was received from a "same-site" request (as defined in Section 5.2), skip the remaining
// substeps and continue processing the cookie.
// FIXME: 3. If the cookie was received from a request which is navigating a top-level traversable [HTML] (e.g. if the
// request's "reserved client" is either null or an environment whose "target browsing context"'s navigable
// is a top-level traversable), skip the remaining substeps and continue processing the cookie.
// FIXME: 4. Abort these steps and ignore the newly created cookie entirely.
}
// 19. If the cookie's "same-site-flag" is "None", abort these steps and ignore the cookie entirely unless the
// cookie's secure-only-flag is true.
if (cookie.same_site == Web::Cookie::SameSite::None && !cookie.secure)
return;
auto has_case_insensitive_prefix = [&](StringView value, StringView prefix) {
if (value.length() < prefix.length())
return false;
value = value.substring_view(0, prefix.length());
return value.equals_ignoring_ascii_case(prefix);
};
// 20. If the cookie-name begins with a case-insensitive match for the string "__Secure-", abort these steps and
// ignore the cookie entirely unless the cookie's secure-only-flag is true.
if (has_case_insensitive_prefix(cookie.name, "__Secure-"sv) && !cookie.secure)
return;
// 21. If the cookie-name begins with a case-insensitive match for the string "__Host-", abort these steps and
// ignore the cookie entirely unless the cookie meets all the following criteria:
if (has_case_insensitive_prefix(cookie.name, "__Host-"sv)) {
// 1. The cookie's secure-only-flag is true.
if (!cookie.secure)
return;
// 2. The cookie's host-only-flag is true.
if (!cookie.host_only)
return;
// 3. The cookie-attribute-list contains an attribute with an attribute-name of "Path", and the cookie's path is /.
if (parsed_cookie.path.has_value() && parsed_cookie.path != "/"sv)
return;
}
// 22. If the cookie-name is empty and either of the following conditions are true, abort these steps and ignore
// the cookie entirely:
if (cookie.name.is_empty()) {
// * the cookie-value begins with a case-insensitive match for the string "__Secure-"
if (has_case_insensitive_prefix(cookie.value, "__Secure-"sv))
return;
// * the cookie-value begins with a case-insensitive match for the string "__Host-"
if (has_case_insensitive_prefix(cookie.value, "__Host-"sv))
return;
}
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
// 23. If the cookie store contains a cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie:
if (auto const& old_cookie = m_transient_storage.get_cookie(key); old_cookie.has_value() && old_cookie->host_only == cookie.host_only) {
// 1. Let old-cookie be the existing cookie with the same name, domain, host-only-flag, and path as the
// newly-created cookie. (Notice that this algorithm maintains the invariant that there is at most one such
// cookie.)
// 2. If the newly-created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is true,
// abort these steps and ignore the newly created cookie entirely.
if (source == Web::Cookie::Source::NonHttp && old_cookie->http_only)
return;
// 3. Update the creation-time of the newly-created cookie to match the creation-time of the old-cookie.
cookie.creation_time = old_cookie->creation_time;
// 4. Remove the old-cookie from the cookie store.
// NOTE: Rather than deleting then re-inserting this cookie, we update it in-place.
}
// 24. Insert the newly-created cookie into the cookie store.
m_transient_storage.set_cookie(move(key), move(cookie));
m_transient_storage.purge_expired_cookies();
}
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.8.3
Vector<Web::Cookie::Cookie> CookieJar::get_matching_cookies(const URL::URL& url, StringView canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode)
{
auto now = UnixDateTime::now();
// 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements:
Vector<Web::Cookie::Cookie> cookie_list;
m_transient_storage.for_each_cookie([&](Web::Cookie::Cookie& cookie) {
// * Either:
// The cookie's host-only-flag is true and the canonicalized host of the retrieval's URI is identical to
// the cookie's domain.
bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain);
// Or:
// The cookie's host-only-flag is false and the canonicalized host of the retrieval's URI domain-matches
// the cookie's domain.
bool is_not_host_only_and_domain_matches = !cookie.host_only && Web::Cookie::domain_matches(canonicalized_domain, cookie.domain);
if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches)
return;
// * The retrieval's URI's path path-matches the cookie's path.
if (!path_matches(url.serialize_path(), cookie.path))
return;
// * If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection (as
// defined by the user agent).
if (cookie.secure && url.scheme() != "https"sv)
return;
// * If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP".
if (cookie.http_only && (source != Web::Cookie::Source::Http))
return;
// FIXME: * If the cookie's same-site-flag is not "None" and the retrieval's same-site status is "cross-site", then
// exclude the cookie unless all of the following conditions are met:
// * The retrieval's type is "HTTP".
// * The same-site-flag is "Lax" or "Default".
// * The HTTP request associated with the retrieval uses a "safe" method.
// * The target browsing context of the HTTP request associated with the retrieval is the active browsing context
// or a top-level traversable.
// NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies.
if (mode == MatchingCookiesSpecMode::WebDriver) {
cookie_list.append(cookie);
return;
}
// 3. Update the last-access-time of each cookie in the cookie-list to the current date and time.
// NOTE: We do this first so that both our internal storage and cookie-list are updated.
cookie.last_access_time = now;
// 2. The user agent SHOULD sort the cookie-list in the following order:
auto cookie_path_length = cookie.path.bytes().size();
auto cookie_creation_time = cookie.creation_time;
cookie_list.insert_before_matching(cookie, [cookie_path_length, cookie_creation_time](auto const& entry) {
// * Cookies with longer paths are listed before cookies with shorter paths.
if (cookie_path_length > entry.path.bytes().size()) {
return true;
}
// * Among cookies that have equal-length path fields, cookies with earlier creation-times are listed
// before cookies with later creation-times.
if (cookie_path_length == entry.path.bytes().size()) {
if (cookie_creation_time < entry.creation_time)
return true;
}
return false;
});
});
if (mode != MatchingCookiesSpecMode::WebDriver)
m_transient_storage.purge_expired_cookies();
return cookie_list;
}
void CookieJar::TransientStorage::set_cookies(Cookies cookies)
{
m_cookies = move(cookies);
purge_expired_cookies();
}
void CookieJar::TransientStorage::set_cookie(CookieStorageKey key, Web::Cookie::Cookie cookie)
{
m_cookies.set(key, cookie);
m_dirty_cookies.set(move(key), move(cookie));
}
Optional<Web::Cookie::Cookie> CookieJar::TransientStorage::get_cookie(CookieStorageKey const& key)
{
return m_cookies.get(key);
}
UnixDateTime CookieJar::TransientStorage::purge_expired_cookies(Optional<AK::Duration> offset)
{
auto now = UnixDateTime::now();
if (offset.has_value()) {
now += *offset;
for (auto& cookie : m_dirty_cookies)
cookie.value.expiry_time -= *offset;
}
auto is_expired = [&](auto const&, auto const& cookie) { return cookie.expiry_time < now; };
m_cookies.remove_all_matching(is_expired);
return now;
}
void CookieJar::PersistedStorage::insert_cookie(Web::Cookie::Cookie const& cookie)
{
database.execute_statement(
statements.insert_cookie,
{},
cookie.name,
cookie.value,
to_underlying(cookie.same_site),
cookie.creation_time,
cookie.last_access_time,
cookie.expiry_time,
cookie.domain,
cookie.path,
cookie.secure,
cookie.http_only,
cookie.host_only,
cookie.persistent);
}
static Web::Cookie::Cookie parse_cookie(Database& database, Database::StatementID statement_id)
{
int column = 0;
auto convert_text = [&](auto& field) { field = database.result_column<String>(statement_id, column++); };
auto convert_bool = [&](auto& field) { field = database.result_column<bool>(statement_id, column++); };
auto convert_time = [&](auto& field) { field = database.result_column<UnixDateTime>(statement_id, column++); };
auto convert_same_site = [&](auto& field) {
auto same_site = database.result_column<UnderlyingType<Web::Cookie::SameSite>>(statement_id, column++);
field = static_cast<Web::Cookie::SameSite>(same_site);
};
Web::Cookie::Cookie cookie;
convert_text(cookie.name);
convert_text(cookie.value);
convert_same_site(cookie.same_site);
convert_time(cookie.creation_time);
convert_time(cookie.last_access_time);
convert_time(cookie.expiry_time);
convert_text(cookie.domain);
convert_text(cookie.path);
convert_bool(cookie.secure);
convert_bool(cookie.http_only);
convert_bool(cookie.host_only);
convert_bool(cookie.persistent);
return cookie;
}
CookieJar::TransientStorage::Cookies CookieJar::PersistedStorage::select_all_cookies()
{
HashMap<CookieStorageKey, Web::Cookie::Cookie> cookies;
database.execute_statement(
statements.select_all_cookies,
[&](auto statement_id) {
auto cookie = parse_cookie(database, statement_id);
CookieStorageKey key { cookie.name, cookie.domain, cookie.path };
cookies.set(move(key), move(cookie));
});
return cookies;
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Traits.h>
#include <LibCore/DateTime.h>
#include <LibCore/Timer.h>
#include <LibURL/Forward.h>
#include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Forward.h>
#include <LibWebView/Database.h>
#include <LibWebView/Forward.h>
namespace WebView {
struct CookieStorageKey {
bool operator==(CookieStorageKey const&) const = default;
String name;
String domain;
String path;
};
class CookieJar {
struct Statements {
Database::StatementID insert_cookie { 0 };
Database::StatementID expire_cookie { 0 };
Database::StatementID select_all_cookies { 0 };
};
class TransientStorage {
public:
using Cookies = HashMap<CookieStorageKey, Web::Cookie::Cookie>;
void set_cookies(Cookies);
void set_cookie(CookieStorageKey, Web::Cookie::Cookie);
Optional<Web::Cookie::Cookie> get_cookie(CookieStorageKey const&);
size_t size() const { return m_cookies.size(); }
UnixDateTime purge_expired_cookies(Optional<AK::Duration> offset = {});
auto take_dirty_cookies() { return move(m_dirty_cookies); }
template<typename Callback>
void for_each_cookie(Callback callback)
{
using ReturnType = InvokeResult<Callback, Web::Cookie::Cookie&>;
for (auto& it : m_cookies) {
if constexpr (IsSame<ReturnType, IterationDecision>) {
if (callback(it.value) == IterationDecision::Break)
return;
} else {
static_assert(IsSame<ReturnType, void>);
callback(it.value);
}
}
}
private:
Cookies m_cookies;
Cookies m_dirty_cookies;
};
struct PersistedStorage {
void insert_cookie(Web::Cookie::Cookie const& cookie);
TransientStorage::Cookies select_all_cookies();
Database& database;
Statements statements;
RefPtr<Core::Timer> synchronization_timer {};
};
public:
static ErrorOr<NonnullOwnPtr<CookieJar>> create(Database&);
static NonnullOwnPtr<CookieJar> create();
~CookieJar();
String get_cookie(const URL::URL& url, Web::Cookie::Source source);
void set_cookie(const URL::URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source);
void update_cookie(Web::Cookie::Cookie);
void dump_cookies();
Vector<Web::Cookie::Cookie> get_all_cookies();
Vector<Web::Cookie::Cookie> get_all_cookies(URL::URL const& url);
Optional<Web::Cookie::Cookie> get_named_cookie(URL::URL const& url, StringView name);
void expire_cookies_with_time_offset(AK::Duration);
private:
explicit CookieJar(Optional<PersistedStorage>);
AK_MAKE_NONCOPYABLE(CookieJar);
AK_MAKE_NONMOVABLE(CookieJar);
static Optional<String> canonicalize_domain(const URL::URL& url);
static bool path_matches(StringView request_path, StringView cookie_path);
enum class MatchingCookiesSpecMode {
RFC6265,
WebDriver,
};
void store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL::URL& url, String canonicalized_domain, Web::Cookie::Source source);
Vector<Web::Cookie::Cookie> get_matching_cookies(const URL::URL& url, StringView canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode = MatchingCookiesSpecMode::RFC6265);
Optional<PersistedStorage> m_persisted_storage;
TransientStorage m_transient_storage;
};
}
template<>
struct AK::Traits<WebView::CookieStorageKey> : public AK::DefaultTraits<WebView::CookieStorageKey> {
static unsigned hash(WebView::CookieStorageKey const& key)
{
unsigned hash = 0;
hash = pair_int_hash(hash, key.name.hash());
hash = pair_int_hash(hash, key.domain.hash());
hash = pair_int_hash(hash, key.path.hash());
return hash;
}
};

View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteString.h>
#include <AK/String.h>
#include <AK/Time.h>
#include <LibCore/Directory.h>
#include <LibCore/StandardPaths.h>
#include <LibWebView/Database.h>
#include <sqlite3.h>
namespace WebView {
static constexpr StringView sql_error(int error_code)
{
char const* _sql_error = sqlite3_errstr(error_code);
return { _sql_error, __builtin_strlen(_sql_error) };
}
#define SQL_TRY(expression) \
({ \
/* Ignore -Wshadow to allow nesting the macro. */ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", auto _sql_result = (expression)); \
if (_sql_result != SQLITE_OK) [[unlikely]] \
return Error::from_string_view(sql_error(_sql_result)); \
})
#define SQL_MUST(expression) \
({ \
/* Ignore -Wshadow to allow nesting the macro. */ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", auto _sql_result = (expression)); \
if (_sql_result != SQLITE_OK) [[unlikely]] { \
warnln("\033[31;1mDatabase error\033[0m: {}: {}", sql_error(_sql_result), sqlite3_errmsg(m_database)); \
VERIFY_NOT_REACHED(); \
} \
})
ErrorOr<NonnullRefPtr<Database>> Database::create()
{
// FIXME: Move this to a generic "Ladybird data directory" helper.
auto database_path = ByteString::formatted("{}/Ladybird", Core::StandardPaths::user_data_directory());
TRY(Core::Directory::create(database_path, Core::Directory::CreateDirectories::Yes));
auto database_file = ByteString::formatted("{}/Ladybird.db", database_path);
sqlite3* m_database { nullptr };
SQL_TRY(sqlite3_open(database_file.characters(), &m_database));
return adopt_nonnull_ref_or_enomem(new (nothrow) Database(m_database));
}
Database::Database(sqlite3* database)
: m_database(database)
{
VERIFY(m_database);
}
Database::~Database()
{
for (auto* prepared_statement : m_prepared_statements)
sqlite3_finalize(prepared_statement);
sqlite3_close(m_database);
}
ErrorOr<Database::StatementID> Database::prepare_statement(StringView statement)
{
sqlite3_stmt* prepared_statement { nullptr };
SQL_TRY(sqlite3_prepare_v2(m_database, statement.characters_without_null_termination(), static_cast<int>(statement.length()), &prepared_statement, nullptr));
auto statement_id = m_prepared_statements.size();
m_prepared_statements.append(prepared_statement);
return statement_id;
}
void Database::execute_statement(StatementID statement_id, OnResult on_result)
{
auto* statement = prepared_statement(statement_id);
while (true) {
auto result = sqlite3_step(statement);
switch (result) {
case SQLITE_DONE:
SQL_MUST(sqlite3_reset(statement));
return;
case SQLITE_ROW:
if (on_result)
on_result(statement_id);
continue;
default:
SQL_MUST(result);
return;
}
}
}
template<typename ValueType>
void Database::apply_placeholder(StatementID statement_id, int index, ValueType const& value)
{
auto* statement = prepared_statement(statement_id);
if constexpr (IsSame<ValueType, String>) {
StringView string { value };
SQL_MUST(sqlite3_bind_text(statement, index, string.characters_without_null_termination(), static_cast<int>(string.length()), SQLITE_TRANSIENT));
} else if constexpr (IsSame<ValueType, UnixDateTime>) {
SQL_MUST(sqlite3_bind_int64(statement, index, value.offset_to_epoch().to_milliseconds()));
} else if constexpr (IsSame<ValueType, int>) {
SQL_MUST(sqlite3_bind_int(statement, index, value));
} else if constexpr (IsSame<ValueType, bool>) {
SQL_MUST(sqlite3_bind_int(statement, index, static_cast<int>(value)));
}
}
template void Database::apply_placeholder(StatementID, int, String const&);
template void Database::apply_placeholder(StatementID, int, UnixDateTime const&);
template void Database::apply_placeholder(StatementID, int, int const&);
template void Database::apply_placeholder(StatementID, int, bool const&);
template<typename ValueType>
ValueType Database::result_column(StatementID statement_id, int column)
{
auto* statement = prepared_statement(statement_id);
if constexpr (IsSame<ValueType, String>) {
auto const* text = reinterpret_cast<char const*>(sqlite3_column_text(statement, column));
return MUST(String::from_utf8(StringView { text, strlen(text) }));
} else if constexpr (IsSame<ValueType, UnixDateTime>) {
auto milliseconds = sqlite3_column_int64(statement, column);
return UnixDateTime::from_milliseconds_since_epoch(milliseconds);
} else if constexpr (IsSame<ValueType, int>) {
return sqlite3_column_int(statement, column);
} else if constexpr (IsSame<ValueType, bool>) {
return static_cast<bool>(sqlite3_column_int(statement, column));
}
VERIFY_NOT_REACHED();
}
template String Database::result_column(StatementID, int);
template UnixDateTime Database::result_column(StatementID, int);
template int Database::result_column(StatementID, int);
template bool Database::result_column(StatementID, int);
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/Function.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
struct sqlite3;
struct sqlite3_stmt;
namespace WebView {
class Database : public RefCounted<Database> {
public:
static ErrorOr<NonnullRefPtr<Database>> create();
~Database();
using StatementID = size_t;
using OnResult = Function<void(StatementID)>;
ErrorOr<StatementID> prepare_statement(StringView statement);
void execute_statement(StatementID, OnResult on_result);
template<typename... PlaceholderValues>
void execute_statement(StatementID statement_id, OnResult on_result, PlaceholderValues&&... placeholder_values)
{
int index = 1;
(apply_placeholder(statement_id, index++, forward<PlaceholderValues>(placeholder_values)), ...);
execute_statement(statement_id, move(on_result));
}
template<typename ValueType>
ValueType result_column(StatementID, int column);
private:
explicit Database(sqlite3*);
template<typename ValueType>
void apply_placeholder(StatementID statement_id, int index, ValueType const& value);
ALWAYS_INLINE sqlite3_stmt* prepared_statement(StatementID statement_id)
{
VERIFY(statement_id < m_prepared_statements.size());
return m_prepared_statements[statement_id];
}
sqlite3* m_database { nullptr };
Vector<sqlite3_stmt*> m_prepared_statements;
};
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022, The SerenityOS developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Traits.h>
namespace WebView {
class CookieJar;
class Database;
class InspectorClient;
class OutOfProcessWebView;
class ProcessManager;
class ViewImplementation;
class WebContentClient;
struct Attribute;
struct CookieStorageKey;
struct ProcessHandle;
struct SearchEngine;
}
namespace AK {
template<>
struct Traits<WebView::CookieStorageKey>;
}

View file

@ -0,0 +1,818 @@
/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#include <AK/Enumerate.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/LexicalPath.h>
#include <AK/SourceGenerator.h>
#include <AK/StringBuilder.h>
#include <LibCore/Directory.h>
#include <LibCore/File.h>
#include <LibCore/Resource.h>
#include <LibJS/MarkupGenerator.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/InspectorClient.h>
#include <LibWebView/SourceHighlighter.h>
namespace WebView {
static constexpr auto INSPECTOR_HTML = "resource://ladybird/inspector.html"sv;
static constexpr auto INSPECTOR_CSS = "resource://ladybird/inspector.css"sv;
static constexpr auto INSPECTOR_JS = "resource://ladybird/inspector.js"sv;
static ErrorOr<JsonValue> parse_json_tree(StringView json)
{
auto parsed_tree = TRY(JsonValue::from_string(json));
if (!parsed_tree.is_object())
return Error::from_string_literal("Expected tree to be a JSON object");
return parsed_tree;
}
static String style_sheet_identifier_to_json(Web::CSS::StyleSheetIdentifier const& identifier)
{
return MUST(String::formatted("{{ type: '{}', domNodeId: {}, url: '{}' }}"sv,
Web::CSS::style_sheet_identifier_type_to_string(identifier.type),
identifier.dom_element_unique_id.map([](auto& it) { return String::number(it.value()); }).value_or("undefined"_string),
identifier.url.value_or("undefined"_string)));
}
InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImplementation& inspector_web_view)
: m_content_web_view(content_web_view)
, m_inspector_web_view(inspector_web_view)
{
m_content_web_view.on_received_dom_tree = [this](auto const& dom_tree) {
auto result = parse_json_tree(dom_tree);
if (result.is_error()) {
dbgln("Failed to load DOM tree: {}", result.error());
return;
}
auto dom_tree_html = generate_dom_tree(result.value().as_object());
auto dom_tree_base64 = MUST(encode_base64(dom_tree_html.bytes()));
auto script = MUST(String::formatted("inspector.loadDOMTree(\"{}\");", dom_tree_base64));
m_inspector_web_view.run_javascript(script);
m_dom_tree_loaded = true;
if (m_pending_selection.has_value())
select_node(m_pending_selection.release_value());
else
select_default_node();
};
m_content_web_view.on_received_dom_node_properties = [this](auto const& inspected_node_properties) {
StringBuilder builder;
// FIXME: Support box model metrics and ARIA properties.
auto generate_property_script = [&](auto const& computed_style, auto const& resolved_style, auto const& custom_properties, auto const& fonts) {
builder.append("inspector.createPropertyTables(\""sv);
builder.append_escaped_for_json(computed_style);
builder.append("\", \""sv);
builder.append_escaped_for_json(resolved_style);
builder.append("\", \""sv);
builder.append_escaped_for_json(custom_properties);
builder.append("\");"sv);
builder.append("inspector.createFontList(\""sv);
builder.append_escaped_for_json(fonts);
builder.append("\");"sv);
};
if (inspected_node_properties.has_value()) {
generate_property_script(
inspected_node_properties->computed_style_json,
inspected_node_properties->resolved_style_json,
inspected_node_properties->custom_properties_json,
inspected_node_properties->fonts_json);
} else {
generate_property_script("{}"sv, "{}"sv, "{}"sv, "{}"sv);
}
m_inspector_web_view.run_javascript(builder.string_view());
};
m_content_web_view.on_received_accessibility_tree = [this](auto const& accessibility_tree) {
auto result = parse_json_tree(accessibility_tree);
if (result.is_error()) {
dbgln("Failed to load accessibility tree: {}", result.error());
return;
}
auto accessibility_tree_html = generate_accessibility_tree(result.value().as_object());
auto accessibility_tree_base64 = MUST(encode_base64(accessibility_tree_html.bytes()));
auto script = MUST(String::formatted("inspector.loadAccessibilityTree(\"{}\");", accessibility_tree_base64));
m_inspector_web_view.run_javascript(script);
};
m_content_web_view.on_received_hovered_node_id = [this](auto node_id) {
select_node(node_id);
};
m_content_web_view.on_received_style_sheet_list = [this](auto const& style_sheets) {
StringBuilder builder;
builder.append("inspector.setStyleSheets(["sv);
for (auto& style_sheet : style_sheets) {
builder.appendff("{}, "sv, style_sheet_identifier_to_json(style_sheet));
}
builder.append("]);"sv);
m_inspector_web_view.run_javascript(builder.string_view());
};
m_content_web_view.on_received_style_sheet_source = [this](Web::CSS::StyleSheetIdentifier const& identifier, auto const& base_url, String const& source) {
auto html = highlight_source(identifier.url.value_or({}), base_url, source, Syntax::Language::CSS, HighlightOutputMode::SourceOnly);
auto script = MUST(String::formatted("inspector.setStyleSheetSource({}, \"{}\");",
style_sheet_identifier_to_json(identifier),
MUST(encode_base64(html.bytes()))));
m_inspector_web_view.run_javascript(script);
};
m_content_web_view.on_finshed_editing_dom_node = [this](auto const& node_id) {
m_pending_selection = node_id;
m_dom_tree_loaded = false;
m_dom_node_attributes.clear();
inspect();
};
m_content_web_view.on_received_dom_node_html = [this](auto const& html) {
if (m_content_web_view.on_insert_clipboard_entry)
m_content_web_view.on_insert_clipboard_entry(html, "unspecified"_string, "text/plain"_string);
};
m_content_web_view.on_received_console_message = [this](auto message_index) {
handle_console_message(message_index);
};
m_content_web_view.on_received_console_messages = [this](auto start_index, auto const& message_types, auto const& messages) {
handle_console_messages(start_index, message_types, messages);
};
m_inspector_web_view.enable_inspector_prototype();
m_inspector_web_view.use_native_user_style_sheet();
m_inspector_web_view.on_inspector_loaded = [this]() {
m_inspector_loaded = true;
inspect();
m_content_web_view.js_console_request_messages(0);
};
m_inspector_web_view.on_inspector_requested_dom_tree_context_menu = [this](auto node_id, auto position, auto const& type, auto const& tag, auto const& attribute_index) {
Optional<Attribute> attribute;
if (attribute_index.has_value())
attribute = m_dom_node_attributes.get(node_id)->at(*attribute_index);
m_context_menu_data = ContextMenuData { node_id, tag, attribute };
if (type.is_one_of("text"sv, "comment"sv)) {
if (on_requested_dom_node_text_context_menu)
on_requested_dom_node_text_context_menu(position);
} else if (type == "tag"sv) {
VERIFY(tag.has_value());
if (on_requested_dom_node_tag_context_menu)
on_requested_dom_node_tag_context_menu(position, *tag);
} else if (type == "attribute"sv) {
VERIFY(tag.has_value());
VERIFY(attribute.has_value());
if (on_requested_dom_node_attribute_context_menu)
on_requested_dom_node_attribute_context_menu(position, *tag, *attribute);
}
};
m_inspector_web_view.on_inspector_selected_dom_node = [this](auto node_id, auto const& pseudo_element) {
m_content_web_view.inspect_dom_node(node_id, pseudo_element);
};
m_inspector_web_view.on_inspector_set_dom_node_text = [this](auto node_id, auto const& text) {
m_content_web_view.set_dom_node_text(node_id, text);
};
m_inspector_web_view.on_inspector_set_dom_node_tag = [this](auto node_id, auto const& tag) {
m_content_web_view.set_dom_node_tag(node_id, tag);
};
m_inspector_web_view.on_inspector_added_dom_node_attributes = [this](auto node_id, auto const& attributes) {
m_content_web_view.add_dom_node_attributes(node_id, attributes);
};
m_inspector_web_view.on_inspector_replaced_dom_node_attribute = [this](auto node_id, u32 attribute_index, auto const& replacement_attributes) {
auto const& attribute = m_dom_node_attributes.get(node_id)->at(attribute_index);
m_content_web_view.replace_dom_node_attribute(node_id, attribute.name, replacement_attributes);
};
m_inspector_web_view.on_inspector_requested_cookie_context_menu = [this](auto cookie_index, auto position) {
if (cookie_index >= m_cookies.size())
return;
m_cookie_context_menu_index = cookie_index;
if (on_requested_cookie_context_menu)
on_requested_cookie_context_menu(position, m_cookies[cookie_index]);
};
m_inspector_web_view.on_inspector_requested_style_sheet_source = [this](auto const& identifier) {
m_content_web_view.request_style_sheet_source(identifier);
};
m_inspector_web_view.on_inspector_executed_console_script = [this](auto const& script) {
append_console_source(script);
m_content_web_view.js_console_input(script.to_byte_string());
};
m_inspector_web_view.on_inspector_exported_inspector_html = [this](String const& html) {
auto maybe_inspector_path = Application::the().path_for_downloaded_file("inspector"sv);
if (maybe_inspector_path.is_error()) {
append_console_warning(MUST(String::formatted("Unable to select a download location: {}", maybe_inspector_path.error())));
return;
}
auto inspector_path = maybe_inspector_path.release_value();
if (auto result = Core::Directory::create(inspector_path.string(), Core::Directory::CreateDirectories::Yes); result.is_error()) {
append_console_warning(MUST(String::formatted("Unable to create {}: {}", inspector_path, result.error())));
return;
}
auto export_file = [&](auto name, auto const& contents) {
auto path = inspector_path.append(name);
auto file = Core::File::open(path.string(), Core::File::OpenMode::Write);
if (file.is_error()) {
append_console_warning(MUST(String::formatted("Unable to open {}: {}", path, file.error())));
return false;
}
if (auto result = file.value()->write_until_depleted(contents); result.is_error()) {
append_console_warning(MUST(String::formatted("Unable to save {}: {}", path, result.error())));
return false;
}
return true;
};
auto inspector_css = MUST(Core::Resource::load_from_uri(INSPECTOR_CSS));
auto inspector_js = MUST(Core::Resource::load_from_uri(INSPECTOR_JS));
auto inspector_html = MUST(html.replace(INSPECTOR_CSS, "inspector.css"sv, ReplaceMode::All));
inspector_html = MUST(inspector_html.replace(INSPECTOR_JS, "inspector.js"sv, ReplaceMode::All));
if (!export_file("inspector.html"sv, inspector_html))
return;
if (!export_file("inspector.css"sv, inspector_css->data()))
return;
if (!export_file("inspector.js"sv, inspector_js->data()))
return;
append_console_message(MUST(String::formatted("Exported Inspector files to {}", inspector_path)));
};
load_inspector();
}
InspectorClient::~InspectorClient()
{
m_content_web_view.on_finshed_editing_dom_node = nullptr;
m_content_web_view.on_received_accessibility_tree = nullptr;
m_content_web_view.on_received_console_message = nullptr;
m_content_web_view.on_received_console_messages = nullptr;
m_content_web_view.on_received_dom_node_html = nullptr;
m_content_web_view.on_received_dom_node_properties = nullptr;
m_content_web_view.on_received_dom_tree = nullptr;
m_content_web_view.on_received_hovered_node_id = nullptr;
m_content_web_view.on_received_style_sheet_list = nullptr;
m_content_web_view.on_inspector_requested_style_sheet_source = nullptr;
}
void InspectorClient::inspect()
{
if (!m_inspector_loaded)
return;
m_content_web_view.inspect_dom_tree();
m_content_web_view.inspect_accessibility_tree();
m_content_web_view.list_style_sheets();
load_cookies();
}
void InspectorClient::reset()
{
static constexpr auto script = "inspector.reset();"sv;
m_inspector_web_view.run_javascript(script);
m_body_or_frameset_node_id.clear();
m_pending_selection.clear();
m_dom_tree_loaded = false;
m_dom_node_attributes.clear();
m_highest_notified_message_index = -1;
m_highest_received_message_index = -1;
m_waiting_for_messages = false;
}
void InspectorClient::select_hovered_node()
{
m_content_web_view.get_hovered_node_id();
}
void InspectorClient::select_default_node()
{
if (m_body_or_frameset_node_id.has_value())
select_node(*m_body_or_frameset_node_id);
}
void InspectorClient::clear_selection()
{
m_content_web_view.clear_inspected_dom_node();
static constexpr auto script = "inspector.clearInspectedDOMNode();"sv;
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::select_node(Web::UniqueNodeID node_id)
{
if (!m_dom_tree_loaded) {
m_pending_selection = node_id;
return;
}
auto script = MUST(String::formatted("inspector.inspectDOMNodeID({});", node_id.value()));
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::load_cookies()
{
m_cookies = Application::cookie_jar().get_all_cookies(m_content_web_view.url());
JsonArray json_cookies;
for (auto const& [index, cookie] : enumerate(m_cookies)) {
JsonObject json_cookie;
json_cookie.set("index"sv, JsonValue { index });
json_cookie.set("name"sv, JsonValue { cookie.name });
json_cookie.set("value"sv, JsonValue { cookie.value });
json_cookie.set("domain"sv, JsonValue { cookie.domain });
json_cookie.set("path"sv, JsonValue { cookie.path });
json_cookie.set("creationTime"sv, JsonValue { cookie.creation_time.milliseconds_since_epoch() });
json_cookie.set("lastAccessTime"sv, JsonValue { cookie.last_access_time.milliseconds_since_epoch() });
json_cookie.set("expiryTime"sv, JsonValue { cookie.expiry_time.milliseconds_since_epoch() });
MUST(json_cookies.append(move(json_cookie)));
}
StringBuilder builder;
builder.append("inspector.setCookies("sv);
json_cookies.serialize(builder);
builder.append(");"sv);
m_inspector_web_view.run_javascript(builder.string_view());
}
void InspectorClient::context_menu_edit_dom_node()
{
VERIFY(m_context_menu_data.has_value());
auto script = MUST(String::formatted("inspector.editDOMNodeID({});", m_context_menu_data->dom_node_id));
m_inspector_web_view.run_javascript(script);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_copy_dom_node()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.get_dom_node_html(m_context_menu_data->dom_node_id);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_screenshot_dom_node()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.take_dom_node_screenshot(m_context_menu_data->dom_node_id)
->when_resolved([this](auto const& path) {
append_console_message(MUST(String::formatted("Screenshot saved to: {}", path)));
})
.when_rejected([this](auto const& error) {
append_console_warning(MUST(String::formatted("Warning: {}", error)));
});
m_context_menu_data.clear();
}
void InspectorClient::context_menu_create_child_element()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.create_child_element(m_context_menu_data->dom_node_id);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_create_child_text_node()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.create_child_text_node(m_context_menu_data->dom_node_id);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_clone_dom_node()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.clone_dom_node(m_context_menu_data->dom_node_id);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_remove_dom_node()
{
VERIFY(m_context_menu_data.has_value());
m_content_web_view.remove_dom_node(m_context_menu_data->dom_node_id);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_add_dom_node_attribute()
{
VERIFY(m_context_menu_data.has_value());
auto script = MUST(String::formatted("inspector.addAttributeToDOMNodeID({});", m_context_menu_data->dom_node_id));
m_inspector_web_view.run_javascript(script);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_remove_dom_node_attribute()
{
VERIFY(m_context_menu_data.has_value());
VERIFY(m_context_menu_data->attribute.has_value());
m_content_web_view.replace_dom_node_attribute(m_context_menu_data->dom_node_id, m_context_menu_data->attribute->name, {});
m_context_menu_data.clear();
}
void InspectorClient::context_menu_copy_dom_node_attribute_value()
{
VERIFY(m_context_menu_data.has_value());
VERIFY(m_context_menu_data->attribute.has_value());
if (m_content_web_view.on_insert_clipboard_entry)
m_content_web_view.on_insert_clipboard_entry(m_context_menu_data->attribute->value, "unspecified"_string, "text/plain"_string);
m_context_menu_data.clear();
}
void InspectorClient::context_menu_delete_cookie()
{
VERIFY(m_cookie_context_menu_index.has_value());
VERIFY(*m_cookie_context_menu_index < m_cookies.size());
auto& cookie = m_cookies[*m_cookie_context_menu_index];
cookie.expiry_time = UnixDateTime::earliest();
Application::cookie_jar().update_cookie(move(cookie));
load_cookies();
m_cookie_context_menu_index.clear();
}
void InspectorClient::context_menu_delete_all_cookies()
{
for (auto& cookie : m_cookies) {
cookie.expiry_time = UnixDateTime::earliest();
Application::cookie_jar().update_cookie(move(cookie));
}
load_cookies();
m_cookie_context_menu_index.clear();
}
void InspectorClient::load_inspector()
{
auto inspector_html = MUST(Core::Resource::load_from_uri(INSPECTOR_HTML));
auto generate_property_table = [&](auto name) {
return MUST(String::formatted(R"~~~(
<div id="{0}" class="tab-content">
<table class="property-table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody id="{0}-table">
</tbody>
</table>
</div>
)~~~",
name));
};
StringBuilder builder;
SourceGenerator generator { builder };
generator.set("INSPECTOR_CSS"sv, INSPECTOR_CSS);
generator.set("INSPECTOR_JS"sv, INSPECTOR_JS);
generator.set("INSPECTOR_STYLE"sv, HTML_HIGHLIGHTER_STYLE);
generator.set("COMPUTED_STYLE"sv, generate_property_table("computed-style"sv));
generator.set("RESOVLED_STYLE"sv, generate_property_table("resolved-style"sv));
generator.set("CUSTOM_PROPERTIES"sv, generate_property_table("custom-properties"sv));
generator.append(inspector_html->data());
m_inspector_web_view.load_html(generator.as_string_view());
}
template<typename Generator>
static void generate_tree(StringBuilder& builder, JsonObject const& node, Generator&& generator)
{
if (auto children = node.get_array("children"sv); children.has_value() && !children->is_empty()) {
auto name = node.get_byte_string("name"sv).value_or({});
builder.append("<details>"sv);
builder.append("<summary>"sv);
generator(node);
builder.append("</summary>"sv);
children->for_each([&](auto const& child) {
builder.append("<div>"sv);
generate_tree(builder, child.as_object(), generator);
builder.append("</div>"sv);
});
builder.append("</details>"sv);
} else {
generator(node);
}
}
String InspectorClient::generate_dom_tree(JsonObject const& dom_tree)
{
StringBuilder builder;
generate_tree(builder, dom_tree, [&](JsonObject const& node) {
auto type = node.get_byte_string("type"sv).value_or("unknown"sv);
auto name = node.get_byte_string("name"sv).value_or({});
StringBuilder data_attributes;
auto append_data_attribute = [&](auto name, auto value) {
if (!data_attributes.is_empty())
data_attributes.append(' ');
data_attributes.appendff("data-{}=\"{}\"", name, value);
};
i32 node_id = 0;
if (auto pseudo_element = node.get_integer<i32>("pseudo-element"sv); pseudo_element.has_value()) {
node_id = node.get_integer<i32>("parent-id"sv).value();
append_data_attribute("pseudo-element"sv, *pseudo_element);
} else {
node_id = node.get_integer<i32>("id"sv).value();
}
append_data_attribute("id"sv, node_id);
if (type == "text"sv) {
auto deprecated_text = node.get_byte_string("text"sv).release_value();
deprecated_text = escape_html_entities(deprecated_text);
auto text = MUST(Web::Infra::strip_and_collapse_whitespace(deprecated_text));
builder.appendff("<span data-node-type=\"text\" class=\"hoverable editable\" {}>", data_attributes.string_view());
if (text.is_empty())
builder.appendff("<span class=\"internal\">{}</span>", name);
else
builder.append(text);
builder.append("</span>"sv);
return;
}
if (type == "comment"sv) {
auto comment = node.get_byte_string("data"sv).release_value();
comment = escape_html_entities(comment);
builder.appendff("<span class=\"hoverable comment\" {}>", data_attributes.string_view());
builder.append("<span>&lt;!--</span>"sv);
builder.appendff("<span data-node-type=\"comment\" class=\"editable\">{}</span>", comment);
builder.append("<span>--&gt;</span>"sv);
builder.append("</span>"sv);
return;
}
if (type == "shadow-root"sv) {
auto mode = node.get_byte_string("mode"sv).release_value();
builder.appendff("<span class=\"hoverable internal\" {}>", data_attributes.string_view());
builder.appendff("{} ({})", name, mode);
builder.append("</span>"sv);
return;
}
if (type != "element"sv) {
builder.appendff("<span class=\"hoverable internal\" {}>", data_attributes.string_view());
builder.appendff(name);
builder.append("</span>"sv);
return;
}
if (name.equals_ignoring_ascii_case("BODY"sv) || name.equals_ignoring_ascii_case("FRAMESET"sv))
m_body_or_frameset_node_id = node_id;
auto tag = name.to_lowercase();
builder.appendff("<span class=\"hoverable\" {}>", data_attributes.string_view());
builder.append("<span>&lt;</span>"sv);
builder.appendff("<span data-node-type=\"tag\" data-tag=\"{0}\" class=\"editable tag\">{0}</span>", tag);
if (auto attributes = node.get_object("attributes"sv); attributes.has_value()) {
attributes->for_each_member([&](auto const& name, auto const& value) {
auto& dom_node_attributes = m_dom_node_attributes.ensure(node_id);
auto value_string = value.as_string();
builder.append("&nbsp;"sv);
builder.appendff("<span data-node-type=\"attribute\" data-tag=\"{}\" data-attribute-index={} class=\"editable\">", tag, dom_node_attributes.size());
builder.appendff("<span class=\"attribute-name\">{}</span>", escape_html_entities(name));
builder.append('=');
builder.appendff("<span class=\"attribute-value\">\"{}\"</span>", escape_html_entities(value_string));
builder.append("</span>"sv);
dom_node_attributes.empend(MUST(String::from_byte_string(name)), MUST(String::from_byte_string(value_string)));
});
}
builder.append("<span>&gt;</span>"sv);
builder.append("</span>"sv);
});
return MUST(builder.to_string());
}
String InspectorClient::generate_accessibility_tree(JsonObject const& accessibility_tree)
{
StringBuilder builder;
generate_tree(builder, accessibility_tree, [&](JsonObject const& node) {
auto type = node.get_byte_string("type"sv).value_or("unknown"sv);
auto role = node.get_byte_string("role"sv).value_or({});
if (type == "text"sv) {
auto text = node.get_byte_string("text"sv).release_value();
text = escape_html_entities(text);
builder.appendff("<span class=\"hoverable\">");
builder.append(MUST(Web::Infra::strip_and_collapse_whitespace(text)));
builder.append("</span>"sv);
return;
}
if (type != "element"sv) {
builder.appendff("<span class=\"hoverable internal\">");
builder.appendff(role.to_lowercase());
builder.append("</span>"sv);
return;
}
auto name = node.get_byte_string("name"sv).value_or({});
auto description = node.get_byte_string("description"sv).value_or({});
builder.appendff("<span class=\"hoverable\">");
builder.append(role.to_lowercase());
builder.appendff(" name: \"{}\", description: \"{}\"", name, description);
builder.append("</span>"sv);
});
return MUST(builder.to_string());
}
void InspectorClient::request_console_messages()
{
VERIFY(!m_waiting_for_messages);
m_content_web_view.js_console_request_messages(m_highest_received_message_index + 1);
m_waiting_for_messages = true;
}
void InspectorClient::handle_console_message(i32 message_index)
{
if (message_index <= m_highest_received_message_index) {
dbgln("Notified about console message we already have");
return;
}
if (message_index <= m_highest_notified_message_index) {
dbgln("Notified about console message we're already aware of");
return;
}
m_highest_notified_message_index = message_index;
if (!m_waiting_for_messages)
request_console_messages();
}
void InspectorClient::handle_console_messages(i32 start_index, ReadonlySpan<ByteString> message_types, ReadonlySpan<ByteString> messages)
{
auto end_index = start_index + static_cast<i32>(message_types.size()) - 1;
if (end_index <= m_highest_received_message_index) {
dbgln("Received old console messages");
return;
}
for (size_t i = 0; i < message_types.size(); ++i) {
auto const& type = message_types[i];
auto const& message = messages[i];
if (type == "html"sv)
append_console_output(message);
else if (type == "clear"sv)
clear_console_output();
else if (type == "group"sv)
begin_console_group(message, true);
else if (type == "groupCollapsed"sv)
begin_console_group(message, false);
else if (type == "groupEnd"sv)
end_console_group();
else
VERIFY_NOT_REACHED();
}
m_highest_received_message_index = end_index;
m_waiting_for_messages = false;
if (m_highest_received_message_index < m_highest_notified_message_index)
request_console_messages();
}
void InspectorClient::append_console_source(StringView source)
{
StringBuilder builder;
builder.append("<span class=\"console-prompt\">&gt;&nbsp;</span>"sv);
builder.append(MUST(JS::MarkupGenerator::html_from_source(source)));
append_console_output(builder.string_view());
}
void InspectorClient::append_console_message(StringView message)
{
StringBuilder builder;
builder.append("<span class=\"console-prompt\">#&nbsp;</span>"sv);
builder.appendff("<span class=\"console-message\">{}</span>", message);
append_console_output(builder.string_view());
}
void InspectorClient::append_console_warning(StringView warning)
{
StringBuilder builder;
builder.append("<span class=\"console-prompt\">#&nbsp;</span>"sv);
builder.appendff("<span class=\"console-warning\">{}</span>", warning);
append_console_output(builder.string_view());
}
void InspectorClient::append_console_output(StringView html)
{
auto html_base64 = MUST(encode_base64(html.bytes()));
auto script = MUST(String::formatted("inspector.appendConsoleOutput(\"{}\");", html_base64));
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::clear_console_output()
{
static constexpr auto script = "inspector.clearConsoleOutput();"sv;
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::begin_console_group(StringView label, bool start_expanded)
{
auto label_base64 = MUST(encode_base64(label.bytes()));
auto script = MUST(String::formatted("inspector.beginConsoleGroup(\"{}\", {});", label_base64, start_expanded));
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::end_console_group()
{
static constexpr auto script = "inspector.endConsoleGroup();"sv;
m_inspector_web_view.run_javascript(script);
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/JsonValue.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibGfx/Point.h>
#include <LibWebView/Attribute.h>
#include <LibWebView/ViewImplementation.h>
#pragma once
namespace WebView {
class InspectorClient {
public:
InspectorClient(ViewImplementation& content_web_view, ViewImplementation& inspector_web_view);
~InspectorClient();
void inspect();
void reset();
void select_hovered_node();
void select_default_node();
void clear_selection();
void context_menu_edit_dom_node();
void context_menu_copy_dom_node();
void context_menu_screenshot_dom_node();
void context_menu_create_child_element();
void context_menu_create_child_text_node();
void context_menu_clone_dom_node();
void context_menu_remove_dom_node();
void context_menu_add_dom_node_attribute();
void context_menu_remove_dom_node_attribute();
void context_menu_copy_dom_node_attribute_value();
void context_menu_delete_cookie();
void context_menu_delete_all_cookies();
Function<void(Gfx::IntPoint)> on_requested_dom_node_text_context_menu;
Function<void(Gfx::IntPoint, String const&)> on_requested_dom_node_tag_context_menu;
Function<void(Gfx::IntPoint, String const&, Attribute const&)> on_requested_dom_node_attribute_context_menu;
Function<void(Gfx::IntPoint, Web::Cookie::Cookie const&)> on_requested_cookie_context_menu;
private:
void load_inspector();
String generate_dom_tree(JsonObject const&);
String generate_accessibility_tree(JsonObject const&);
void select_node(Web::UniqueNodeID);
void load_cookies();
void request_console_messages();
void handle_console_message(i32 message_index);
void handle_console_messages(i32 start_index, ReadonlySpan<ByteString> message_types, ReadonlySpan<ByteString> messages);
void append_console_source(StringView);
void append_console_message(StringView);
void append_console_warning(StringView);
void append_console_output(StringView);
void clear_console_output();
void begin_console_group(StringView label, bool start_expanded);
void end_console_group();
ViewImplementation& m_content_web_view;
ViewImplementation& m_inspector_web_view;
Optional<Web::UniqueNodeID> m_body_or_frameset_node_id;
Optional<Web::UniqueNodeID> m_pending_selection;
bool m_inspector_loaded { false };
bool m_dom_tree_loaded { false };
struct ContextMenuData {
Web::UniqueNodeID dom_node_id;
Optional<String> tag;
Optional<Attribute> attribute;
};
Optional<ContextMenuData> m_context_menu_data;
HashMap<Web::UniqueNodeID, Vector<Attribute>> m_dom_node_attributes;
Vector<Web::Cookie::Cookie> m_cookies;
Optional<size_t> m_cookie_context_menu_index;
i32 m_highest_notified_message_index { -1 };
i32 m_highest_received_message_index { -1 };
bool m_waiting_for_messages { false };
};
}

View file

@ -0,0 +1,37 @@
/*
* Stylesheet designed to be used as the user stylesheet by apps that want their
* web content to look like the native GUI.
*/
html {
background-color: -libweb-palette-base;
color: -libweb-palette-base-text;
}
input, textarea {
background-color: -libweb-palette-base;
border-color: -libweb-palette-threed-shadow1;
color: -libweb-palette-base-text;
}
button, input[type=submit], input[type=button], input[type=reset], select {
background-color: -libweb-palette-button;
border-color: -libweb-palette-threed-shadow1;
color: -libweb-palette-button-text;
}
button:hover, input[type=submit]:hover, input[type=button]:hover, input[type=reset]:hover, select:hover {
background-color: -libweb-palette-hover-highlight;
}
:link {
color: -libweb-link;
}
:visited {
color: -libweb-palette-visited-link;
}
:link:active, :visited:active {
color: -libweb-palette-active-link;
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibURL/URL.h>
#include <LibWebView/ProcessType.h>
namespace WebView {
enum class NewWindow {
No,
Yes,
};
enum class ForceNewProcess {
No,
Yes,
};
enum class AllowPopups {
No,
Yes,
};
enum class DisableScripting {
No,
Yes,
};
enum class DisableSQLDatabase {
No,
Yes,
};
enum class EnableAutoplay {
No,
Yes,
};
struct ChromeOptions {
Vector<URL::URL> urls;
Vector<ByteString> raw_urls;
URL::URL new_tab_page_url;
Vector<ByteString> certificates {};
NewWindow new_window { NewWindow::No };
ForceNewProcess force_new_process { ForceNewProcess::No };
AllowPopups allow_popups { AllowPopups::No };
DisableScripting disable_scripting { DisableScripting::No };
DisableSQLDatabase disable_sql_database { DisableSQLDatabase::No };
Optional<ProcessType> debug_helper_process {};
Optional<ProcessType> profile_helper_process {};
Optional<ByteString> webdriver_content_ipc_path {};
};
enum class IsLayoutTestMode {
No,
Yes,
};
enum class LogAllJSExceptions {
No,
Yes,
};
enum class EnableIDLTracing {
No,
Yes,
};
enum class EnableHTTPCache {
No,
Yes,
};
enum class ExposeInternalsObject {
No,
Yes,
};
enum class ForceCPUPainting {
No,
Yes,
};
enum class ForceFontconfig {
No,
Yes,
};
enum class CollectGarbageOnEveryAllocation {
No,
Yes,
};
struct WebContentOptions {
String command_line;
String executable_path;
Optional<ByteString> config_path {};
Optional<StringView> user_agent_preset {};
IsLayoutTestMode is_layout_test_mode { IsLayoutTestMode::No };
LogAllJSExceptions log_all_js_exceptions { LogAllJSExceptions::No };
EnableIDLTracing enable_idl_tracing { EnableIDLTracing::No };
EnableHTTPCache enable_http_cache { EnableHTTPCache::No };
ExposeInternalsObject expose_internals_object { ExposeInternalsObject::No };
ForceCPUPainting force_cpu_painting { ForceCPUPainting::No };
ForceFontconfig force_fontconfig { ForceFontconfig::No };
EnableAutoplay enable_autoplay { EnableAutoplay::No };
CollectGarbageOnEveryAllocation collect_garbage_on_every_allocation { CollectGarbageOnEveryAllocation::No };
};
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/EnumBits.h>
namespace WebView {
enum class PageInfoType {
Text = 1 << 0,
LayoutTree = 1 << 2,
PaintTree = 1 << 3,
GCGraph = 1 << 4,
};
AK_ENUM_BITWISE_OPERATORS(PageInfoType);
}

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/Environment.h>
#include <LibCore/Process.h>
#include <LibCore/Socket.h>
#include <LibCore/StandardPaths.h>
#include <LibWebView/Process.h>
namespace WebView {
Process::Process(ProcessType type, RefPtr<IPC::ConnectionBase> connection, Core::Process process)
: m_process(move(process))
, m_type(type)
, m_connection(move(connection))
{
}
Process::~Process()
{
if (m_connection)
m_connection->shutdown();
}
ErrorOr<Process::ProcessAndIPCTransport> Process::spawn_and_connect_to_process(Core::ProcessSpawnOptions const& options)
{
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
int socket_fds[2] {};
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
ArmedScopeGuard guard_fd_0 { [&] { MUST(Core::System::close(socket_fds[0])); } };
ArmedScopeGuard guard_fd_1 { [&] { MUST(Core::System::close(socket_fds[1])); } };
auto& file_actions = const_cast<Core::ProcessSpawnOptions&>(options).file_actions;
file_actions.append(Core::FileAction::CloseFile { socket_fds[0] });
auto takeover_string = MUST(String::formatted("{}:{}", options.name, socket_fds[1]));
TRY(Core::Environment::set("SOCKET_TAKEOVER"sv, takeover_string, Core::Environment::Overwrite::Yes));
auto process = TRY(Core::Process::spawn(options));
auto ipc_socket = TRY(Core::LocalSocket::adopt_fd(socket_fds[0]));
guard_fd_0.disarm();
TRY(ipc_socket->set_blocking(true));
return ProcessAndIPCTransport { move(process), IPC::Transport(move(ipc_socket)) };
}
ErrorOr<Optional<pid_t>> Process::get_process_pid(StringView process_name, StringView pid_path)
{
if (Core::System::stat(pid_path).is_error())
return OptionalNone {};
Optional<pid_t> pid;
{
auto pid_file = Core::File::open(pid_path, Core::File::OpenMode::Read);
if (pid_file.is_error()) {
warnln("Could not open {} PID file '{}': {}", process_name, pid_path, pid_file.error());
return pid_file.release_error();
}
auto contents = pid_file.value()->read_until_eof();
if (contents.is_error()) {
warnln("Could not read {} PID file '{}': {}", process_name, pid_path, contents.error());
return contents.release_error();
}
pid = StringView { contents.value() }.to_number<pid_t>();
}
if (!pid.has_value()) {
warnln("{} PID file '{}' exists, but with an invalid PID", process_name, pid_path);
TRY(Core::System::unlink(pid_path));
return OptionalNone {};
}
if (kill(*pid, 0) < 0) {
warnln("{} PID file '{}' exists with PID {}, but process cannot be found", process_name, pid_path, *pid);
TRY(Core::System::unlink(pid_path));
return OptionalNone {};
}
return pid;
}
// This is heavily based on how SystemServer's Service creates its socket.
ErrorOr<int> Process::create_ipc_socket(ByteString const& socket_path)
{
if (!Core::System::stat(socket_path).is_error())
TRY(Core::System::unlink(socket_path));
#ifdef SOCK_NONBLOCK
auto socket_fd = TRY(Core::System::socket(AF_LOCAL, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0));
#else
auto socket_fd = TRY(Core::System::socket(AF_LOCAL, SOCK_STREAM, 0));
int option = 1;
TRY(Core::System::ioctl(socket_fd, FIONBIO, &option));
TRY(Core::System::fcntl(socket_fd, F_SETFD, FD_CLOEXEC));
#endif
#if !defined(AK_OS_BSD_GENERIC) && !defined(AK_OS_GNU_HURD)
TRY(Core::System::fchmod(socket_fd, 0600));
#endif
auto socket_address = Core::SocketAddress::local(socket_path);
auto socket_address_un = socket_address.to_sockaddr_un().release_value();
TRY(Core::System::bind(socket_fd, reinterpret_cast<sockaddr*>(&socket_address_un), sizeof(socket_address_un)));
TRY(Core::System::listen(socket_fd, 16));
return socket_fd;
}
ErrorOr<Process::ProcessPaths> Process::paths_for_process(StringView process_name)
{
auto runtime_directory = TRY(Core::StandardPaths::runtime_directory());
auto socket_path = ByteString::formatted("{}/{}.socket", runtime_directory, process_name);
auto pid_path = ByteString::formatted("{}/{}.pid", runtime_directory, process_name);
return ProcessPaths { move(socket_path), move(pid_path) };
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/WeakPtr.h>
#include <LibCore/Process.h>
#include <LibIPC/Connection.h>
#include <LibIPC/Transport.h>
#include <LibWebView/ProcessType.h>
namespace WebView {
class Process {
AK_MAKE_NONCOPYABLE(Process);
AK_MAKE_DEFAULT_MOVABLE(Process);
public:
Process(ProcessType type, RefPtr<IPC::ConnectionBase> connection, Core::Process process);
~Process();
template<typename ClientType>
struct ProcessAndClient;
template<typename ClientType, typename... ClientArguments>
static ErrorOr<ProcessAndClient<ClientType>> spawn(ProcessType type, Core::ProcessSpawnOptions const& options, ClientArguments&&... client_arguments);
ProcessType type() const { return m_type; }
Optional<String> const& title() const { return m_title; }
void set_title(Optional<String> title) { m_title = move(title); }
template<typename ConnectionFromClient>
Optional<ConnectionFromClient&> client()
{
if (auto strong_connection = m_connection.strong_ref())
return verify_cast<ConnectionFromClient>(*strong_connection);
return {};
}
pid_t pid() const { return m_process.pid(); }
struct ProcessPaths {
ByteString socket_path;
ByteString pid_path;
};
static ErrorOr<ProcessPaths> paths_for_process(StringView process_name);
static ErrorOr<Optional<pid_t>> get_process_pid(StringView process_name, StringView pid_path);
static ErrorOr<int> create_ipc_socket(ByteString const& socket_path);
private:
struct ProcessAndIPCTransport {
Core::Process process;
IPC::Transport transport;
};
static ErrorOr<ProcessAndIPCTransport> spawn_and_connect_to_process(Core::ProcessSpawnOptions const& options);
Core::Process m_process;
ProcessType m_type;
Optional<String> m_title;
WeakPtr<IPC::ConnectionBase> m_connection;
};
template<typename ClientType>
struct Process::ProcessAndClient {
Process process;
NonnullRefPtr<ClientType> client;
};
template<typename ClientType, typename... ClientArguments>
ErrorOr<Process::ProcessAndClient<ClientType>> Process::spawn(ProcessType type, Core::ProcessSpawnOptions const& options, ClientArguments&&... client_arguments)
{
auto [core_process, transport] = TRY(spawn_and_connect_to_process(options));
auto client = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ClientType { move(transport), forward<ClientArguments>(client_arguments)... }));
return ProcessAndClient<ClientType> { Process { type, client, move(core_process) }, client };
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWebView/ProcessHandle.h>
template<>
ErrorOr<void> IPC::encode(IPC::Encoder& encoder, WebView::ProcessHandle const& handle)
{
TRY(encoder.encode(handle.pid));
return {};
}
template<>
ErrorOr<WebView::ProcessHandle> IPC::decode(IPC::Decoder& decoder)
{
auto pid = TRY(decoder.decode<pid_t>());
return WebView::ProcessHandle { pid };
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <LibIPC/Forward.h>
namespace WebView {
struct ProcessHandle {
// FIXME: Use mach_port_t on macOS/Hurd and HANDLE on Windows.
pid_t pid { -1 };
};
}
template<>
ErrorOr<void> IPC::encode(IPC::Encoder&, WebView::ProcessHandle const&);
template<>
ErrorOr<WebView::ProcessHandle> IPC::decode(IPC::Decoder&);

View file

@ -0,0 +1,207 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/NumberFormat.h>
#include <AK/String.h>
#include <LibCore/EventLoop.h>
#include <LibCore/System.h>
#include <LibWebView/ProcessManager.h>
namespace WebView {
ProcessType process_type_from_name(StringView name)
{
if (name == "Chrome"sv)
return ProcessType::Chrome;
if (name == "WebContent"sv)
return ProcessType::WebContent;
if (name == "WebWorker"sv)
return ProcessType::WebWorker;
if (name == "RequestServer"sv)
return ProcessType::RequestServer;
if (name == "ImageDecoder"sv)
return ProcessType::ImageDecoder;
dbgln("Unknown process type: '{}'", name);
VERIFY_NOT_REACHED();
}
StringView process_name_from_type(ProcessType type)
{
switch (type) {
case ProcessType::Chrome:
return "Chrome"sv;
case ProcessType::WebContent:
return "WebContent"sv;
case ProcessType::WebWorker:
return "WebWorker"sv;
case ProcessType::RequestServer:
return "RequestServer"sv;
case ProcessType::ImageDecoder:
return "ImageDecoder"sv;
}
VERIFY_NOT_REACHED();
}
ProcessManager::ProcessManager()
: on_process_exited([](Process&&) {})
{
m_signal_handle = Core::EventLoop::register_signal(SIGCHLD, [this](int) {
auto result = Core::System::waitpid(-1, WNOHANG);
while (!result.is_error() && result.value().pid > 0) {
auto& [pid, status] = result.value();
if (WIFEXITED(status) || WIFSIGNALED(status)) {
if (auto process = remove_process(pid); process.has_value())
on_process_exited(process.release_value());
}
result = Core::System::waitpid(-1, WNOHANG);
}
});
add_process(Process(WebView::ProcessType::Chrome, nullptr, Core::Process::current()));
#ifdef AK_OS_MACH
auto self_send_port = mach_task_self();
auto res = mach_port_mod_refs(mach_task_self(), self_send_port, MACH_PORT_RIGHT_SEND, +1);
VERIFY(res == KERN_SUCCESS);
set_process_mach_port(getpid(), Core::MachPort::adopt_right(self_send_port, Core::MachPort::PortRight::Send));
#endif
}
ProcessManager::~ProcessManager()
{
Core::EventLoop::unregister_signal(m_signal_handle);
}
Optional<Process&> ProcessManager::find_process(pid_t pid)
{
return m_processes.get(pid);
}
void ProcessManager::add_process(WebView::Process&& process)
{
Threading::MutexLocker locker { m_lock };
auto pid = process.pid();
auto result = m_processes.set(pid, move(process));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
m_statistics.processes.append(make<Core::Platform::ProcessInfo>(pid));
}
#if defined(AK_OS_MACH)
void ProcessManager::set_process_mach_port(pid_t pid, Core::MachPort&& port)
{
Threading::MutexLocker locker { m_lock };
for (auto const& info : m_statistics.processes) {
if (info->pid == pid) {
info->child_task_port = move(port);
return;
}
}
}
#endif
Optional<Process> ProcessManager::remove_process(pid_t pid)
{
Threading::MutexLocker locker { m_lock };
m_statistics.processes.remove_first_matching([&](auto const& info) {
return (info->pid == pid);
});
return m_processes.take(pid);
}
void ProcessManager::update_all_process_statistics()
{
Threading::MutexLocker locker { m_lock };
(void)update_process_statistics(m_statistics);
}
String ProcessManager::generate_html()
{
Threading::MutexLocker locker { m_lock };
StringBuilder builder;
builder.append(R"(
<html>
<head>
<title>Task Manager</title>
<style>
@media (prefers-color-scheme: dark) {
/* FIXME: We should be able to remove the HTML style when "color-scheme" is supported */
html {
background-color: rgb(30, 30, 30);
color: white;
}
tr:nth-child(even) {
background: rgb(57, 57, 57);
}
}
@media (prefers-color-scheme: light) {
tr:nth-child(even) {
background: #f7f7f7;
}
}
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
border-bottom: 1px solid #aaa;
}
td, th {
padding: 4px;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Name</th>
<th>PID</th>
<th>Memory Usage</th>
<th>CPU %</th>
</tr>
</thead>
<tbody>
)"sv);
m_statistics.for_each_process([&](auto const& process) {
builder.append("<tr>"sv);
builder.append("<td>"sv);
auto& process_handle = this->find_process(process.pid).value();
builder.append(WebView::process_name_from_type(process_handle.type()));
if (process_handle.title().has_value())
builder.appendff(" - {}", escape_html_entities(*process_handle.title()));
builder.append("</td>"sv);
builder.append("<td>"sv);
builder.append(String::number(process.pid));
builder.append("</td>"sv);
builder.append("<td>"sv);
builder.append(human_readable_size(process.memory_usage_bytes));
builder.append("</td>"sv);
builder.append("<td>"sv);
builder.append(MUST(String::formatted("{:.1f}", process.cpu_percent)));
builder.append("</td>"sv);
builder.append("</tr>"sv);
});
builder.append(R"(
</tbody>
</table>
</body>
</html>
)"sv);
return builder.to_string_without_validation();
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibCore/EventReceiver.h>
#include <LibCore/Platform/ProcessStatistics.h>
#include <LibThreading/Mutex.h>
#include <LibWebView/Forward.h>
#include <LibWebView/Process.h>
#include <LibWebView/ProcessType.h>
namespace WebView {
ProcessType process_type_from_name(StringView);
StringView process_name_from_type(ProcessType type);
class ProcessManager {
AK_MAKE_NONCOPYABLE(ProcessManager);
public:
ProcessManager();
~ProcessManager();
void add_process(Process&&);
Optional<Process> remove_process(pid_t);
Optional<Process&> find_process(pid_t);
#if defined(AK_OS_MACH)
void set_process_mach_port(pid_t, Core::MachPort&&);
#endif
void update_all_process_statistics();
String generate_html();
Function<void(Process&&)> on_process_exited;
private:
Core::Platform::ProcessStatistics m_statistics;
HashMap<pid_t, Process> m_processes;
int m_signal_handle { -1 };
Threading::Mutex m_lock;
};
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
namespace WebView {
enum class ProcessType : u8 {
Chrome,
WebContent,
WebWorker,
RequestServer,
ImageDecoder,
};
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Find.h>
#include <AK/String.h>
#include <LibWebView/SearchEngine.h>
namespace WebView {
static constexpr auto builtin_search_engines = Array {
SearchEngine { "Bing"sv, "https://www.bing.com/search?q={}"sv },
SearchEngine { "Brave"sv, "https://search.brave.com/search?q={}"sv },
SearchEngine { "DuckDuckGo"sv, "https://duckduckgo.com/?q={}"sv },
SearchEngine { "Ecosia"sv, "https://ecosia.org/search?q={}"sv },
SearchEngine { "GitHub"sv, "https://github.com/search?q={}"sv },
SearchEngine { "Google"sv, "https://www.google.com/search?q={}"sv },
SearchEngine { "GoogleScholar"sv, "https://scholar.google.com/scholar?q={}"sv },
SearchEngine { "Kagi"sv, "https://kagi.com/search?q={}"sv },
SearchEngine { "Mojeek"sv, "https://www.mojeek.com/search?q={}"sv },
SearchEngine { "Startpage"sv, "https://startpage.com/search?q={}"sv },
SearchEngine { "Wikipedia"sv, "https://en.wikipedia.org/w/index.php?title=Special:Search&search={}"sv },
SearchEngine { "Yahoo"sv, "https://search.yahoo.com/search?p={}"sv },
SearchEngine { "Yandex"sv, "https://yandex.com/search/?text={}"sv },
};
ReadonlySpan<SearchEngine> search_engines()
{
return builtin_search_engines;
}
SearchEngine const& default_search_engine()
{
static auto default_engine = find_search_engine_by_name("Google"sv);
VERIFY(default_engine.has_value());
return *default_engine;
}
Optional<SearchEngine const&> find_search_engine_by_name(StringView name)
{
auto it = AK::find_if(builtin_search_engines.begin(), builtin_search_engines.end(),
[&](auto const& engine) {
return engine.name == name;
});
if (it == builtin_search_engines.end())
return {};
return *it;
}
Optional<SearchEngine const&> find_search_engine_by_query_url(StringView query_url)
{
auto it = AK::find_if(builtin_search_engines.begin(), builtin_search_engines.end(),
[&](auto const& engine) {
return engine.query_url == query_url;
});
if (it == builtin_search_engines.end())
return {};
return *it;
}
String format_search_query_for_display(StringView query_url, StringView query)
{
static constexpr auto MAX_SEARCH_STRING_LENGTH = 32;
if (auto search_engine = find_search_engine_by_query_url(query_url); search_engine.has_value()) {
return MUST(String::formatted("Search {} for \"{:.{}}{}\"",
search_engine->name,
query,
MAX_SEARCH_STRING_LENGTH,
query.length() > MAX_SEARCH_STRING_LENGTH ? "..."sv : ""sv));
}
return MUST(String::formatted("Search for \"{:.{}}{}\"",
query,
MAX_SEARCH_STRING_LENGTH,
query.length() > MAX_SEARCH_STRING_LENGTH ? "..."sv : ""sv));
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Span.h>
#include <AK/StringView.h>
namespace WebView {
struct SearchEngine {
StringView name;
StringView query_url;
};
ReadonlySpan<SearchEngine> search_engines();
SearchEngine const& default_search_engine();
Optional<SearchEngine const&> find_search_engine_by_name(StringView name);
Optional<SearchEngine const&> find_search_engine_by_query_url(StringView query_url);
String format_search_query_for_display(StringView query_url, StringView query);
}

View file

@ -0,0 +1,386 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibJS/SyntaxHighlighter.h>
#include <LibJS/Token.h>
#include <LibURL/URL.h>
#include <LibWeb/CSS/Parser/Token.h>
#include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h>
#include <LibWebView/SourceHighlighter.h>
namespace WebView {
SourceDocument::SourceDocument(StringView source)
: m_source(source)
{
m_source.for_each_split_view('\n', AK::SplitBehavior::KeepEmpty, [&](auto line) {
m_lines.append(Syntax::TextDocumentLine { *this, line });
});
}
Syntax::TextDocumentLine& SourceDocument::line(size_t line_index)
{
return m_lines[line_index];
}
Syntax::TextDocumentLine const& SourceDocument::line(size_t line_index) const
{
return m_lines[line_index];
}
SourceHighlighterClient::SourceHighlighterClient(StringView source, Syntax::Language language)
: m_document(SourceDocument::create(source))
{
// HACK: Syntax highlighters require a palette, but we don't actually care about the output styling, only the type of token for each span.
// Also, getting a palette from the chrome is nontrivial. So, create a dummy blank one and use that.
auto buffer = MUST(Core::AnonymousBuffer::create_with_size(sizeof(Gfx::SystemTheme)));
auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(buffer);
Gfx::Palette dummy_palette { palette_impl };
switch (language) {
case Syntax::Language::CSS:
m_highlighter = make<Web::CSS::SyntaxHighlighter>();
break;
case Syntax::Language::HTML:
m_highlighter = make<Web::HTML::SyntaxHighlighter>();
break;
case Syntax::Language::JavaScript:
m_highlighter = make<JS::SyntaxHighlighter>();
break;
default:
break;
}
if (m_highlighter) {
m_highlighter->attach(*this);
m_highlighter->rehighlight(dummy_palette);
}
}
Vector<Syntax::TextDocumentSpan> const& SourceHighlighterClient::spans() const
{
return document().spans();
}
void SourceHighlighterClient::set_span_at_index(size_t index, Syntax::TextDocumentSpan span)
{
document().set_span_at_index(index, span);
}
Vector<Syntax::TextDocumentFoldingRegion>& SourceHighlighterClient::folding_regions()
{
return document().folding_regions();
}
Vector<Syntax::TextDocumentFoldingRegion> const& SourceHighlighterClient::folding_regions() const
{
return document().folding_regions();
}
ByteString SourceHighlighterClient::highlighter_did_request_text() const
{
return document().text();
}
void SourceHighlighterClient::highlighter_did_request_update()
{
// No-op
}
Syntax::Document& SourceHighlighterClient::highlighter_did_request_document()
{
return document();
}
Syntax::TextPosition SourceHighlighterClient::highlighter_did_request_cursor() const
{
return {};
}
void SourceHighlighterClient::highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan> spans)
{
document().set_spans(span_collection_index, move(spans));
}
void SourceHighlighterClient::highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion> folding_regions)
{
document().set_folding_regions(move(folding_regions));
}
String highlight_source(URL::URL const& url, URL::URL const& base_url, StringView source, Syntax::Language language, HighlightOutputMode mode)
{
SourceHighlighterClient highlighter_client { source, language };
return highlighter_client.to_html_string(url, base_url, mode);
}
StringView SourceHighlighterClient::class_for_token(u64 token_type) const
{
auto class_for_css_token = [](u64 token_type) {
switch (static_cast<Web::CSS::Parser::Token::Type>(token_type)) {
case Web::CSS::Parser::Token::Type::Invalid:
case Web::CSS::Parser::Token::Type::BadString:
case Web::CSS::Parser::Token::Type::BadUrl:
return "invalid"sv;
case Web::CSS::Parser::Token::Type::Ident:
return "identifier"sv;
case Web::CSS::Parser::Token::Type::Function:
return "function"sv;
case Web::CSS::Parser::Token::Type::AtKeyword:
return "at-keyword"sv;
case Web::CSS::Parser::Token::Type::Hash:
return "hash"sv;
case Web::CSS::Parser::Token::Type::String:
return "string"sv;
case Web::CSS::Parser::Token::Type::Url:
return "url"sv;
case Web::CSS::Parser::Token::Type::Number:
case Web::CSS::Parser::Token::Type::Dimension:
case Web::CSS::Parser::Token::Type::Percentage:
return "number"sv;
case Web::CSS::Parser::Token::Type::Whitespace:
return "whitespace"sv;
case Web::CSS::Parser::Token::Type::Delim:
case Web::CSS::Parser::Token::Type::Colon:
case Web::CSS::Parser::Token::Type::Semicolon:
case Web::CSS::Parser::Token::Type::Comma:
case Web::CSS::Parser::Token::Type::OpenSquare:
case Web::CSS::Parser::Token::Type::CloseSquare:
case Web::CSS::Parser::Token::Type::OpenParen:
case Web::CSS::Parser::Token::Type::CloseParen:
case Web::CSS::Parser::Token::Type::OpenCurly:
case Web::CSS::Parser::Token::Type::CloseCurly:
return "delimiter"sv;
case Web::CSS::Parser::Token::Type::CDO:
case Web::CSS::Parser::Token::Type::CDC:
return "comment"sv;
case Web::CSS::Parser::Token::Type::EndOfFile:
default:
break;
}
return ""sv;
};
auto class_for_js_token = [](u64 token_type) {
auto category = JS::Token::category(static_cast<JS::TokenType>(token_type));
switch (category) {
case JS::TokenCategory::Invalid:
return "invalid"sv;
case JS::TokenCategory::Number:
return "number"sv;
case JS::TokenCategory::String:
return "string"sv;
case JS::TokenCategory::Punctuation:
return "punctuation"sv;
case JS::TokenCategory::Operator:
return "operator"sv;
case JS::TokenCategory::Keyword:
return "keyword"sv;
case JS::TokenCategory::ControlKeyword:
return "control-keyword"sv;
case JS::TokenCategory::Identifier:
return "identifier"sv;
default:
break;
}
return ""sv;
};
switch (m_highlighter->language()) {
case Syntax::Language::CSS:
return class_for_css_token(token_type);
case Syntax::Language::JavaScript:
return class_for_js_token(token_type);
case Syntax::Language::HTML: {
// HTML has nested CSS and JS highlighters, so we have to decode their token types.
// HTML
if (token_type < Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE) {
switch (static_cast<Web::HTML::AugmentedTokenKind>(token_type)) {
case Web::HTML::AugmentedTokenKind::AttributeName:
return "attribute-name"sv;
case Web::HTML::AugmentedTokenKind::AttributeValue:
return "attribute-value"sv;
case Web::HTML::AugmentedTokenKind::OpenTag:
case Web::HTML::AugmentedTokenKind::CloseTag:
return "tag"sv;
case Web::HTML::AugmentedTokenKind::Comment:
return "comment"sv;
case Web::HTML::AugmentedTokenKind::Doctype:
return "doctype"sv;
case Web::HTML::AugmentedTokenKind::__Count:
default:
return ""sv;
}
}
// JS
if (token_type < Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE) {
return class_for_js_token(token_type - Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE);
}
// CSS
return class_for_css_token(token_type - Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE);
}
default:
return "unknown"sv;
}
}
String SourceHighlighterClient::to_html_string(URL::URL const& url, URL::URL const& base_url, HighlightOutputMode mode) const
{
StringBuilder builder;
auto append_escaped = [&](Utf32View text) {
for (auto code_point : text) {
if (code_point == '&') {
builder.append("&amp;"sv);
} else if (code_point == 0xA0) {
builder.append("&nbsp;"sv);
} else if (code_point == '<') {
builder.append("&lt;"sv);
} else if (code_point == '>') {
builder.append("&gt;"sv);
} else {
builder.append_code_point(code_point);
}
}
};
auto start_token = [&](u64 type) {
builder.appendff("<span class=\"{}\">", class_for_token(type));
};
auto end_token = [&]() {
builder.append("</span>"sv);
};
if (mode == HighlightOutputMode::FullDocument) {
builder.append(R"~~~(
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="dark light">)~~~"sv);
builder.appendff("<title>View Source - {}</title>", escape_html_entities(url.serialize_for_display()));
builder.appendff("<style type=\"text/css\">{}</style>", HTML_HIGHLIGHTER_STYLE);
builder.append(R"~~~(
</head>
<body>)~~~"sv);
}
builder.append("<pre class=\"html\">"sv);
static constexpr auto href = to_array<u32>({ 'h', 'r', 'e', 'f' });
static constexpr auto src = to_array<u32>({ 's', 'r', 'c' });
bool linkify_attribute = false;
auto resolve_url_for_attribute = [&](Utf32View const& attribute_value) -> Optional<URL::URL> {
if (!linkify_attribute)
return {};
auto attribute_url = MUST(String::formatted("{}", attribute_value));
auto attribute_url_without_quotes = attribute_url.bytes_as_string_view().trim("\""sv);
if (auto resolved = Web::DOMURL::parse(attribute_url_without_quotes, base_url); resolved.is_valid())
return resolved;
return {};
};
size_t span_index = 0;
for (size_t line_index = 0; line_index < document().line_count(); ++line_index) {
auto& line = document().line(line_index);
auto line_view = line.view();
builder.append("<div class=\"line\">"sv);
size_t next_column = 0;
auto draw_text_helper = [&](size_t start, size_t end, Optional<Syntax::TextDocumentSpan const&> span) {
size_t length = end - start;
if (length == 0)
return;
auto text = line_view.substring_view(start, length);
if (span.has_value()) {
bool append_anchor_close = false;
if (span->data == to_underlying(Web::HTML::AugmentedTokenKind::AttributeName)) {
linkify_attribute = text == Utf32View { href } || text == Utf32View { src };
} else if (span->data == to_underlying(Web::HTML::AugmentedTokenKind::AttributeValue)) {
if (auto href = resolve_url_for_attribute(text); href.has_value()) {
builder.appendff("<a href=\"{}\">", *href);
append_anchor_close = true;
}
}
start_token(span->data);
append_escaped(text);
end_token();
if (append_anchor_close)
builder.append("</a>"sv);
} else {
append_escaped(text);
}
};
while (span_index < document().spans().size()) {
auto& span = document().spans()[span_index];
if (span.range.start().line() > line_index) {
// No more spans in this line, moving on
break;
}
size_t span_start;
if (span.range.start().line() < line_index) {
span_start = 0;
} else {
span_start = span.range.start().column();
}
size_t span_end;
bool span_consumed;
if (span.range.end().line() > line_index) {
span_end = line.length();
span_consumed = false;
} else {
span_end = span.range.end().column();
span_consumed = true;
}
if (span_start != next_column) {
// Draw unspanned text between spans
draw_text_helper(next_column, span_start, {});
}
draw_text_helper(span_start, span_end, span);
next_column = span_end;
if (!span_consumed) {
// Continue with same span on next line
break;
} else {
++span_index;
}
}
// Draw unspanned text after last span
if (next_column < line.length()) {
draw_text_helper(next_column, line.length(), {});
}
builder.append("</div>"sv);
}
builder.append("</pre>"sv);
if (mode == HighlightOutputMode::FullDocument) {
builder.append(R"~~~(
</body>
</html>
)~~~"sv);
}
return builder.to_string_without_validation();
}
}

View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <LibSyntax/Document.h>
#include <LibSyntax/HighlighterClient.h>
#include <LibSyntax/Language.h>
#include <LibURL/Forward.h>
namespace WebView {
enum class HighlightOutputMode {
FullDocument, // Include HTML header, title, style sheet, etc
SourceOnly, // Just the highlighted source
};
class SourceDocument final : public Syntax::Document {
public:
static NonnullRefPtr<SourceDocument> create(StringView source)
{
return adopt_ref(*new (nothrow) SourceDocument(source));
}
virtual ~SourceDocument() = default;
StringView text() const { return m_source; }
size_t line_count() const { return m_lines.size(); }
// ^ Syntax::Document
virtual Syntax::TextDocumentLine const& line(size_t line_index) const override;
virtual Syntax::TextDocumentLine& line(size_t line_index) override;
private:
SourceDocument(StringView source);
// ^ Syntax::Document
virtual void update_views(Badge<Syntax::TextDocumentLine>) override { }
StringView m_source;
Vector<Syntax::TextDocumentLine> m_lines;
};
class SourceHighlighterClient final : public Syntax::HighlighterClient {
public:
SourceHighlighterClient(StringView source, Syntax::Language);
virtual ~SourceHighlighterClient() = default;
String to_html_string(URL::URL const& url, URL::URL const& base_url, HighlightOutputMode) const;
private:
// ^ Syntax::HighlighterClient
virtual Vector<Syntax::TextDocumentSpan> const& spans() const override;
virtual void set_span_at_index(size_t index, Syntax::TextDocumentSpan span) override;
virtual Vector<Syntax::TextDocumentFoldingRegion>& folding_regions() override;
virtual Vector<Syntax::TextDocumentFoldingRegion> const& folding_regions() const override;
virtual ByteString highlighter_did_request_text() const override;
virtual void highlighter_did_request_update() override;
virtual Syntax::Document& highlighter_did_request_document() override;
virtual Syntax::TextPosition highlighter_did_request_cursor() const override;
virtual void highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan>) override;
virtual void highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion>) override;
StringView class_for_token(u64 token_type) const;
SourceDocument& document() const { return *m_document; }
NonnullRefPtr<SourceDocument> m_document;
OwnPtr<Syntax::Highlighter> m_highlighter;
};
String highlight_source(URL::URL const& url, URL::URL const& base_url, StringView, Syntax::Language, HighlightOutputMode);
constexpr inline StringView HTML_HIGHLIGHTER_STYLE = R"~~~(
@media (prefers-color-scheme: dark) {
/* FIXME: We should be able to remove the HTML style when "color-scheme" is supported */
html {
background-color: rgb(30, 30, 30);
color: white;
counter-reset: line;
}
:root {
--comment-color: lightgreen;
--keyword-color: orangered;
--name-color: orange;
--value-color: deepskyblue;
--internal-color: darkgrey;
--string-color: goldenrod;
--error-color: red;
--line-number-color: darkgrey;
}
}
@media (prefers-color-scheme: light) {
:root {
--comment-color: green;
--keyword-color: red;
--name-color: darkorange;
--value-color: blue;
--internal-color: dimgrey;
--string-color: darkgoldenrod;
--error-color: darkred;
--line-number-color: dimgrey;
}
}
.html {
font-size: 10pt;
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.line {
counter-increment: line;
white-space: pre;
}
.line::before {
content: counter(line) " ";
display: inline-block;
width: 2.5em;
padding-right: 0.5em;
text-align: right;
color: var(--line-number-color);
}
.tag {
font-weight: 600;
color: var(--keyword-color);
}
.comment {
color: var(--comment-color);
}
.attribute-name {
color: var(--name-color);
}
.attribute-value {
color: var(--value-color);
}
.internal {
color: var(--internal-color);
}
.invalid {
color: var(--error-color);
text-decoration: currentColor wavy underline;
}
.at-keyword, .function, .keyword, .control-keyword, .url {
color: var(--keyword-color);
}
.number, .hash {
color: var(--value-color);
}
.string {
color: var(--string-color);
}
)~~~"sv;
}

View file

@ -0,0 +1,2 @@
endpoint UIProcessClient {
}

View file

@ -0,0 +1,4 @@
endpoint UIProcessServer {
create_new_tab(Vector<ByteString> urls) => ()
create_new_window(Vector<ByteString> urls) => ()
}

View file

@ -0,0 +1,176 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/String.h>
#include <LibCore/System.h>
#include <LibFileSystem/FileSystem.h>
#include <LibWebView/URL.h>
#if defined(ENABLE_PUBLIC_SUFFIX)
# include <LibWebView/PublicSuffixData.h>
#endif
namespace WebView {
bool is_public_suffix([[maybe_unused]] StringView host)
{
#if defined(ENABLE_PUBLIC_SUFFIX)
return PublicSuffixData::the()->is_public_suffix(host);
#else
return false;
#endif
}
Optional<String> get_public_suffix([[maybe_unused]] StringView host)
{
#if defined(ENABLE_PUBLIC_SUFFIX)
return MUST(PublicSuffixData::the()->get_public_suffix(host));
#else
return {};
#endif
}
Optional<URL::URL> sanitize_url(StringView url, Optional<StringView> search_engine, AppendTLD append_tld)
{
if (FileSystem::exists(url.trim_whitespace())) {
auto path = FileSystem::real_path(url.trim_whitespace());
if (path.is_error())
return {};
return URL::create_with_file_scheme(path.value());
}
auto format_search_engine = [&]() -> Optional<URL::URL> {
if (!search_engine.has_value())
return {};
return MUST(String::formatted(*search_engine, URL::percent_decode(url)));
};
String url_buffer;
if (append_tld == AppendTLD::Yes) {
// FIXME: Expand the list of top level domains.
if (!url.ends_with(".com"sv) && !url.ends_with(".net"sv) && !url.ends_with(".org"sv)) {
url_buffer = MUST(String::formatted("{}.com", url));
url = url_buffer;
}
}
ByteString url_with_scheme = url;
if (!(url_with_scheme.starts_with("about:"sv) || url_with_scheme.contains("://"sv) || url_with_scheme.starts_with("data:"sv)))
url_with_scheme = ByteString::formatted("https://{}"sv, url_with_scheme);
auto result = URL::create_with_url_or_path(url_with_scheme);
if (!result.is_valid())
return format_search_engine();
return result;
}
Vector<URL::URL> sanitize_urls(ReadonlySpan<ByteString> raw_urls, URL::URL const& new_tab_page_url)
{
Vector<URL::URL> sanitized_urls;
sanitized_urls.ensure_capacity(raw_urls.size());
for (auto const& raw_url : raw_urls) {
if (auto url = sanitize_url(raw_url); url.has_value())
sanitized_urls.unchecked_append(url.release_value());
}
if (sanitized_urls.is_empty())
sanitized_urls.append(new_tab_page_url);
return sanitized_urls;
}
static URLParts break_file_url_into_parts(URL::URL const& url, StringView url_string)
{
auto scheme = url_string.substring_view(0, url.scheme().bytes_as_string_view().length() + "://"sv.length());
auto path = url_string.substring_view(scheme.length());
return URLParts { scheme, path, {} };
}
static URLParts break_web_url_into_parts(URL::URL const& url, StringView url_string)
{
auto scheme = url_string.substring_view(0, url.scheme().bytes_as_string_view().length() + "://"sv.length());
auto url_without_scheme = url_string.substring_view(scheme.length());
StringView domain;
StringView remainder;
if (auto index = url_without_scheme.find_any_of("/?#"sv); index.has_value()) {
domain = url_without_scheme.substring_view(0, *index);
remainder = url_without_scheme.substring_view(*index);
} else {
domain = url_without_scheme;
}
auto public_suffix = get_public_suffix(domain);
if (!public_suffix.has_value() || !domain.ends_with(*public_suffix))
return { scheme, domain, remainder };
auto subdomain = domain.substring_view(0, domain.length() - public_suffix->bytes_as_string_view().length());
subdomain = subdomain.trim("."sv, TrimMode::Right);
if (auto index = subdomain.find_last('.'); index.has_value()) {
subdomain = subdomain.substring_view(0, *index + 1);
domain = domain.substring_view(subdomain.length());
} else {
subdomain = {};
}
auto scheme_and_subdomain = url_string.substring_view(0, scheme.length() + subdomain.length());
return { scheme_and_subdomain, domain, remainder };
}
Optional<URLParts> break_url_into_parts(StringView url_string)
{
auto url = URL::create_with_url_or_path(url_string);
if (!url.is_valid())
return {};
auto const& scheme = url.scheme();
auto scheme_length = scheme.bytes_as_string_view().length();
if (!url_string.starts_with(scheme))
return {};
if (!url_string.substring_view(scheme_length).starts_with("://"sv))
return {};
if (url.scheme() == "file"sv)
return break_file_url_into_parts(url, url_string);
if (url.scheme().is_one_of("http"sv, "https"sv))
return break_web_url_into_parts(url, url_string);
return {};
}
URLType url_type(URL::URL const& url)
{
if (url.scheme() == "mailto"sv)
return URLType::Email;
if (url.scheme() == "tel"sv)
return URLType::Telephone;
return URLType::Other;
}
String url_text_to_copy(URL::URL const& url)
{
auto url_text = MUST(url.to_string());
if (url.scheme() == "mailto"sv)
return MUST(url_text.substring_from_byte_offset("mailto:"sv.length()));
if (url.scheme() == "tel"sv)
return MUST(url_text.substring_from_byte_offset("tel:"sv.length()));
return url_text;
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <LibURL/URL.h>
namespace WebView {
bool is_public_suffix(StringView host);
Optional<String> get_public_suffix(StringView host);
enum class AppendTLD {
No,
Yes,
};
Optional<URL::URL> sanitize_url(StringView, Optional<StringView> search_engine = {}, AppendTLD = AppendTLD::No);
Vector<URL::URL> sanitize_urls(ReadonlySpan<ByteString> raw_urls, URL::URL const& new_tab_page_url);
struct URLParts {
StringView scheme_and_subdomain;
StringView effective_tld_plus_one;
StringView remainder;
};
Optional<URLParts> break_url_into_parts(StringView url);
// These are both used for the "right-click -> copy FOO" interaction for links.
enum class URLType {
Email,
Telephone,
Other,
};
URLType url_type(URL::URL const&);
String url_text_to_copy(URL::URL const&);
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "UserAgent.h"
namespace WebView {
OrderedHashMap<StringView, StringView> const user_agents = {
{ "Chrome Linux Desktop"sv, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"sv },
{ "Chrome macOS Desktop"sv, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"sv },
{ "Firefox Linux Desktop"sv, "Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0"sv },
{ "Firefox macOS Desktop"sv, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0"sv },
{ "Safari macOS Desktop"sv, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15"sv },
{ "Chrome Android Mobile"sv, "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.114 Mobile Safari/537.36"sv },
{ "Firefox Android Mobile"sv, "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/116.0 Firefox/116.0"sv },
{ "Safari iOS Mobile"sv, "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"sv },
};
Optional<StringView> normalize_user_agent_name(StringView name)
{
for (auto const& user_agent : user_agents) {
if (user_agent.key.equals_ignoring_ascii_case(name))
return user_agent.key;
}
return {};
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2023, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/StringView.h>
namespace WebView {
extern OrderedHashMap<StringView, StringView> const user_agents;
Optional<StringView> normalize_user_agent_name(StringView);
}

View file

@ -0,0 +1,664 @@
/*
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Error.h>
#include <AK/String.h>
#include <LibCore/DateTime.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/Timer.h>
#include <LibGfx/ImageFormats/PNGWriter.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWebView/Application.h>
#include <LibWebView/ViewImplementation.h>
#ifdef AK_OS_MACOS
# include <LibCore/IOSurface.h>
# include <LibCore/MachPort.h>
#endif
namespace WebView {
ViewImplementation::ViewImplementation()
{
m_repeated_crash_timer = Core::Timer::create_single_shot(1000, [this] {
// Reset the "crashing a lot" counter after 1 second in case we just
// happen to be visiting crashy websites a lot.
this->m_crash_count = 0;
});
on_request_file = [this](auto const& path, auto request_id) {
auto file = Core::File::open(path, Core::File::OpenMode::Read);
if (file.is_error())
client().async_handle_file_return(page_id(), file.error().code(), {}, request_id);
else
client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id);
};
}
ViewImplementation::~ViewImplementation()
{
if (m_client_state.client)
m_client_state.client->unregister_view(m_client_state.page_index);
}
WebContentClient& ViewImplementation::client()
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
WebContentClient const& ViewImplementation::client() const
{
VERIFY(m_client_state.client);
return *m_client_state.client;
}
u64 ViewImplementation::page_id() const
{
VERIFY(m_client_state.client);
return m_client_state.page_index;
}
void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size)
{
if (m_client_state.back_bitmap.id == bitmap_id) {
m_client_state.has_usable_bitmap = true;
m_client_state.back_bitmap.last_painted_size = size.to_type<Web::DevicePixels>();
swap(m_client_state.back_bitmap, m_client_state.front_bitmap);
m_backup_bitmap = nullptr;
if (on_ready_to_paint)
on_ready_to_paint();
}
client().async_ready_to_paint(page_id());
}
void ViewImplementation::set_window_position(Gfx::IntPoint position)
{
client().async_set_window_position(m_client_state.page_index, position.to_type<Web::DevicePixels>());
}
void ViewImplementation::set_window_size(Gfx::IntSize size)
{
client().async_set_window_size(m_client_state.page_index, size.to_type<Web::DevicePixels>());
}
void ViewImplementation::did_update_window_rect()
{
client().async_did_update_window_rect(m_client_state.page_index);
}
void ViewImplementation::load(URL::URL const& url)
{
m_url = url;
client().async_load_url(page_id(), url);
}
void ViewImplementation::load_html(StringView html)
{
client().async_load_html(page_id(), html);
}
void ViewImplementation::load_empty_document()
{
load_html(""sv);
}
void ViewImplementation::reload()
{
client().async_reload(page_id());
}
void ViewImplementation::traverse_the_history_by_delta(int delta)
{
client().async_traverse_the_history_by_delta(page_id(), delta);
}
void ViewImplementation::zoom_in()
{
if (m_zoom_level >= ZOOM_MAX_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level + ZOOM_STEP) * 100) / 100.0f;
update_zoom();
}
void ViewImplementation::zoom_out()
{
if (m_zoom_level <= ZOOM_MIN_LEVEL)
return;
m_zoom_level = round_to<int>((m_zoom_level - ZOOM_STEP) * 100) / 100.0f;
update_zoom();
}
void ViewImplementation::reset_zoom()
{
m_zoom_level = 1.0f;
update_zoom();
}
void ViewImplementation::enqueue_input_event(Web::InputEvent event)
{
// Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS
// prevented the default event behavior, at which point we either discard or handle that event, and then try to
// process the next one.
m_pending_input_events.enqueue(move(event));
m_pending_input_events.tail().visit(
[this](Web::KeyEvent const& event) {
client().async_key_event(m_client_state.page_index, event.clone_without_chrome_data());
},
[this](Web::MouseEvent const& event) {
client().async_mouse_event(m_client_state.page_index, event.clone_without_chrome_data());
},
[this](Web::DragEvent& event) {
auto cloned_event = event.clone_without_chrome_data();
cloned_event.files = move(event.files);
client().async_drag_event(m_client_state.page_index, move(cloned_event));
});
}
void ViewImplementation::did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result)
{
auto event = m_pending_input_events.dequeue();
if (event_result == Web::EventResult::Handled)
return;
// Here we handle events that were not consumed or cancelled by the WebContent. Propagate the event back
// to the concrete view implementation.
event.visit(
[this](Web::KeyEvent const& event) {
if (on_finish_handling_key_event)
on_finish_handling_key_event(event);
},
[this](Web::DragEvent const& event) {
if (on_finish_handling_drag_event)
on_finish_handling_drag_event(event);
},
[](auto const&) {});
}
void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
{
client().async_set_preferred_color_scheme(page_id(), color_scheme);
}
void ViewImplementation::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
{
client().async_set_preferred_contrast(page_id(), contrast);
}
void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
{
client().async_set_preferred_motion(page_id(), motion);
}
void ViewImplementation::set_preferred_languages(Vector<String> preferred_languages)
{
client().async_set_preferred_languages(page_id(), move(preferred_languages));
}
void ViewImplementation::set_enable_do_not_track(bool enable)
{
client().async_set_enable_do_not_track(page_id(), enable);
}
void ViewImplementation::set_enable_autoplay(bool enable)
{
if (enable) {
client().async_set_autoplay_allowed_on_all_websites(page_id());
} else {
client().async_set_autoplay_allowlist(page_id(), {});
}
}
ByteString ViewImplementation::selected_text()
{
return client().get_selected_text(page_id());
}
Optional<String> ViewImplementation::selected_text_with_whitespace_collapsed()
{
auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text()));
if (selected_text.is_empty())
return OptionalNone {};
return selected_text;
}
void ViewImplementation::select_all()
{
client().async_select_all(page_id());
}
void ViewImplementation::paste(String const& text)
{
client().async_paste(page_id(), text);
}
void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity)
{
client().async_find_in_page(page_id(), query, case_sensitivity);
}
void ViewImplementation::find_in_page_next_match()
{
client().async_find_in_page_next_match(page_id());
}
void ViewImplementation::find_in_page_previous_match()
{
client().async_find_in_page_previous_match(page_id());
}
void ViewImplementation::get_source()
{
client().async_get_source(page_id());
}
void ViewImplementation::inspect_dom_tree()
{
client().async_inspect_dom_tree(page_id());
}
void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::Selector::PseudoElement::Type> pseudo_element)
{
client().async_inspect_dom_node(page_id(), node_id, move(pseudo_element));
}
void ViewImplementation::inspect_accessibility_tree()
{
client().async_inspect_accessibility_tree(page_id());
}
void ViewImplementation::clear_inspected_dom_node()
{
inspect_dom_node(0, {});
}
void ViewImplementation::get_hovered_node_id()
{
client().async_get_hovered_node_id(page_id());
}
void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String text)
{
client().async_set_dom_node_text(page_id(), node_id, move(text));
}
void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String name)
{
client().async_set_dom_node_tag(page_id(), node_id, move(name));
}
void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, Vector<Attribute> attributes)
{
client().async_add_dom_node_attributes(page_id(), node_id, move(attributes));
}
void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String name, Vector<Attribute> replacement_attributes)
{
client().async_replace_dom_node_attribute(page_id(), node_id, move(name), move(replacement_attributes));
}
void ViewImplementation::create_child_element(Web::UniqueNodeID node_id)
{
client().async_create_child_element(page_id(), node_id);
}
void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id)
{
client().async_create_child_text_node(page_id(), node_id);
}
void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id)
{
client().async_clone_dom_node(page_id(), node_id);
}
void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id)
{
client().async_remove_dom_node(page_id(), node_id);
}
void ViewImplementation::get_dom_node_html(Web::UniqueNodeID node_id)
{
client().async_get_dom_node_html(page_id(), node_id);
}
void ViewImplementation::list_style_sheets()
{
client().async_list_style_sheets(page_id());
}
void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier)
{
client().async_request_style_sheet_source(page_id(), identifier);
}
void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument)
{
client().async_debug_request(page_id(), request, argument);
}
void ViewImplementation::run_javascript(StringView js_source)
{
client().async_run_javascript(page_id(), js_source);
}
void ViewImplementation::js_console_input(ByteString const& js_source)
{
client().async_js_console_input(page_id(), js_source);
}
void ViewImplementation::js_console_request_messages(i32 start_index)
{
client().async_js_console_request_messages(page_id(), start_index);
}
void ViewImplementation::alert_closed()
{
client().async_alert_closed(page_id());
}
void ViewImplementation::confirm_closed(bool accepted)
{
client().async_confirm_closed(page_id(), accepted);
}
void ViewImplementation::prompt_closed(Optional<String> response)
{
client().async_prompt_closed(page_id(), move(response));
}
void ViewImplementation::color_picker_update(Optional<Color> picked_color, Web::HTML::ColorPickerUpdateState state)
{
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<u32> const& selected_item_id)
{
client().async_select_dropdown_closed(page_id(), selected_item_id);
}
void ViewImplementation::toggle_media_play_state()
{
client().async_toggle_media_play_state(page_id());
}
void ViewImplementation::toggle_media_mute_state()
{
client().async_toggle_media_mute_state(page_id());
}
void ViewImplementation::toggle_media_loop_state()
{
client().async_toggle_media_loop_state(page_id());
}
void ViewImplementation::toggle_media_controls_state()
{
client().async_toggle_media_controls_state(page_id());
}
void ViewImplementation::toggle_page_mute_state()
{
m_mute_state = Web::HTML::invert_mute_state(m_mute_state);
client().async_toggle_page_mute_state(page_id());
}
void ViewImplementation::did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState play_state)
{
bool state_changed = false;
switch (play_state) {
case Web::HTML::AudioPlayState::Paused:
if (--m_number_of_elements_playing_audio == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
case Web::HTML::AudioPlayState::Playing:
if (m_number_of_elements_playing_audio++ == 0) {
m_audio_play_state = play_state;
state_changed = true;
}
break;
}
if (state_changed && on_audio_play_state_changed)
on_audio_play_state_changed(m_audio_play_state);
}
void ViewImplementation::did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled) const
{
if (on_navigation_buttons_state_changed)
on_navigation_buttons_state_changed(back_enabled, forward_enabled);
}
void ViewImplementation::did_allocate_backing_stores(Badge<WebContentClient>, i32 front_bitmap_id, Gfx::ShareableBitmap const& front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap const& back_bitmap)
{
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_bitmap = m_client_state.front_bitmap.bitmap;
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
m_client_state.has_usable_bitmap = false;
m_client_state.front_bitmap.bitmap = front_bitmap.bitmap();
m_client_state.front_bitmap.id = front_bitmap_id;
m_client_state.back_bitmap.bitmap = back_bitmap.bitmap();
m_client_state.back_bitmap.id = back_bitmap_id;
}
#ifdef AK_OS_MACOS
void ViewImplementation::did_allocate_iosurface_backing_stores(i32 front_id, Core::MachPort&& front_port, i32 back_id, Core::MachPort&& back_port)
{
if (m_client_state.has_usable_bitmap) {
// NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one.
m_backup_bitmap = m_client_state.front_bitmap.bitmap;
m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size;
}
m_client_state.has_usable_bitmap = false;
auto front_iosurface = Core::IOSurfaceHandle::from_mach_port(move(front_port));
auto back_iosurface = Core::IOSurfaceHandle::from_mach_port(move(back_port));
auto front_size = Gfx::IntSize { front_iosurface.width(), front_iosurface.height() };
auto back_size = Gfx::IntSize { back_iosurface.width(), back_iosurface.height() };
auto bytes_per_row = front_iosurface.bytes_per_row();
auto front_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, front_size, bytes_per_row, front_iosurface.data(), [handle = move(front_iosurface)] {});
auto back_bitmap = Gfx::Bitmap::create_wrapper(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, back_size, bytes_per_row, back_iosurface.data(), [handle = move(back_iosurface)] {});
m_client_state.front_bitmap.bitmap = front_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.front_bitmap.id = front_id;
m_client_state.back_bitmap.bitmap = back_bitmap.release_value_but_fixme_should_propagate_errors();
m_client_state.back_bitmap.id = back_id;
}
#endif
void ViewImplementation::handle_resize()
{
client().async_set_viewport_size(page_id(), this->viewport_size());
}
void ViewImplementation::handle_web_content_process_crash()
{
dbgln("WebContent process crashed!");
dbgln("Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose");
++m_crash_count;
constexpr size_t max_reasonable_crash_count = 5U;
if (m_crash_count >= max_reasonable_crash_count) {
dbgln("WebContent has crashed {} times in quick succession! Not restarting...", m_crash_count);
m_repeated_crash_timer->stop();
return;
}
m_repeated_crash_timer->restart();
initialize_client();
VERIFY(m_client_state.client);
// Don't keep a stale backup bitmap around.
m_backup_bitmap = nullptr;
handle_resize();
StringBuilder builder;
builder.append("<html><head><title>Crashed: "sv);
builder.append(escape_html_entities(m_url.to_byte_string()));
builder.append("</title></head><body>"sv);
builder.append("<h1>Web page crashed"sv);
if (!m_url.host().has<Empty>()) {
builder.appendff(" on {}", escape_html_entities(m_url.serialized_host().release_value_but_fixme_should_propagate_errors()));
}
builder.append("</h1>"sv);
auto escaped_url = escape_html_entities(m_url.to_byte_string());
builder.appendff("The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.", escaped_url, escaped_url);
builder.append("</body></html>"sv);
load_html(builder.to_byte_string());
}
static ErrorOr<LexicalPath> save_screenshot(Gfx::ShareableBitmap const& bitmap)
{
if (!bitmap.is_valid())
return Error::from_string_literal("Failed to take a screenshot");
auto file = Core::DateTime::now().to_byte_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv);
auto path = TRY(Application::the().path_for_downloaded_file(file));
auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap.bitmap()));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(encoded));
return path;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_screenshot(ScreenshotType type)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
// For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
Gfx::ShareableBitmap bitmap;
switch (type) {
case ScreenshotType::Visible:
if (auto* visible_bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr()) {
if (auto result = save_screenshot(visible_bitmap->to_shareable_bitmap()); result.is_error())
promise->reject(result.release_error());
else
promise->resolve(result.release_value());
}
break;
case ScreenshotType::Full:
m_pending_screenshot = promise;
client().async_take_document_screenshot(page_id());
break;
}
return promise;
}
NonnullRefPtr<Core::Promise<LexicalPath>> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id)
{
auto promise = Core::Promise<LexicalPath>::construct();
if (m_pending_screenshot) {
// For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need
// to allow spamming screenshot requests for some reason.
promise->reject(Error::from_string_literal("A screenshot request is already in progress"));
return promise;
}
m_pending_screenshot = promise;
client().async_take_dom_node_screenshot(page_id(), node_id);
return promise;
}
void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const& screenshot)
{
VERIFY(m_pending_screenshot);
if (auto result = save_screenshot(screenshot); result.is_error())
m_pending_screenshot->reject(result.release_error());
else
m_pending_screenshot->resolve(result.release_value());
m_pending_screenshot = nullptr;
}
NonnullRefPtr<Core::Promise<String>> ViewImplementation::request_internal_page_info(PageInfoType type)
{
auto promise = Core::Promise<String>::construct();
if (m_pending_info_request) {
// For simplicitly, only allow one info request at a time for now.
promise->reject(Error::from_string_literal("A page info request is already in progress"));
return promise;
}
m_pending_info_request = promise;
client().async_request_internal_page_info(page_id(), type);
return promise;
}
void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, String const& info)
{
VERIFY(m_pending_info_request);
m_pending_info_request->resolve(String { info });
m_pending_info_request = nullptr;
}
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
{
auto promise = request_internal_page_info(PageInfoType::GCGraph);
auto gc_graph_json = TRY(promise->await());
LexicalPath path { Core::StandardPaths::tempfile_directory() };
path = path.append(TRY(Core::DateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.json"sv)));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
return path;
}
void ViewImplementation::set_user_style_sheet(String source)
{
client().async_set_user_style(page_id(), move(source));
}
void ViewImplementation::use_native_user_style_sheet()
{
extern String native_stylesheet_source;
set_user_style_sheet(native_stylesheet_source);
}
void ViewImplementation::enable_inspector_prototype()
{
client().async_enable_inspector_prototype(page_id());
}
}

View file

@ -0,0 +1,298 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/Function.h>
#include <AK/LexicalPath.h>
#include <AK/Queue.h>
#include <AK/String.h>
#include <LibCore/Forward.h>
#include <LibCore/Promise.h>
#include <LibGfx/Forward.h>
#include <LibGfx/StandardCursor.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/Page/EventResult.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWebView/Forward.h>
#include <LibWebView/PageInfo.h>
#include <LibWebView/WebContentClient.h>
namespace WebView {
class ViewImplementation {
public:
virtual ~ViewImplementation();
struct DOMNodeProperties {
String computed_style_json;
String resolved_style_json;
String custom_properties_json;
String node_box_sizing_json;
String aria_properties_state_json;
String fonts_json;
};
void set_url(Badge<WebContentClient>, URL::URL url) { m_url = move(url); }
URL::URL const& url() const { return m_url; }
String const& handle() const { return m_client_state.client_handle; }
void server_did_paint(Badge<WebContentClient>, i32 bitmap_id, Gfx::IntSize size);
void set_window_position(Gfx::IntPoint);
void set_window_size(Gfx::IntSize);
void did_update_window_rect();
void load(URL::URL const&);
void load_html(StringView);
void load_empty_document();
void reload();
void traverse_the_history_by_delta(int delta);
void zoom_in();
void zoom_out();
void reset_zoom();
float zoom_level() const { return m_zoom_level; }
float device_pixel_ratio() const { return m_device_pixel_ratio; }
void enqueue_input_event(Web::InputEvent);
void did_finish_handling_input_event(Badge<WebContentClient>, Web::EventResult event_result);
void set_preferred_color_scheme(Web::CSS::PreferredColorScheme);
void set_preferred_contrast(Web::CSS::PreferredContrast);
void set_preferred_motion(Web::CSS::PreferredMotion);
void set_preferred_languages(Vector<String>);
void set_enable_do_not_track(bool);
void set_enable_autoplay(bool);
ByteString selected_text();
Optional<String> selected_text_with_whitespace_collapsed();
void select_all();
void find_in_page(String const& query, CaseSensitivity = CaseSensitivity::CaseInsensitive);
void find_in_page_next_match();
void find_in_page_previous_match();
void paste(String const&);
void get_source();
void inspect_dom_tree();
void inspect_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::Selector::PseudoElement::Type> pseudo_element);
void inspect_accessibility_tree();
void clear_inspected_dom_node();
void get_hovered_node_id();
void set_dom_node_text(Web::UniqueNodeID node_id, String text);
void set_dom_node_tag(Web::UniqueNodeID node_id, String name);
void add_dom_node_attributes(Web::UniqueNodeID node_id, Vector<Attribute> attributes);
void replace_dom_node_attribute(Web::UniqueNodeID node_id, String name, Vector<Attribute> replacement_attributes);
void create_child_element(Web::UniqueNodeID node_id);
void create_child_text_node(Web::UniqueNodeID node_id);
void clone_dom_node(Web::UniqueNodeID node_id);
void remove_dom_node(Web::UniqueNodeID node_id);
void get_dom_node_html(Web::UniqueNodeID node_id);
void list_style_sheets();
void request_style_sheet_source(Web::CSS::StyleSheetIdentifier const&);
void debug_request(ByteString const& request, ByteString const& argument = {});
void run_javascript(StringView);
void js_console_input(ByteString const& js_source);
void js_console_request_messages(i32 start_index);
void alert_closed();
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<u32> const& selected_item_id);
void toggle_media_play_state();
void toggle_media_mute_state();
void toggle_media_loop_state();
void toggle_media_controls_state();
Web::HTML::MuteState page_mute_state() const { return m_mute_state; }
void toggle_page_mute_state();
void did_change_audio_play_state(Badge<WebContentClient>, Web::HTML::AudioPlayState);
Web::HTML::AudioPlayState audio_play_state() const { return m_audio_play_state; }
void did_update_navigation_buttons_state(Badge<WebContentClient>, bool back_enabled, bool forward_enabled) const;
void did_allocate_backing_stores(Badge<WebContentClient>, i32 front_bitmap_id, Gfx::ShareableBitmap const&, i32 back_bitmap_id, Gfx::ShareableBitmap const&);
#ifdef AK_OS_MACOS
void did_allocate_iosurface_backing_stores(i32 front_bitmap_id, Core::MachPort&&, i32 back_bitmap_id, Core::MachPort&&);
#endif
enum class ScreenshotType {
Visible,
Full,
};
NonnullRefPtr<Core::Promise<LexicalPath>> take_screenshot(ScreenshotType);
NonnullRefPtr<Core::Promise<LexicalPath>> take_dom_node_screenshot(Web::UniqueNodeID);
virtual void did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const&);
NonnullRefPtr<Core::Promise<String>> request_internal_page_info(PageInfoType);
void did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, String const&);
ErrorOr<LexicalPath> dump_gc_graph();
void set_user_style_sheet(String source);
// Load Native.css as the User style sheet, which attempts to make WebView content look as close to
// native GUI widgets as possible.
void use_native_user_style_sheet();
void enable_inspector_prototype();
Function<void()> on_ready_to_paint;
Function<String(Web::HTML::ActivateTab, Web::HTML::WebViewHints, Optional<u64>)> on_new_web_view;
Function<void()> on_activate_tab;
Function<void()> on_close;
Function<void(Gfx::IntPoint screen_position)> on_context_menu_request;
Function<void(URL::URL const&, Gfx::IntPoint screen_position)> on_link_context_menu_request;
Function<void(URL::URL const&, Gfx::IntPoint screen_position, Gfx::ShareableBitmap const&)> on_image_context_menu_request;
Function<void(Gfx::IntPoint screen_position, Web::Page::MediaContextMenu const&)> on_media_context_menu_request;
Function<void(URL::URL const&)> on_link_hover;
Function<void()> on_link_unhover;
Function<void(URL::URL const&, ByteString const& target, unsigned modifiers)> on_link_click;
Function<void(URL::URL const&, ByteString const& target, unsigned modifiers)> on_link_middle_click;
Function<void(ByteString const&)> on_title_change;
Function<void(URL::URL const&)> on_url_change;
Function<void(URL::URL const&, bool)> on_load_start;
Function<void(URL::URL const&)> on_load_finish;
Function<void(ByteString const& path, i32)> on_request_file;
Function<void(Gfx::Bitmap const&)> on_favicon_change;
Function<void(Gfx::StandardCursor)> on_cursor_change;
Function<void(Gfx::IntPoint, ByteString const&)> on_request_tooltip_override;
Function<void()> on_stop_tooltip_override;
Function<void(ByteString const&)> on_enter_tooltip_area;
Function<void()> on_leave_tooltip_area;
Function<void(String const& message)> on_request_alert;
Function<void(String const& message)> on_request_confirm;
Function<void(String const& message, String const& default_)> on_request_prompt;
Function<void(String const& message)> on_request_set_prompt_text;
Function<void()> on_request_accept_dialog;
Function<void()> on_request_dismiss_dialog;
Function<void(URL::URL const&, URL::URL const&, String const&)> on_received_source;
Function<void(ByteString const&)> on_received_dom_tree;
Function<void(Optional<DOMNodeProperties>)> on_received_dom_node_properties;
Function<void(ByteString const&)> on_received_accessibility_tree;
Function<void(Vector<Web::CSS::StyleSheetIdentifier>)> on_received_style_sheet_list;
Function<void(Web::CSS::StyleSheetIdentifier const&)> on_inspector_requested_style_sheet_source;
Function<void(Web::CSS::StyleSheetIdentifier const&, URL::URL const&, String const&)> on_received_style_sheet_source;
Function<void(Web::UniqueNodeID)> on_received_hovered_node_id;
Function<void(Optional<Web::UniqueNodeID> const& node_id)> on_finshed_editing_dom_node;
Function<void(String const&)> on_received_dom_node_html;
Function<void(i32 message_id)> on_received_console_message;
Function<void(i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages)> on_received_console_messages;
Function<void(i32 count_waiting)> on_resource_status_change;
Function<void()> on_restore_window;
Function<void(Gfx::IntPoint)> on_reposition_window;
Function<void(Gfx::IntSize)> on_resize_window;
Function<void()> on_maximize_window;
Function<void()> on_minimize_window;
Function<void()> on_fullscreen_window;
Function<void(Color current_color)> on_request_color_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(Web::DragEvent const&)> on_finish_handling_drag_event;
Function<void(String const&)> on_text_test_finish;
Function<void(size_t current_match_index, Optional<size_t> const& total_match_count)> on_find_in_page;
Function<void(Gfx::Color)> on_theme_color_change;
Function<void(String const&, String const&, String const&)> on_insert_clipboard_entry;
Function<void(Web::HTML::AudioPlayState)> on_audio_play_state_changed;
Function<void(bool, bool)> on_navigation_buttons_state_changed;
Function<void()> on_inspector_loaded;
Function<void(Web::UniqueNodeID, Optional<Web::CSS::Selector::PseudoElement::Type> const&)> on_inspector_selected_dom_node;
Function<void(Web::UniqueNodeID, String const&)> on_inspector_set_dom_node_text;
Function<void(Web::UniqueNodeID, String const&)> on_inspector_set_dom_node_tag;
Function<void(Web::UniqueNodeID, Vector<Attribute> const&)> on_inspector_added_dom_node_attributes;
Function<void(Web::UniqueNodeID, size_t, Vector<Attribute> const&)> on_inspector_replaced_dom_node_attribute;
Function<void(Web::UniqueNodeID, Gfx::IntPoint, String const&, Optional<String> const&, Optional<size_t> const&)> on_inspector_requested_dom_tree_context_menu;
Function<void(size_t, Gfx::IntPoint)> on_inspector_requested_cookie_context_menu;
Function<void(String const&)> on_inspector_executed_console_script;
Function<void(String const&)> on_inspector_exported_inspector_html;
Function<IPC::File()> on_request_worker_agent;
virtual Web::DevicePixelSize viewport_size() const = 0;
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const = 0;
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const = 0;
protected:
static constexpr auto ZOOM_MIN_LEVEL = 0.3f;
static constexpr auto ZOOM_MAX_LEVEL = 5.0f;
static constexpr auto ZOOM_STEP = 0.1f;
ViewImplementation();
WebContentClient& client();
WebContentClient const& client() const;
u64 page_id() const;
virtual void update_zoom() = 0;
void handle_resize();
enum class CreateNewClient {
No,
Yes,
};
virtual void initialize_client(CreateNewClient = CreateNewClient::Yes) { }
void handle_web_content_process_crash();
struct SharedBitmap {
i32 id { -1 };
Web::DevicePixelSize last_painted_size;
RefPtr<Gfx::Bitmap> bitmap;
};
struct ClientState {
RefPtr<WebContentClient> client;
String client_handle;
SharedBitmap front_bitmap;
SharedBitmap back_bitmap;
u64 page_index { 0 };
bool has_usable_bitmap { false };
} m_client_state;
URL::URL m_url;
float m_zoom_level { 1.0 };
float m_device_pixel_ratio { 1.0 };
Queue<Web::InputEvent> m_pending_input_events;
RefPtr<Core::Timer> m_backing_store_shrink_timer;
RefPtr<Gfx::Bitmap> m_backup_bitmap;
Web::DevicePixelSize m_backup_bitmap_size;
size_t m_crash_count = 0;
RefPtr<Core::Timer> m_repeated_crash_timer;
RefPtr<Core::Promise<LexicalPath>> m_pending_screenshot;
RefPtr<Core::Promise<String>> m_pending_info_request;
Web::HTML::AudioPlayState m_audio_play_state { Web::HTML::AudioPlayState::Paused };
size_t m_number_of_elements_playing_audio { 0 };
Web::HTML::MuteState m_mute_state { Web::HTML::MuteState::Unmuted };
};
}

View file

@ -0,0 +1,710 @@
/*
* Copyright (c) 2020-2021, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "WebContentClient.h"
#include "Application.h"
#include "ViewImplementation.h"
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWebView/CookieJar.h>
namespace WebView {
HashTable<WebContentClient*> WebContentClient::s_clients;
Optional<ViewImplementation&> WebContentClient::view_for_pid_and_page_id(pid_t pid, u64 page_id)
{
for (auto* client : s_clients) {
if (client->m_process_handle.pid == pid)
return client->view_for_page_id(page_id);
}
return {};
}
WebContentClient::WebContentClient(IPC::Transport transport, ViewImplementation& view)
: IPC::ConnectionToServer<WebContentClientEndpoint, WebContentServerEndpoint>(*this, move(transport))
{
s_clients.set(this);
m_views.set(0, &view);
}
WebContentClient::~WebContentClient()
{
s_clients.remove(this);
}
void WebContentClient::die()
{
// Intentionally empty. Restart is handled at another level.
}
void WebContentClient::register_view(u64 page_id, ViewImplementation& view)
{
VERIFY(page_id > 0);
m_views.set(page_id, &view);
}
void WebContentClient::unregister_view(u64 page_id)
{
m_views.remove(page_id);
if (m_views.is_empty()) {
on_web_content_process_crash = nullptr;
async_close_server();
}
}
void WebContentClient::did_paint(u64 page_id, Gfx::IntRect const& rect, i32 bitmap_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->server_did_paint({}, bitmap_id, rect.size());
}
void WebContentClient::did_start_loading(u64 page_id, URL::URL const& url, bool is_redirect)
{
if (auto process = WebView::Application::the().find_process(m_process_handle.pid); process.has_value())
process->set_title(OptionalNone {});
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->set_url({}, url);
if (view->on_load_start)
view->on_load_start(url, is_redirect);
}
}
void WebContentClient::did_finish_loading(u64 page_id, URL::URL const& url)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->set_url({}, url);
if (view->on_load_finish)
view->on_load_finish(url);
}
}
void WebContentClient::did_finish_text_test(u64 page_id, String const& text)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_text_test_finish)
view->on_text_test_finish(text);
}
}
void WebContentClient::did_find_in_page(u64 page_id, size_t current_match_index, Optional<size_t> const& total_match_count)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_find_in_page)
view->on_find_in_page(current_match_index, total_match_count);
}
}
void WebContentClient::did_request_refresh(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->reload();
}
void WebContentClient::did_request_cursor_change(u64 page_id, i32 cursor_type)
{
if (cursor_type < 0 || cursor_type >= (i32)Gfx::StandardCursor::__Count) {
dbgln("DidRequestCursorChange: Bad cursor type");
return;
}
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_cursor_change)
view->on_cursor_change(static_cast<Gfx::StandardCursor>(cursor_type));
}
}
void WebContentClient::did_change_title(u64 page_id, ByteString const& title)
{
if (auto process = WebView::Application::the().find_process(m_process_handle.pid); process.has_value())
process->set_title(MUST(String::from_byte_string(title)));
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (!view->on_title_change)
return;
if (title.is_empty())
view->on_title_change(view->url().to_byte_string());
else
view->on_title_change(title);
}
}
void WebContentClient::did_change_url(u64 page_id, URL::URL const& url)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->set_url({}, url);
if (view->on_url_change)
view->on_url_change(url);
}
}
void WebContentClient::did_request_tooltip_override(u64 page_id, Gfx::IntPoint position, ByteString const& title)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_tooltip_override)
view->on_request_tooltip_override(view->to_widget_position(position), title);
}
}
void WebContentClient::did_stop_tooltip_override(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_stop_tooltip_override)
view->on_stop_tooltip_override();
}
}
void WebContentClient::did_enter_tooltip_area(u64 page_id, ByteString const& title)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_enter_tooltip_area)
view->on_enter_tooltip_area(title);
}
}
void WebContentClient::did_leave_tooltip_area(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_leave_tooltip_area)
view->on_leave_tooltip_area();
}
}
void WebContentClient::did_hover_link(u64 page_id, URL::URL const& url)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_hover)
view->on_link_hover(url);
}
}
void WebContentClient::did_unhover_link(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_unhover)
view->on_link_unhover();
}
}
void WebContentClient::did_click_link(u64 page_id, URL::URL const& url, ByteString const& target, unsigned modifiers)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_click)
view->on_link_click(url, target, modifiers);
}
}
void WebContentClient::did_middle_click_link(u64 page_id, URL::URL const& url, ByteString const& target, unsigned modifiers)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_middle_click)
view->on_link_middle_click(url, target, modifiers);
}
}
void WebContentClient::did_request_context_menu(u64 page_id, Gfx::IntPoint content_position)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_context_menu_request)
view->on_context_menu_request(view->to_widget_position(content_position));
}
}
void WebContentClient::did_request_link_context_menu(u64 page_id, Gfx::IntPoint content_position, URL::URL const& url, ByteString const&, unsigned)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_link_context_menu_request)
view->on_link_context_menu_request(url, view->to_widget_position(content_position));
}
}
void WebContentClient::did_request_image_context_menu(u64 page_id, Gfx::IntPoint content_position, URL::URL const& url, ByteString const&, unsigned, Gfx::ShareableBitmap const& bitmap)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_image_context_menu_request)
view->on_image_context_menu_request(url, view->to_widget_position(content_position), bitmap);
}
}
void WebContentClient::did_request_media_context_menu(u64 page_id, Gfx::IntPoint content_position, ByteString const&, unsigned, Web::Page::MediaContextMenu const& menu)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_media_context_menu_request)
view->on_media_context_menu_request(view->to_widget_position(content_position), menu);
}
}
void WebContentClient::did_get_source(u64 page_id, URL::URL const& url, URL::URL const& base_url, String const& source)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_source)
view->on_received_source(url, base_url, source);
}
}
void WebContentClient::did_inspect_dom_tree(u64 page_id, ByteString const& dom_tree)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_dom_tree)
view->on_received_dom_tree(dom_tree);
}
}
void WebContentClient::did_inspect_dom_node(u64 page_id, bool has_style, ByteString const& computed_style, ByteString const& resolved_style, ByteString const& custom_properties, ByteString const& node_box_sizing, ByteString const& aria_properties_state, ByteString const& fonts)
{
auto view = view_for_page_id(page_id);
if (!view.has_value() || !view->on_received_dom_node_properties)
return;
Optional<ViewImplementation::DOMNodeProperties> properties;
if (has_style) {
properties = ViewImplementation::DOMNodeProperties {
.computed_style_json = MUST(String::from_byte_string(computed_style)),
.resolved_style_json = MUST(String::from_byte_string(resolved_style)),
.custom_properties_json = MUST(String::from_byte_string(custom_properties)),
.node_box_sizing_json = MUST(String::from_byte_string(node_box_sizing)),
.aria_properties_state_json = MUST(String::from_byte_string(aria_properties_state)),
.fonts_json = MUST(String::from_byte_string(fonts))
};
}
view->on_received_dom_node_properties(move(properties));
}
void WebContentClient::did_inspect_accessibility_tree(u64 page_id, ByteString const& accessibility_tree)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_accessibility_tree)
view->on_received_accessibility_tree(accessibility_tree);
}
}
void WebContentClient::did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID const& node_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_hovered_node_id)
view->on_received_hovered_node_id(node_id);
}
}
void WebContentClient::did_finish_editing_dom_node(u64 page_id, Optional<Web::UniqueNodeID> const& node_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_finshed_editing_dom_node)
view->on_finshed_editing_dom_node(node_id);
}
}
void WebContentClient::did_get_dom_node_html(u64 page_id, String const& html)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_dom_node_html)
view->on_received_dom_node_html(html);
}
}
void WebContentClient::did_take_screenshot(u64 page_id, Gfx::ShareableBitmap const& screenshot)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_receive_screenshot({}, screenshot);
}
void WebContentClient::did_get_internal_page_info(u64 page_id, WebView::PageInfoType type, String const& info)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_receive_internal_page_info({}, type, info);
}
void WebContentClient::did_output_js_console_message(u64 page_id, i32 message_index)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_console_message)
view->on_received_console_message(message_index);
}
}
void WebContentClient::did_get_js_console_messages(u64 page_id, i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_console_messages)
view->on_received_console_messages(start_index, message_types, messages);
}
}
void WebContentClient::did_request_alert(u64 page_id, String const& message)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_alert)
view->on_request_alert(message);
}
}
void WebContentClient::did_request_confirm(u64 page_id, String const& message)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_confirm)
view->on_request_confirm(message);
}
}
void WebContentClient::did_request_prompt(u64 page_id, String const& message, String const& default_)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_prompt)
view->on_request_prompt(message, default_);
}
}
void WebContentClient::did_request_set_prompt_text(u64 page_id, String const& message)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_set_prompt_text)
view->on_request_set_prompt_text(message);
}
}
void WebContentClient::did_request_accept_dialog(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_accept_dialog)
view->on_request_accept_dialog();
}
}
void WebContentClient::did_request_dismiss_dialog(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_dismiss_dialog)
view->on_request_dismiss_dialog();
}
}
void WebContentClient::did_change_favicon(u64 page_id, Gfx::ShareableBitmap const& favicon)
{
if (!favicon.is_valid()) {
dbgln("DidChangeFavicon: Received invalid favicon");
return;
}
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_favicon_change)
view->on_favicon_change(*favicon.bitmap());
}
}
Messages::WebContentClient::DidRequestAllCookiesResponse WebContentClient::did_request_all_cookies(URL::URL const& url)
{
return Application::cookie_jar().get_all_cookies(url);
}
Messages::WebContentClient::DidRequestNamedCookieResponse WebContentClient::did_request_named_cookie(URL::URL const& url, String const& name)
{
return Application::cookie_jar().get_named_cookie(url, name);
}
Messages::WebContentClient::DidRequestCookieResponse WebContentClient::did_request_cookie(URL::URL const& url, Web::Cookie::Source source)
{
return Application::cookie_jar().get_cookie(url, source);
}
void WebContentClient::did_set_cookie(URL::URL const& url, Web::Cookie::ParsedCookie const& cookie, Web::Cookie::Source source)
{
Application::cookie_jar().set_cookie(url, cookie, source);
}
void WebContentClient::did_update_cookie(Web::Cookie::Cookie const& cookie)
{
Application::cookie_jar().update_cookie(cookie);
}
void WebContentClient::did_expire_cookies_with_time_offset(AK::Duration offset)
{
Application::cookie_jar().expire_cookies_with_time_offset(offset);
}
Messages::WebContentClient::DidRequestNewWebViewResponse WebContentClient::did_request_new_web_view(u64 page_id, Web::HTML::ActivateTab const& activate_tab, Web::HTML::WebViewHints const& hints, Optional<u64> const& page_index)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_new_web_view)
return view->on_new_web_view(activate_tab, hints, page_index);
}
return String {};
}
void WebContentClient::did_request_activate_tab(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_activate_tab)
view->on_activate_tab();
}
}
void WebContentClient::did_close_browsing_context(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_close)
view->on_close();
}
}
void WebContentClient::did_update_resource_count(u64 page_id, i32 count_waiting)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_resource_status_change)
view->on_resource_status_change(count_waiting);
}
}
void WebContentClient::did_request_restore_window(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_restore_window)
view->on_restore_window();
}
}
void WebContentClient::did_request_reposition_window(u64 page_id, Gfx::IntPoint position)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_reposition_window)
view->on_reposition_window(position);
}
}
void WebContentClient::did_request_resize_window(u64 page_id, Gfx::IntSize size)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_resize_window)
view->on_resize_window(size);
}
}
void WebContentClient::did_request_maximize_window(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_maximize_window)
view->on_maximize_window();
}
}
void WebContentClient::did_request_minimize_window(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_minimize_window)
view->on_minimize_window();
}
}
void WebContentClient::did_request_fullscreen_window(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_fullscreen_window)
view->on_fullscreen_window();
}
}
void WebContentClient::did_request_file(u64 page_id, ByteString const& path, i32 request_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_file)
view->on_request_file(path, request_id);
}
}
void WebContentClient::did_request_color_picker(u64 page_id, Color const& current_color)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_color_picker)
view->on_request_color_picker(current_color);
}
}
void WebContentClient::did_request_file_picker(u64 page_id, Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles allow_multiple_files)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_file_picker)
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)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_select_dropdown)
view->on_request_select_dropdown(content_position, minimum_width, items);
}
}
void WebContentClient::did_finish_handling_input_event(u64 page_id, Web::EventResult event_result)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_finish_handling_input_event({}, event_result);
}
void WebContentClient::did_change_theme_color(u64 page_id, Gfx::Color color)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_theme_color_change)
view->on_theme_color_change(color);
}
}
void WebContentClient::did_insert_clipboard_entry(u64 page_id, String const& data, String const& presentation_style, String const& mime_type)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_insert_clipboard_entry)
view->on_insert_clipboard_entry(data, presentation_style, mime_type);
}
}
void WebContentClient::did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState play_state)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_change_audio_play_state({}, play_state);
}
void WebContentClient::did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_update_navigation_buttons_state({}, back_enabled, forward_enabled);
}
void WebContentClient::did_allocate_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::ShareableBitmap const& front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap const& back_bitmap)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_allocate_backing_stores({}, front_bitmap_id, front_bitmap, back_bitmap_id, back_bitmap);
}
void WebContentClient::inspector_did_load(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_loaded)
view->on_inspector_loaded();
}
}
void WebContentClient::inspector_did_select_dom_node(u64 page_id, Web::UniqueNodeID const& node_id, Optional<Web::CSS::Selector::PseudoElement::Type> const& pseudo_element)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_selected_dom_node)
view->on_inspector_selected_dom_node(node_id, pseudo_element);
}
}
void WebContentClient::inspector_did_set_dom_node_text(u64 page_id, Web::UniqueNodeID const& node_id, String const& text)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_set_dom_node_text)
view->on_inspector_set_dom_node_text(node_id, text);
}
}
void WebContentClient::inspector_did_set_dom_node_tag(u64 page_id, Web::UniqueNodeID const& node_id, String const& tag)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_set_dom_node_tag)
view->on_inspector_set_dom_node_tag(node_id, tag);
}
}
void WebContentClient::inspector_did_add_dom_node_attributes(u64 page_id, Web::UniqueNodeID const& node_id, Vector<Attribute> const& attributes)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_added_dom_node_attributes)
view->on_inspector_added_dom_node_attributes(node_id, attributes);
}
}
void WebContentClient::inspector_did_replace_dom_node_attribute(u64 page_id, Web::UniqueNodeID const& node_id, size_t attribute_index, Vector<Attribute> const& replacement_attributes)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_replaced_dom_node_attribute)
view->on_inspector_replaced_dom_node_attribute(node_id, attribute_index, replacement_attributes);
}
}
void WebContentClient::inspector_did_request_dom_tree_context_menu(u64 page_id, Web::UniqueNodeID const& node_id, Gfx::IntPoint position, String const& type, Optional<String> const& tag, Optional<size_t> const& attribute_index)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_requested_dom_tree_context_menu)
view->on_inspector_requested_dom_tree_context_menu(node_id, view->to_widget_position(position), type, tag, attribute_index);
}
}
void WebContentClient::inspector_did_request_cookie_context_menu(u64 page_id, size_t cookie_index, Gfx::IntPoint position)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_requested_cookie_context_menu)
view->on_inspector_requested_cookie_context_menu(cookie_index, view->to_widget_position(position));
}
}
void WebContentClient::inspector_did_execute_console_script(u64 page_id, String const& script)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_executed_console_script)
view->on_inspector_executed_console_script(script);
}
}
void WebContentClient::inspector_did_export_inspector_html(u64 page_id, String const& html)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_exported_inspector_html)
view->on_inspector_exported_inspector_html(html);
}
}
Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request_worker_agent(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_request_worker_agent)
return view->on_request_worker_agent();
}
return IPC::File {};
}
void WebContentClient::inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> const& stylesheets)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_style_sheet_list)
view->on_received_style_sheet_list(stylesheets);
}
}
void WebContentClient::inspector_did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_requested_style_sheet_source)
view->on_inspector_requested_style_sheet_source(identifier);
}
}
void WebContentClient::did_get_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier, URL::URL const& base_url, String const& source)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_received_style_sheet_source)
view->on_received_style_sheet_source(identifier, base_url, source);
}
}
Optional<ViewImplementation&> WebContentClient::view_for_page_id(u64 page_id, SourceLocation location)
{
if (auto view = m_views.get(page_id); view.has_value())
return *view.value();
dbgln("WebContentClient::{}: Did not find a page with ID {}", location.function_name(), page_id);
return {};
}
}

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2020-2021, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/SourceLocation.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibIPC/Transport.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/SelectItem.h>
#include <LibWeb/HTML/WebViewHints.h>
#include <LibWeb/Page/EventResult.h>
#include <WebContent/WebContentClientEndpoint.h>
#include <WebContent/WebContentServerEndpoint.h>
namespace WebView {
class ViewImplementation;
class WebContentClient final
: public IPC::ConnectionToServer<WebContentClientEndpoint, WebContentServerEndpoint>
, public WebContentClientEndpoint {
IPC_CLIENT_CONNECTION(WebContentClient, "/tmp/session/%sid/portal/webcontent"sv);
public:
static Optional<ViewImplementation&> view_for_pid_and_page_id(pid_t pid, u64 page_id);
template<CallableAs<IterationDecision, WebContentClient&> Callback>
static void for_each_client(Callback callback);
static size_t client_count() { return s_clients.size(); }
WebContentClient(IPC::Transport, ViewImplementation&);
~WebContentClient();
void register_view(u64 page_id, ViewImplementation&);
void unregister_view(u64 page_id);
Function<void()> on_web_content_process_crash;
void set_pid(pid_t pid) { m_process_handle.pid = pid; }
private:
virtual void die() override;
virtual void did_paint(u64 page_id, Gfx::IntRect const&, i32) override;
virtual void did_finish_loading(u64 page_id, URL::URL const&) override;
virtual void did_request_refresh(u64 page_id) override;
virtual void did_request_cursor_change(u64 page_id, i32) override;
virtual void did_change_title(u64 page_id, ByteString const&) override;
virtual void did_change_url(u64 page_id, URL::URL const&) override;
virtual void did_request_tooltip_override(u64 page_id, Gfx::IntPoint, ByteString const&) override;
virtual void did_stop_tooltip_override(u64 page_id) override;
virtual void did_enter_tooltip_area(u64 page_id, ByteString const&) override;
virtual void did_leave_tooltip_area(u64 page_id) override;
virtual void did_hover_link(u64 page_id, URL::URL const&) override;
virtual void did_unhover_link(u64 page_id) override;
virtual void did_click_link(u64 page_id, URL::URL const&, ByteString const&, unsigned) override;
virtual void did_middle_click_link(u64 page_id, URL::URL const&, ByteString const&, unsigned) override;
virtual void did_start_loading(u64 page_id, URL::URL const&, bool) override;
virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint) override;
virtual void did_request_link_context_menu(u64 page_id, Gfx::IntPoint, URL::URL const&, ByteString const&, unsigned) override;
virtual void did_request_image_context_menu(u64 page_id, Gfx::IntPoint, URL::URL const&, ByteString const&, unsigned, Gfx::ShareableBitmap const&) override;
virtual void did_request_media_context_menu(u64 page_id, Gfx::IntPoint, ByteString const&, unsigned, Web::Page::MediaContextMenu const&) override;
virtual void did_get_source(u64 page_id, URL::URL const&, URL::URL const&, String const&) override;
virtual void did_inspect_dom_tree(u64 page_id, ByteString const&) override;
virtual void did_inspect_dom_node(u64 page_id, bool has_style, ByteString const& computed_style, ByteString const& resolved_style, ByteString const& custom_properties, ByteString const& node_box_sizing, ByteString const& aria_properties_state, ByteString const& fonts) override;
virtual void did_inspect_accessibility_tree(u64 page_id, ByteString const&) override;
virtual void did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID const& node_id) override;
virtual void did_finish_editing_dom_node(u64 page_id, Optional<Web::UniqueNodeID> const& node_id) override;
virtual void did_get_dom_node_html(u64 page_id, String const& html) override;
virtual void did_take_screenshot(u64 page_id, Gfx::ShareableBitmap const& screenshot) override;
virtual void did_get_internal_page_info(u64 page_id, PageInfoType, String const&) override;
virtual void did_output_js_console_message(u64 page_id, i32 message_index) override;
virtual void did_get_js_console_messages(u64 page_id, i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages) override;
virtual void did_change_favicon(u64 page_id, Gfx::ShareableBitmap const&) override;
virtual void did_request_alert(u64 page_id, String const&) override;
virtual void did_request_confirm(u64 page_id, String const&) override;
virtual void did_request_prompt(u64 page_id, String const&, String const&) override;
virtual void did_request_set_prompt_text(u64 page_id, String const& message) override;
virtual void did_request_accept_dialog(u64 page_id) override;
virtual void did_request_dismiss_dialog(u64 page_id) override;
virtual Messages::WebContentClient::DidRequestAllCookiesResponse did_request_all_cookies(URL::URL const&) override;
virtual Messages::WebContentClient::DidRequestNamedCookieResponse did_request_named_cookie(URL::URL const&, String const&) override;
virtual Messages::WebContentClient::DidRequestCookieResponse did_request_cookie(URL::URL const&, Web::Cookie::Source) override;
virtual void did_set_cookie(URL::URL const&, Web::Cookie::ParsedCookie const&, Web::Cookie::Source) override;
virtual void did_update_cookie(Web::Cookie::Cookie const&) override;
virtual void did_expire_cookies_with_time_offset(AK::Duration) override;
virtual Messages::WebContentClient::DidRequestNewWebViewResponse did_request_new_web_view(u64 page_id, Web::HTML::ActivateTab const&, Web::HTML::WebViewHints const&, Optional<u64> const& page_index) override;
virtual void did_request_activate_tab(u64 page_id) override;
virtual void did_close_browsing_context(u64 page_id) override;
virtual void did_update_resource_count(u64 page_id, i32 count_waiting) override;
virtual void did_request_restore_window(u64 page_id) override;
virtual void did_request_reposition_window(u64 page_id, Gfx::IntPoint) override;
virtual void did_request_resize_window(u64 page_id, Gfx::IntSize) override;
virtual void did_request_maximize_window(u64 page_id) override;
virtual void did_request_minimize_window(u64 page_id) override;
virtual void 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::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, Web::EventResult event_result) override;
virtual void did_finish_text_test(u64 page_id, String const& text) override;
virtual void did_find_in_page(u64 page_id, size_t current_match_index, Optional<size_t> const& total_match_count) override;
virtual void did_change_theme_color(u64 page_id, Gfx::Color color) override;
virtual void did_insert_clipboard_entry(u64 page_id, String const& data, String const& presentation_style, String const& mime_type) override;
virtual void did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState) override;
virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override;
virtual void did_allocate_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::ShareableBitmap const&, i32 back_bitmap_id, Gfx::ShareableBitmap const&) override;
virtual void inspector_did_load(u64 page_id) override;
virtual void inspector_did_select_dom_node(u64 page_id, Web::UniqueNodeID const& node_id, Optional<Web::CSS::Selector::PseudoElement::Type> const& pseudo_element) override;
virtual void inspector_did_set_dom_node_text(u64 page_id, Web::UniqueNodeID const& node_id, String const& text) override;
virtual void inspector_did_set_dom_node_tag(u64 page_id, Web::UniqueNodeID const& node_id, String const& tag) override;
virtual void inspector_did_add_dom_node_attributes(u64 page_id, Web::UniqueNodeID const& node_id, Vector<Attribute> const& attributes) override;
virtual void inspector_did_replace_dom_node_attribute(u64 page_id, Web::UniqueNodeID const& node_id, size_t attribute_index, Vector<Attribute> const& replacement_attributes) override;
virtual void inspector_did_request_dom_tree_context_menu(u64 page_id, Web::UniqueNodeID const& node_id, Gfx::IntPoint position, String const& type, Optional<String> const& tag, Optional<size_t> const& attribute_index) override;
virtual void inspector_did_request_cookie_context_menu(u64 page_id, size_t cookie_index, Gfx::IntPoint position) override;
virtual void inspector_did_execute_console_script(u64 page_id, String const& script) override;
virtual void inspector_did_export_inspector_html(u64 page_id, String const& html) override;
virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id) override;
virtual void inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> const& stylesheets) override;
virtual void inspector_did_request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier) override;
virtual void did_get_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier, URL::URL const&, String const& source) override;
Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());
// FIXME: Does a HashMap holding references make sense?
HashMap<u64, ViewImplementation*> m_views;
ProcessHandle m_process_handle;
static HashTable<WebContentClient*> s_clients;
};
template<CallableAs<IterationDecision, WebContentClient&> Callback>
void WebContentClient::for_each_client(Callback callback)
{
for (auto& it : s_clients) {
if (callback(*it) == IterationDecision::Break)
return;
}
}
}