mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-21 16:58:58 +00:00
Everywhere: Hoist the Libraries folder to the top-level
This commit is contained in:
parent
950e819ee7
commit
93712b24bf
Notes:
github-actions[bot]
2024-11-10 11:51:52 +00:00
Author: https://github.com/trflynn89
Commit: 93712b24bf
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2256
Reviewed-by: https://github.com/sideshowbarker
4547 changed files with 104 additions and 113 deletions
258
Libraries/LibWebView/Application.cpp
Normal file
258
Libraries/LibWebView/Application.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
101
Libraries/LibWebView/Application.h
Normal file
101
Libraries/LibWebView/Application.h
Normal 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&);
|
26
Libraries/LibWebView/Attribute.cpp
Normal file
26
Libraries/LibWebView/Attribute.cpp
Normal 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) };
|
||||
}
|
29
Libraries/LibWebView/Attribute.h
Normal file
29
Libraries/LibWebView/Attribute.h
Normal 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&);
|
||||
|
||||
}
|
68
Libraries/LibWebView/CMakeLists.txt
Normal file
68
Libraries/LibWebView/CMakeLists.txt
Normal 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()
|
130
Libraries/LibWebView/ChromeProcess.cpp
Normal file
130
Libraries/LibWebView/ChromeProcess.cpp
Normal 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));
|
||||
}
|
||||
|
||||
}
|
69
Libraries/LibWebView/ChromeProcess.h
Normal file
69
Libraries/LibWebView/ChromeProcess.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
707
Libraries/LibWebView/CookieJar.cpp
Normal file
707
Libraries/LibWebView/CookieJar.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
132
Libraries/LibWebView/CookieJar.h
Normal file
132
Libraries/LibWebView/CookieJar.h
Normal 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;
|
||||
}
|
||||
};
|
152
Libraries/LibWebView/Database.cpp
Normal file
152
Libraries/LibWebView/Database.cpp
Normal 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);
|
||||
|
||||
}
|
61
Libraries/LibWebView/Database.h
Normal file
61
Libraries/LibWebView/Database.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
33
Libraries/LibWebView/Forward.h
Normal file
33
Libraries/LibWebView/Forward.h
Normal 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>;
|
||||
|
||||
}
|
818
Libraries/LibWebView/InspectorClient.cpp
Normal file
818
Libraries/LibWebView/InspectorClient.cpp
Normal 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><!--</span>"sv);
|
||||
builder.appendff("<span data-node-type=\"comment\" class=\"editable\">{}</span>", comment);
|
||||
builder.append("<span>--></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><</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(" "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>></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\">> </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\"># </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\"># </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);
|
||||
}
|
||||
|
||||
}
|
98
Libraries/LibWebView/InspectorClient.h
Normal file
98
Libraries/LibWebView/InspectorClient.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
37
Libraries/LibWebView/Native.css
Normal file
37
Libraries/LibWebView/Native.css
Normal 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;
|
||||
}
|
119
Libraries/LibWebView/Options.h
Normal file
119
Libraries/LibWebView/Options.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
22
Libraries/LibWebView/PageInfo.h
Normal file
22
Libraries/LibWebView/PageInfo.h
Normal 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);
|
||||
|
||||
}
|
127
Libraries/LibWebView/Process.cpp
Normal file
127
Libraries/LibWebView/Process.cpp
Normal 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) };
|
||||
}
|
||||
|
||||
}
|
82
Libraries/LibWebView/Process.h
Normal file
82
Libraries/LibWebView/Process.h
Normal 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 };
|
||||
}
|
||||
|
||||
}
|
23
Libraries/LibWebView/ProcessHandle.cpp
Normal file
23
Libraries/LibWebView/ProcessHandle.cpp
Normal 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 };
|
||||
}
|
25
Libraries/LibWebView/ProcessHandle.h
Normal file
25
Libraries/LibWebView/ProcessHandle.h
Normal 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&);
|
207
Libraries/LibWebView/ProcessManager.cpp
Normal file
207
Libraries/LibWebView/ProcessManager.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
50
Libraries/LibWebView/ProcessManager.h
Normal file
50
Libraries/LibWebView/ProcessManager.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
21
Libraries/LibWebView/ProcessType.h
Normal file
21
Libraries/LibWebView/ProcessType.h
Normal 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,
|
||||
};
|
||||
|
||||
}
|
86
Libraries/LibWebView/SearchEngine.cpp
Normal file
86
Libraries/LibWebView/SearchEngine.cpp
Normal 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));
|
||||
}
|
||||
|
||||
}
|
25
Libraries/LibWebView/SearchEngine.h
Normal file
25
Libraries/LibWebView/SearchEngine.h
Normal 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);
|
||||
|
||||
}
|
386
Libraries/LibWebView/SourceHighlighter.cpp
Normal file
386
Libraries/LibWebView/SourceHighlighter.cpp
Normal 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("&"sv);
|
||||
} else if (code_point == 0xA0) {
|
||||
builder.append(" "sv);
|
||||
} else if (code_point == '<') {
|
||||
builder.append("<"sv);
|
||||
} else if (code_point == '>') {
|
||||
builder.append(">"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();
|
||||
}
|
||||
|
||||
}
|
167
Libraries/LibWebView/SourceHighlighter.h
Normal file
167
Libraries/LibWebView/SourceHighlighter.h
Normal 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;
|
||||
|
||||
}
|
2
Libraries/LibWebView/UIProcessClient.ipc
Normal file
2
Libraries/LibWebView/UIProcessClient.ipc
Normal file
|
@ -0,0 +1,2 @@
|
|||
endpoint UIProcessClient {
|
||||
}
|
4
Libraries/LibWebView/UIProcessServer.ipc
Normal file
4
Libraries/LibWebView/UIProcessServer.ipc
Normal file
|
@ -0,0 +1,4 @@
|
|||
endpoint UIProcessServer {
|
||||
create_new_tab(Vector<ByteString> urls) => ()
|
||||
create_new_window(Vector<ByteString> urls) => ()
|
||||
}
|
176
Libraries/LibWebView/URL.cpp
Normal file
176
Libraries/LibWebView/URL.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
41
Libraries/LibWebView/URL.h
Normal file
41
Libraries/LibWebView/URL.h
Normal 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&);
|
||||
|
||||
}
|
32
Libraries/LibWebView/UserAgent.cpp
Normal file
32
Libraries/LibWebView/UserAgent.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
19
Libraries/LibWebView/UserAgent.h
Normal file
19
Libraries/LibWebView/UserAgent.h
Normal 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);
|
||||
|
||||
}
|
664
Libraries/LibWebView/ViewImplementation.cpp
Normal file
664
Libraries/LibWebView/ViewImplementation.cpp
Normal 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());
|
||||
}
|
||||
|
||||
}
|
298
Libraries/LibWebView/ViewImplementation.h
Normal file
298
Libraries/LibWebView/ViewImplementation.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
710
Libraries/LibWebView/WebContentClient.cpp
Normal file
710
Libraries/LibWebView/WebContentClient.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
151
Libraries/LibWebView/WebContentClient.h
Normal file
151
Libraries/LibWebView/WebContentClient.h
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue