Open github-actions[bot] opened 1 month ago
See WebKit bug: https://bugs\.webkit\.org/show\_bug\.cgi?id\=261874
https://github.com/arturo-lang/arturo/blob/135fdd90eb88beea0e8a44eacc40ab01f1076180/src/extras/webview/webview-unix.cc#L1653
return ""; } // Holds a symbol name and associated type for code clarity. template <typename T> class library_symbol { public: using type = T; constexpr explicit library_symbol(const char *name) : m_name(name) {} constexpr const char *get_name() const { return m_name; } private: const char *m_name; }; // Loads a native shared library and allows one to get addresses for those // symbols. class native_library { public: native_library() = default; explicit native_library(const std::string &name) : m_handle{load_library(name)} {} #ifdef _WIN32 explicit native_library(const std::wstring &name) : m_handle{load_library(name)} {} #endif ~native_library() { if (m_handle) { #ifdef _WIN32 FreeLibrary(m_handle); #else dlclose(m_handle); #endif m_handle = nullptr; } } native_library(const native_library &other) = delete; native_library &operator=(const native_library &other) = delete; native_library(native_library &&other) noexcept { *this = std::move(other); } native_library &operator=(native_library &&other) noexcept { if (this == &other) { return *this; } m_handle = other.m_handle; other.m_handle = nullptr; return *this; } // Returns true if the library is currently loaded; otherwise false. operator bool() const { return is_loaded(); } // Get the address for the specified symbol or nullptr if not found. template <typename Symbol> typename Symbol::type get(const Symbol &symbol) const { if (is_loaded()) { // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) #ifdef _WIN32 #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-function-type" #endif return reinterpret_cast<typename Symbol::type>( GetProcAddress(m_handle, symbol.get_name())); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif #else return reinterpret_cast<typename Symbol::type>( dlsym(m_handle, symbol.get_name())); #endif // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) } return nullptr; } // Returns true if the library is currently loaded; otherwise false. bool is_loaded() const { return !!m_handle; } void detach() { m_handle = nullptr; } // Returns true if the library by the given name is currently loaded; otherwise false. static inline bool is_loaded(const std::string &name) { #ifdef _WIN32 auto handle = GetModuleHandleW(widen_string(name).c_str()); #else auto handle = dlopen(name.c_str(), RTLD_NOW | RTLD_NOLOAD); if (handle) { dlclose(handle); } #endif return !!handle; } private: #ifdef _WIN32 using mod_handle_t = HMODULE; #else using mod_handle_t = void *; #endif static inline mod_handle_t load_library(const std::string &name) { #ifdef _WIN32 return load_library(widen_string(name)); #else return dlopen(name.c_str(), RTLD_NOW); #endif } #ifdef _WIN32 static inline mod_handle_t load_library(const std::wstring &name) { return LoadLibraryW(name.c_str()); } #endif mod_handle_t m_handle{}; }; template <typename WorkFn, typename ResultFn> webview_error_t api_filter(WorkFn &&do_work, ResultFn &&put_result) noexcept { try { auto result = do_work(); if (result.ok()) { put_result(result.value()); return WEBVIEW_ERROR_OK; } return result.error().code(); } catch (const exception &e) { return e.error().code(); } catch (...) { return WEBVIEW_ERROR_UNSPECIFIED; } } template <typename WorkFn> webview_error_t api_filter(WorkFn &&do_work) noexcept { try { auto result = do_work(); if (result.ok()) { return WEBVIEW_ERROR_OK; } return result.error().code(); } catch (const exception &e) { return e.error().code(); } catch (...) { return WEBVIEW_ERROR_UNSPECIFIED; } } class user_script { public: class impl; user_script(const std::string &code, std::unique_ptr<impl> &&impl_) : m_code{code}, m_impl{std::move(impl_)} {} user_script(const user_script &other) = delete; user_script &operator=(const user_script &other) = delete; user_script(user_script &&other) noexcept { *this = std::move(other); } user_script &operator=(user_script &&other) noexcept { if (this == &other) { return *this; } m_code = std::move(other.m_code); m_impl = std::move(other.m_impl); return *this; } const std::string &get_code() const { return m_code; } impl &get_impl() { return *m_impl; } const impl &get_impl() const { return *m_impl; } private: std::string m_code; std::unique_ptr<impl> m_impl; }; class engine_base { public: virtual ~engine_base() = default; noresult navigate(const std::string &url) { if (url.empty()) { return navigate_impl("about:blank"); } return navigate_impl(url); } using binding_t = std::function<void(std::string, std::string, void *)>; class binding_ctx_t { public: binding_ctx_t(binding_t callback, void *arg) : m_callback(callback), m_arg(arg) {} void call(std::string id, std::string args) const { if (m_callback) { m_callback(id, args, m_arg); } } private: // This function is called upon execution of the bound JS function binding_t m_callback; // This user-supplied argument is passed to the callback void *m_arg; }; using sync_binding_t = std::function<std::string(std::string)>; // Synchronous bind noresult bind(const std::string &name, sync_binding_t fn) { auto wrapper = [this, fn](const std::string &id, const std::string &req, void * /*arg*/) { resolve(id, 0, fn(req)); }; return bind(name, wrapper, nullptr); } // Asynchronous bind noresult bind(const std::string &name, binding_t fn, void *arg) { // NOLINTNEXTLINE(readability-container-contains): contains() requires C++20 if (bindings.count(name) > 0) { return error_info{WEBVIEW_ERROR_DUPLICATE}; } bindings.emplace(name, binding_ctx_t(fn, arg)); replace_bind_script(); // Notify that a binding was created if the init script has already // set things up. eval("if (window.__webview__) {\n\ window.__webview__.onBind(" + json_escape(name) + ")\n\ }"); return {}; } noresult unbind(const std::string &name) { auto found = bindings.find(name); if (found == bindings.end()) { return error_info{WEBVIEW_ERROR_NOT_FOUND}; } bindings.erase(found); replace_bind_script(); // Notify that a binding was created if the init script has already // set things up. eval("if (window.__webview__) {\n\ window.__webview__.onUnbind(" + json_escape(name) + ")\n\ }"); return {}; } noresult resolve(const std::string &id, int status, const std::string &result) { // NOLINTNEXTLINE(modernize-avoid-bind): Lambda with move requires C++14 return dispatch(std::bind( [id, status, this](std::string escaped_result) { std::string js = "window.__webview__.onReply(" + json_escape(id) + ", " + std::to_string(status) + ", " + escaped_result + ")"; eval(js); }, result.empty() ? "undefined" : json_escape(result))); } result<void *> window() { return window_impl(); } result<void *> widget() { return widget_impl(); } result<void *> browser_controller() { return browser_controller_impl(); } noresult run() { return run_impl(); } noresult terminate() { return terminate_impl(); } noresult dispatch(std::function<void()> f) { return dispatch_impl(f); } noresult set_title(const std::string &title) { return set_title_impl(title); } noresult set_size(int width, int height, webview_hint_t hints) { return set_size_impl(width, height, hints); } noresult set_html(const std::string &html) { return set_html_impl(html); } noresult init(const std::string &js) { add_user_script(js); return {}; } noresult eval(const std::string &js) { return eval_impl(js); } protected: virtual noresult navigate_impl(const std::string &url) = 0; virtual result<void *> window_impl() = 0; virtual result<void *> widget_impl() = 0; virtual result<void *> browser_controller_impl() = 0; virtual noresult run_impl() = 0; virtual noresult terminate_impl() = 0; virtual noresult dispatch_impl(std::function<void()> f) = 0; virtual noresult set_title_impl(const std::string &title) = 0; virtual noresult set_size_impl(int width, int height, webview_hint_t hints) = 0; virtual noresult set_html_impl(const std::string &html) = 0; virtual noresult eval_impl(const std::string &js) = 0; virtual user_script *add_user_script(const std::string &js) { return std::addressof(*m_user_scripts.emplace(m_user_scripts.end(), add_user_script_impl(js))); } virtual user_script add_user_script_impl(const std::string &js) = 0; virtual void remove_all_user_scripts_impl(const std::list<user_script> &scripts) = 0; virtual bool are_user_scripts_equal_impl(const user_script &first, const user_script &second) = 0; virtual user_script *replace_user_script(const user_script &old_script, const std::string &new_script_code) { remove_all_user_scripts_impl(m_user_scripts); user_script *old_script_ptr{}; for (auto &script : m_user_scripts) { auto is_old_script = are_user_scripts_equal_impl(script, old_script); script = add_user_script_impl(is_old_script ? new_script_code : script.get_code()); if (is_old_script) { old_script_ptr = std::addressof(script); } } return old_script_ptr; } void replace_bind_script() { if (m_bind_script) { m_bind_script = replace_user_script(*m_bind_script, create_bind_script()); } else { m_bind_script = add_user_script(create_bind_script()); } } void add_init_script(const std::string &post_fn) { add_user_script(create_init_script(post_fn)); } std::string create_init_script(const std::string &post_fn) { auto js = std::string{} + "(function() {\n\ 'use strict';\n\ function generateId() {\n\ var crypto = window.crypto || window.msCrypto;\n\ var bytes = new Uint8Array(16);\n\ crypto.getRandomValues(bytes);\n\ return Array.prototype.slice.call(bytes).map(function(n) {\n\ return n.toString(16).padStart(2, '0');\n\ }).join('');\n\ }\n\ var Webview = (function() {\n\ var _promises = {};\n\ function Webview_() {}\n\ Webview_.prototype.post = function(message) {\n\ return (" + post_fn + ")(message);\n\ };\n\ Webview_.prototype.call = function(method) {\n\ var _id = generateId();\n\ var _params = Array.prototype.slice.call(arguments, 1);\n\ var promise = new Promise(function(resolve, reject) {\n\ _promises[_id] = { resolve, reject };\n\ });\n\ this.post(JSON.stringify({\n\ id: _id,\n\ method: method,\n\ params: _params\n\ }));\n\ return promise;\n\ };\n\ Webview_.prototype.onReply = function(id, status, result) {\n\ var promise = _promises[id];\n\ if (result !== undefined) {\n\ try {\n\ result = JSON.parse(result);\n\ } catch {\n\ promise.reject(new Error(\"Failed to parse binding result as JSON\"));\n\ return;\n\ }\n\ }\n\ if (status === 0) {\n\ promise.resolve(result);\n\ } else {\n\ promise.reject(result);\n\ }\n\ };\n\ Webview_.prototype.onBind = function(name) {\n\ if (Object.hasOwn(window, name)) {\n\ throw new Error('Property \"' + name + '\" already exists');\n\ }\n\ window[name] = (function() {\n\ var params = [name].concat(Array.prototype.slice.call(arguments));\n\ return Webview_.prototype.call.apply(this, params);\n\ }).bind(this);\n\ };\n\ Webview_.prototype.onUnbind = function(name) {\n\ if (!Object.hasOwn(window, name)) {\n\ throw new Error('Property \"' + name + '\" does not exist');\n\ }\n\ delete window[name];\n\ };\n\ return Webview_;\n\ })();\n\ window.__webview__ = new Webview();\n\ })()"; return js; } std::string create_bind_script() { std::string js_names = "["; bool first = true; for (const auto &binding : bindings) { if (first) { first = false; } else { js_names += ","; } js_names += json_escape(binding.first); } js_names += "]"; auto js = std::string{} + "(function() {\n\ 'use strict';\n\ var methods = " + js_names + ";\n\ methods.forEach(function(name) {\n\ window.__webview__.onBind(name);\n\ });\n\ })()"; return js; } virtual void on_message(const std::string &msg) { auto id = json_parse(msg, "id", 0); auto name = json_parse(msg, "method", 0); auto args = json_parse(msg, "params", 0); auto found = bindings.find(name); if (found == bindings.end()) { return; } const auto &context = found->second; dispatch([=] { context.call(id, args); }); } virtual void on_window_created() { inc_window_count(); } virtual void on_window_destroyed(bool skip_termination = false) { if (dec_window_count() <= 0) { if (!skip_termination) { terminate(); } } } private: static std::atomic_uint &window_ref_count() { static std::atomic_uint ref_count{0}; return ref_count; } static unsigned int inc_window_count() { return ++window_ref_count(); } static unsigned int dec_window_count() { auto &count = window_ref_count(); if (count > 0) { return --count; } return 0; } std::map<std::string, binding_ctx_t> bindings; user_script *m_bind_script{}; std::list<user_script> m_user_scripts; }; } // namespace detail WEBVIEW_DEPRECATED_PRIVATE inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, const char **value, size_t *valuesz) { return detail::json_parse_c(s, sz, key, keysz, value, valuesz); } WEBVIEW_DEPRECATED_PRIVATE inline std::string json_escape(const std::string &s) { return detail::json_escape(s); } WEBVIEW_DEPRECATED_PRIVATE inline int json_unescape(const char *s, size_t n, char *out) { return detail::json_unescape(s, n, out); } WEBVIEW_DEPRECATED_PRIVATE inline std::string json_parse(const std::string &s, const std::string &key, const int index) { return detail::json_parse(s, key, index); } } // namespace webview #if defined(WEBVIEW_GTK) // // ==================================================================== // // This implementation uses webkit2gtk backend. It requires GTK and // WebKitGTK libraries. Proper compiler flags can be retrieved via: // // pkg-config --cflags --libs gtk4 webkitgtk-6.0 // pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1 // pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0 // // ==================================================================== // #include <cstdlib> #include <gtk/gtk.h> #if GTK_MAJOR_VERSION >= 4 #include <jsc/jsc.h> #include <webkit/webkit.h> #ifdef GDK_WINDOWING_X11 #include <gdk/x11/gdkx.h> #endif #elif GTK_MAJOR_VERSION >= 3 #include <JavaScriptCore/JavaScript.h> #include <webkit2/webkit2.h> #ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> #endif #endif #include <fcntl.h> #include <sys/stat.h> namespace webview { namespace detail { // Namespace containing workaround for WebKit 2.42 when using NVIDIA GPU // driver. // See WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=261874 // Please remove all of the code in this namespace when it's no longer needed. namespace webkit_dmabuf { // Get environment variable. Not thread-safe. static inline std::string get_env(const std::string &name) { auto *value = std::getenv(name.c_str()); if (value) { return {value}; } return {}; } // Set environment variable. Not thread-safe. static inline void set_env(const std::string &name, const std::string &value) { ::setenv(name.c_str(), value.c_str(), 1); } // Checks whether the NVIDIA GPU driver is used based on whether the kernel // module is loaded. static inline bool is_using_nvidia_driver() { struct ::stat buffer {}; if (::stat("/sys/module/nvidia", &buffer) != 0) { return false; } return S_ISDIR(buffer.st_mode); } // Checks whether the windowing system is Wayland. static inline bool is_wayland_display() { if (!get_env("WAYLAND_DISPLAY").empty()) { return true; } if (get_env("XDG_SESSION_TYPE") == "wayland") { return true; } if (get_env("DESKTOP_SESSION").find("wayland") != std::string::npos) { return true; } return false; } // Checks whether the GDK X11 backend is used. // See: https://docs.gtk.org/gdk3/class.DisplayManager.html static inline bool is_gdk_x11_backend() { #ifdef GDK_WINDOWING_X11 auto *gdk_display = gdk_display_get_default(); return GDK_IS_X11_DISPLAY(gdk_display); // NOLINT(misc-const-correctness) #else return false; #endif } // Checks whether WebKit is affected by bug when using DMA-BUF renderer. // Returns true if all of the following conditions are met: // - WebKit version is >= 2.42 (please narrow this down when there's a fix). // - Environment variables are empty or not set: // - WEBKIT_DISABLE_DMABUF_RENDERER // - Windowing system is not Wayland. // - GDK backend is X11. // - NVIDIA GPU driver is used. static inline bool is_webkit_dmabuf_bugged() { auto wk_major = webkit_get_major_version(); auto wk_minor = webkit_get_minor_version(); // TODO: Narrow down affected WebKit version when there's a fixed version auto is_affected_wk_version = wk_major == 2 && wk_minor >= 42; if (!is_affected_wk_version) { return false; } if (!get_env("WEBKIT_DISABLE_DMABUF_RENDERER").empty()) { return false; } if (is_wayland_display()) { return false; } if (!is_gdk_x11_backend()) { return false; } if (!is_using_nvidia_driver()) { return false; } return true; } // Applies workaround for WebKit DMA-BUF bug if needed. // See WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=261874 static inline void apply_webkit_dmabuf_workaround() { if (!is_webkit_dmabuf_bugged()) { return; } set_env("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } } // namespace webkit_dmabuf class user_script::impl { public: impl(WebKitUserScript *script) : m_script{script} { webkit_user_script_ref(script); } ~impl() { webkit_user_script_unref(m_script); } impl(const impl &) = delete; impl &operator=(const impl &) = delete; impl(impl &&) = delete; impl &operator=(impl &&) = delete; WebKitUserScript *get_native() const { return m_script; } private: WebKitUserScript *m_script{}; }; /** * GTK compatibility helper class. */ class gtk_compat { public: static gboolean init_check() { #if GTK_MAJOR_VERSION >= 4 return gtk_init_check(); #else return gtk_init_check(nullptr, nullptr); #endif } static GtkWidget *window_new() { #if GTK_MAJOR_VERSION >= 4 return gtk_window_new(); #else return gtk_window_new(GTK_WINDOW_TOPLEVEL); #endif } static void window_set_child(GtkWindow *window, GtkWidget *widget) { #if GTK_MAJOR_VERSION >= 4 gtk_window_set_child(window, widget); #else gtk_container_add(GTK_CONTAINER(window), widget); #endif } static void window_remove_child(GtkWindow *window, GtkWidget *widget) { #if GTK_MAJOR_VERSION >= 4 if (gtk_window_get_child(window) == widget) { gtk_window_set_child(window, nullptr); } #else gtk_container_remove(GTK_CONTAINER(window), widget); #endif } static void widget_set_visible(GtkWidget *widget, bool visible) { #if GTK_MAJOR_VERSION >= 4 gtk_widget_set_visible(widget, visible ? TRUE : FALSE); #else if (visible) { gtk_widget_show(widget); } else { gtk_widget_hide(widget); } #endif } static void window_set_size(GtkWindow *window, int width, int height) { #if GTK_MAJOR_VERSION >= 4 gtk_window_set_default_size(window, width, height); #else gtk_window_resize(window, width, height); #endif } static void window_set_max_size(GtkWindow *window, int width, int height) { // X11-specific features are available in GTK 3 but not GTK 4 #if GTK_MAJOR_VERSION < 4 GdkGeometry g{}; g.max_width = width; g.max_height = height; GdkWindowHints h = GDK_HINT_MAX_SIZE; gtk_window_set_geometry_hints(GTK_WINDOW(window), nullptr, &g, h); #else // Avoid "unused parameter" warnings (void)window; (void)width; (void)height; #endif } }; /** * WebKitGTK compatibility helper class. */ class webkitgtk_compat { public: #if GTK_MAJOR_VERSION >= 4 using wk_handler_js_value_t = JSCValue; #else using wk_handler_js_value_t = WebKitJavascriptResult; #endif using on_script_message_received_t = std::function<void(WebKitUserContentManager *, const std::string &)>; static void connect_script_message_received(WebKitUserContentManager *manager, const std::string &handler_name, on_script_message_received_t handler) { std::string signal_name = "script-message-received::"; signal_name += handler_name; auto callback = +[](WebKitUserContentManager *manager, wk_handler_js_value_t *r, gpointer arg) { auto *handler = static_cast<on_script_message_received_t *>(arg); (*handler)(manager, get_string_from_js_result(r)); }; auto deleter = +[](gpointer data, GClosure *) { delete static_cast<on_script_message_received_t *>(data); }; g_signal_connect_data(manager, signal_name.c_str(), G_CALLBACK(callback), new on_script_message_received_t{handler}, deleter, static_cast<GConnectFlags>(0) /*G_CONNECT_DEFAULT*/); } static std::string get_string_from_js_result(JSCValue *r) { char *cs = jsc_value_to_string(r); std::string s{cs}; g_free(cs); return s; } #if GTK_MAJOR_VERSION < 4 static std::string get_string_from_js_result(WebKitJavascriptResult *r) { #if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 22) || \ WEBKIT_MAJOR_VERSION > 2 JSCValue *value = webkit_javascript_result_get_js_value(r); return get_string_from_js_result(value); #else JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r); JSValueRef value = webkit_javascript_result_get_value(r); JSStringRef js = JSValueToStringCopy(ctx, value, nullptr); size_t n = JSStringGetMaximumUTF8CStringSize(js); char *cs = g_new(char, n); JSStringGetUTF8CString(js, cs, n); JSStringRelease(js); std::string s{cs}; g_free(cs); return s; #endif } #endif static void user_content_manager_register_script_message_handler( WebKitUserContentManager *manager, const gchar *name) { #if GTK_MAJOR_VERSION >= 4 webkit_user_content_manager_register_script_message_handler(manager, name, nullptr); #else webkit_user_content_manager_register_script_message_handler(manager, name); #endif } }; class gtk_webkit_engine : public engine_base { public: gtk_webkit_engine(bool debug, void *window) : m_owns_window{!window}, m_window(static_cast<GtkWidget *>(window)) { if (m_owns_window) { if (!gtk_compat::init_check()) { throw exception{WEBVIEW_ERROR_UNSPECIFIED, "GTK init failed"}; } m_window = gtk_compat::window_new(); on_window_created(); auto on_window_destroy = +[](GtkWidget *, gpointer arg) { auto *w = static_cast<gtk_webkit_engine *>(arg); w->m_window = nullptr; w->on_window_destroyed(); }; g_signal_connect(G_OBJECT(m_window), "destroy", G_CALLBACK(on_window_destroy), this); } webkit_dmabuf::apply_webkit_dmabuf_workaround(); // Initialize webview widget m_webview = webkit_web_view_new(); g_object_ref_sink(m_webview); WebKitUserContentManager *manager = m_user_content_manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); webkitgtk_compat::connect_script_message_received( manager, "__webview__", [this](WebKitUserContentManager *, const std::string &r) { on_message(r); }); webkitgtk_compat::user_content_manager_register_script_message_handler( manager, "__webview__"); add_init_script("function(message) {\n\ return window.webkit.messageHandlers.__webview__.postMessage(message);\n\ }"); gtk_compat::window_set_child(GTK_WINDOW(m_window), GTK_WIDGET(m_webview)); gtk_compat::widget_set_visible(GTK_WIDGET(m_webview), true); WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
See WebKit bug: https://bugs\.webkit\.org/show\_bug\.cgi?id\=261874
https://github.com/arturo-lang/arturo/blob/135fdd90eb88beea0e8a44eacc40ab01f1076180/src/extras/webview/webview-unix.cc#L1653