From fb375e5e3582a5c9cfe3d11c1036fe9affe1b12f Mon Sep 17 00:00:00 2001 From: Qiantan Hong Date: Sun, 23 Oct 2022 01:55:53 -0700 Subject: [PATCH] Implement WebKit user scripts and script message handlers. * src/xwidget.h (store_xwidget_script_message_event): store script message event into event queue * src/xwidget.c (store_xwidget_script_message_event): (Fxwidget_webkit_add_user_script): (Fxwidget_webkit_remove_all_user_scripts): user script primitives (Fxwidget_webkit_register_script_message): (Fxwidget_webkit_unregister_script_message): script message handler primitives. * src/nsxwidget.c (nsxwidget_webkit_add_user_script): (nsxwidget_webkit_remove_all_user_scripts): (nsxwidget_webkit_register_script_message): (nsxwidget_webkit_unregister_script_message): NS implementation of user script and script message handler. Changed naming of a previous used script message handler to avoid namespace pollution. * src/nsxwidget.h (nsxwidget_webkit_add_user_script): (nsxwidget_webkit_remove_all_user_scripts): (nsxwidget_webkit_register_script_message): (nsxwidget_webkit_unregister_script_message): * lisp/xwidget.el (xwidget-webkit-callback): (xwidget-webkit-push-script-message-handler): (xwidget-webkit-pop-script-message-handler): let lisp recognize and dispatch script message events * doc/lispref/display.texi: document user script and script message handler --- doc/lispref/display.texi | 52 +++++++++++ lisp/xwidget.el | 36 +++++++- src/nsxwidget.h | 5 + src/nsxwidget.m | 80 ++++++++++++++-- src/xwidget.c | 192 +++++++++++++++++++++++++++++++++++++++ src/xwidget.h | 4 + 6 files changed, 361 insertions(+), 8 deletions(-) diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi index 15cd5518d9..48b38ff837 100644 --- a/doc/lispref/display.texi +++ b/doc/lispref/display.texi @@ -7279,6 +7279,58 @@ Xwidgets @var{default} was omitted. @end defun +@defun xwidget-webkit-add-user-script xwidget script when main-frame-only +This function adds the specified user script @code{script} to the +browser widget specified by @var{xwidget}. + +User scripts are custom JavaScript code that is automatically run in +the pages of the browser widget. The @var{when} argument specifies +when @var{script} is run, it can be one of the following: + +@table @code +@item start +@var{script} is run upon first loading a document. +@item end +@var{script} is run after a document loads. +@end table + +The @var{main-frame-only} argument specifies which HTML frames run +@var{script}. If @var{main-frame-only} is @code{nil}, @var{script} is +run in all HTML frames. Otherwise, @var{script} is only run in +top-level HTML frames. +@end defun + +@defun xwidget-webkit-remove-all-user-scripts xwidget +This functions removes all user scripts from the browser widget +specified by @var{xwidget}. +@end defun + +@defun xwidget-webkit-push-script-message-handler xwidget name handler +This function registers @var{handler} to handle script messages with +namespace specified by string @var{name} for the browser widget +specified by @var{xwidget}. + +Script message enables JavaScript code run in browser widgets to pass +data to Emacs. After @var{handler} is registered with @var{name}, +running the following JavaScript code + +@example +window.webkit.messageHandlers.@var{name}.postMessage(@var{JSObject}); +@end example + +@noindent causes @var{handler} to be called with two arguments: @var{xwidget} +and @var{JSObject} converted to a Lisp object. +@end defun + +@defun xwidget-webkit-pop-script-message-handler +This functions removes a handler registered with namespace specified +by string @var{name} for the browser widget specified by +@var{xwidget}. + +The returned value is the removed handler function, or @code{nil} if +no such handler is found. +@end defun + @defun xwidget-webkit-get-title xwidget This function returns the title of @var{xwidget} as a string. @end defun diff --git a/lisp/xwidget.el b/lisp/xwidget.el index 41a1190c64..418a9f3acc 100644 --- a/lisp/xwidget.el +++ b/lisp/xwidget.el @@ -485,7 +485,41 @@ xwidget-webkit-callback (let ((proc (nth 3 last-input-event)) (arg (nth 4 last-input-event))) (funcall proc arg))) - (t (xwidget-log "unhandled event:%s" xwidget-event-type))))) + ((eq xwidget-event-type 'script-message) + (let ((name (nth 3 last-input-event)) + (value (nth 4 last-input-event))) + (let ((handler-pair (assoc name (xwidget-get xwidget 'script-message-handlers)))) + (if handler-pair + (funcall (cdr handler-pair) xwidget value) + (xwidget-log "unhandled script message: %s" name))))) + (t (xwidget-log "unhandled event: %s" xwidget-event-type))))) + +(defun xwidget-webkit-push-script-message-handler (xwidget name handler) + "Register HANDLER to handle script messages with NAME for WebKit XWIDGET. + +HANDLER will be called with two arguments: the WebKit XWIDGET and +the JavaScript object passed from the script message converted to +a Lisp object." + (let ((old-alist (xwidget-get xwidget 'script-message-handlers))) + (unless (assoc name old-alist) + (xwidget-webkit-register-script-message (xwidget-webkit-current-session) name)) + (xwidget-put xwidget 'script-message-handlers + (cons (cons name handler) old-alist))) + name) + +(defun xwidget-webkit-pop-script-message-handler (xwidget name) + "Remove a handler registered with NAME for WebKit XWIDGET. + +Return the removed handler function, or NIL if no such handler is +found." + (let* ((old-alist (xwidget-get xwidget 'script-message-handlers)) + (handler-pair (assoc name old-alist))) + (when handler-pair + (let ((new-alist (delq handler-pair old-alist))) + (xwidget-put xwidget 'script-message-handlers new-alist) + (unless (assoc name new-alist) + (xwidget-webkit-unregister-script-message (xwidget-webkit-current-session) name))) + (cdr handler-pair)))) (defvar bookmark-make-record-function) (when (memq window-system '(mac ns)) diff --git a/src/nsxwidget.h b/src/nsxwidget.h index 666509744a..cd1dc1bc2c 100644 --- a/src/nsxwidget.h +++ b/src/nsxwidget.h @@ -40,6 +40,11 @@ #define NSXWIDGET_H_INCLUDED void nsxwidget_webkit_execute_script (struct xwidget *xw, const char *script, Lisp_Object fun); +void nsxwidget_webkit_add_user_script (struct xwidget *xw, const char *script, + int injection_time_start, int main_frame_only); +void nsxwidget_webkit_remove_all_user_scripts (struct xwidget *xw); +Lisp_Object nsxwidget_webkit_register_script_message (struct xwidget *xw, const char *name); +void nsxwidget_webkit_unregister_script_message (struct xwidget *xw, const char *name); /* Functions for xwidget model. */ #ifdef __OBJC__ diff --git a/src/nsxwidget.m b/src/nsxwidget.m index be0eba0bcb..901277f9a3 100644 --- a/src/nsxwidget.m +++ b/src/nsxwidget.m @@ -88,7 +88,7 @@ - (id)initWithFrame:(CGRect)frame @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)" @" AppleWebKit/603.3.8 (KHTML, like Gecko)" @" Version/11.0.1 Safari/603.3.8"; - [scriptor addScriptMessageHandler:self name:@"keyDown"]; + [scriptor addScriptMessageHandler:self name:@"__xwidget_internal_keyDown"]; [scriptor addUserScript:[[WKUserScript alloc] initWithSource:xwScript injectionTime: @@ -275,23 +275,34 @@ + (void)initialize @"}" @"function xwKeyDown(event) {" @" if (event.ctrlKey && event.key == 'g') {" - @" window.webkit.messageHandlers.keyDown.postMessage('C-g');" + @" window.webkit.messageHandlers.__xwidget_internal_keyDown.postMessage('C-g');" @" }" @"}" @"document.addEventListener('keydown', xwKeyDown);" ; } +static Lisp_Object js_to_lisp (id value); + /* Confirming to WKScriptMessageHandler, listens concerning keyDown in webkit. Currently 'C-g'. */ - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { - if ([message.body isEqualToString:@"C-g"]) + if ([message.name isEqualToString:@"__xwidget_internal_keyDown"]) { - /* Just give up focus, no relay "C-g" to emacs, another "C-g" - follows will be handled by emacs. */ - [self.window makeFirstResponder:self.xw->xv->emacswindow]; + if ([message.body isEqualToString:@"C-g"]) + { + /* Just give up focus, no relay "C-g" to emacs, another "C-g" + follows will be handled by emacs. */ + [self.window makeFirstResponder:self.xw->xv->emacswindow]; + } + } + else + { + store_xwidget_script_message_event (self.xw, + message.name.UTF8String, + js_to_lisp (message.body)); } } @@ -437,6 +448,61 @@ - (void)userContentController:(WKUserContentController *)userContentController }]; } +void +nsxwidget_webkit_add_user_script (struct xwidget *xw, const char *script, + int injection_time_start, int main_frame_only) +{ + XwWebView *xwWebView = (XwWebView *) xw->xwWidget; + WKUserContentController *scriptor = xwWebView.configuration.userContentController; + + NSString *javascriptString = [NSString stringWithUTF8String:script]; + WKUserScriptInjectionTime injectionTime = injection_time_start? + WKUserScriptInjectionTimeAtDocumentStart : WKUserScriptInjectionTimeAtDocumentEnd; + WKUserScript *userScript = [[WKUserScript alloc] + initWithSource: javascriptString + injectionTime: injectionTime + forMainFrameOnly: main_frame_only]; + [scriptor addUserScript: userScript]; +} + +void +nsxwidget_webkit_remove_all_user_scripts (struct xwidget *xw) +{ + XwWebView *xwWebView = (XwWebView *) xw->xwWidget; + WKUserContentController *scriptor = xwWebView.configuration.userContentController; + + [scriptor removeAllUserScripts]; +} + +Lisp_Object +nsxwidget_webkit_register_script_message (struct xwidget *xw, const char *name) +{ + XwWebView *xwWebView = (XwWebView *) xw->xwWidget; + WKUserContentController *scriptor = xwWebView.configuration.userContentController; + + NSString *messageName = [NSString stringWithUTF8String:name]; + + @try + { + [scriptor addScriptMessageHandler:xwWebView name:messageName]; + } + @catch (NSException *e) + { + error ("Failed to register WebKit script message handler"); + } + return Qnil; +} + +void +nsxwidget_webkit_unregister_script_message (struct xwidget *xw, const char *name) +{ + XwWebView *xwWebView = (XwWebView *) xw->xwWidget; + WKUserContentController *scriptor = xwWebView.configuration.userContentController; + + NSString *messageName = [NSString stringWithUTF8String:name]; + [scriptor removeScriptMessageHandlerForName:messageName]; +} + /* Window containing an xwidget. */ @implementation XwWindow @@ -469,7 +535,7 @@ - (BOOL)isFlipped { return YES; } WKUserContentController *scriptor = ((XwWebView *) xw->xwWidget).configuration.userContentController; [scriptor removeAllUserScripts]; - [scriptor removeScriptMessageHandlerForName:@"keyDown"]; + [scriptor removeScriptMessageHandlerForName:@"__xwidget_internal_keyDown"]; [scriptor release]; if (xw->xv) xw->xv->model = Qnil; /* Make sure related view stale. */ diff --git a/src/xwidget.c b/src/xwidget.c index 8bdfab02fd..3cdd95f970 100644 --- a/src/xwidget.c +++ b/src/xwidget.c @@ -126,6 +126,11 @@ webkit_decide_policy_cb (WebKitWebView *, }; static void find_widget (GtkWidget *t, struct widget_search_data *); + +static void webkit_script_message_cb (WebKitUserContentManager *, + WebKitJavascriptResult *, + gpointer); + #endif #ifdef HAVE_PGTK @@ -380,6 +385,9 @@ DEFUN ("make-xwidget", settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (xw->widget_osr)); g_object_set (G_OBJECT (settings), "enable-developer-extras", TRUE, NULL); } + WebKitUserContentManager *manager = webkit_user_content_manager_new (); + xw->widget_osr = webkit_web_view_new_with_user_content_manager (manager); + g_object_unref (manager); gtk_widget_set_size_request (GTK_WIDGET (xw->widget_osr), xw->width, xw->height); @@ -2308,6 +2316,21 @@ store_xwidget_js_callback_event (struct xwidget *xw, kbd_buffer_store_event (&event); } +void +store_xwidget_script_message_event (struct xwidget *xw, + const char *name, + Lisp_Object body) +{ + struct input_event event; + Lisp_Object xwl; + XSETXWIDGET (xwl, xw); + EVENT_INIT (event); + event.kind = XWIDGET_EVENT; + event.frame_or_window = Qnil; + event.arg = list4 (Qscript_message, xwl, build_string (name), body); + kbd_buffer_store_event (&event); +} + #ifdef USE_GTK static void @@ -2622,6 +2645,21 @@ webkit_decide_policy_cb (WebKitWebView *webView, } } +static void +webkit_script_message_cb (WebKitUserContentManager *manager, + WebKitJavascriptResult *js_result, + gpointer data) +{ + struct xwidget *xw = g_object_get_data (G_OBJECT (manager), + XG_XWIDGET); + JSCValue *value = webkit_javascript_result_get_js_value (js_result); + const char *arg = data; + Lisp_Object lisp_value = webkit_js_to_lisp (value); + store_xwidget_script_message_event (xw, arg, lisp_value); + + webkit_javascript_result_unref (js_result); +} + static gboolean webkit_script_dialog_cb (WebKitWebView *webview, WebKitScriptDialog *script_dialog, @@ -3198,6 +3236,151 @@ DEFUN ("xwidget-webkit-execute-script", return Qnil; } +DEFUN ("xwidget-webkit-add-user-script", + Fxwidget_webkit_add_user_script, Sxwidget_webkit_add_user_script, + 4, 4, 0, + doc: /* Add the user script SCRIPT to the WebKit XWIDGET. + +WHEN is one of the following values, and specifies when the script is +run: +- `start', which means the script is run upon first loading a document. +- `end', which means the script is run after a document loads. + +If MAIN_FRAME_ONLY is nil, SCRIPT is run in all HTML frames. +Otherwise, SCRIPT is only run in top-level HTML frames. */) + (Lisp_Object xwidget, Lisp_Object script, + Lisp_Object when, Lisp_Object main_frame_only) +{ + WEBKIT_FN_INIT (); + CHECK_STRING (script); + CHECK_SYMBOL (when); + + script = ENCODE_UTF_8 (script); + + Bool start; + if (EQ (when, Qstart)) + start = true; + else if (EQ (when, Qend)) + start = false; + else + error ("Unknown Xwidget Webkit user script injection time: %s", + SDATA (SYMBOL_NAME (when))); + +#ifdef USE_GTK + WebKitWebView *view = WEBKIT_WEB_VIEW (xw->widget_osr); + WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager (view); + + WebKitUserContentInjectedFrames webkit_injected_frames + = (!NILP (main_frame_only)) + ? WEBKIT_USER_CONTENT_INJECT_TOP_FRAME + : WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES; + WebKitUserScriptInjectionTime webkit_injection_time = start + ? WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START + : WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END; + WebKitUserScript *user_script + = webkit_user_script_new (SSDATA (script), + webkit_injected_frames, + webkit_injection_time, + NULL, NULL); + webkit_user_content_manager_add_script (manager, user_script); + webkit_user_script_unref (user_script); +#elif defined NS_IMPL_COCOA + nsxwidget_webkit_add_user_script (xw, SSDATA (script), injection_time_start, mfo); +#endif + return Qnil; +} + +DEFUN ("xwidget-webkit-remove-all-user-scripts", + Fxwidget_webkit_remove_all_user_scripts, Sxwidget_webkit_remove_all_user_scripts, + 1, 1, 0, + doc: /* Remove all user scripts from WebKit XWIDGET. */) + (Lisp_Object xwidget) +{ + WEBKIT_FN_INIT (); + +#ifdef USE_GTK + WebKitWebView *view = WEBKIT_WEB_VIEW (xw->widget_osr); + WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager (view); + + webkit_user_content_manager_remove_all_scripts (manager); +#elif defined NS_IMPL_COCOA + nsxwidget_webkit_remove_all_user_scripts(xw); +#endif + return Qnil; +} + +DEFUN ("xwidget-webkit-register-script-message", + Fxwidget_webkit_register_script_message, Sxwidget_webkit_register_script_message, + 2, 2, 0, + doc: /* Register script message with NAME in WebKit XWIDGET. + +This operation fails if NAME has already been registered for XWIDGET. */) + (Lisp_Object xwidget, Lisp_Object name) +{ + WEBKIT_FN_INIT (); + CHECK_STRING (name); + char *sname = xlispstrdup (name); + +#ifdef USE_GTK + WebKitWebView *view = WEBKIT_WEB_VIEW (xw->widget_osr); + WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager (view); + + USE_SAFE_ALLOCA; + gchar *signal_name = SAFE_ALLOCA (strlen ("script-message-received::") + strlen (sname) + 1); + stpcpy (stpcpy (signal_name, "script-message-received::"), sname); + g_signal_connect_data (manager, signal_name, G_CALLBACK (webkit_script_message_cb), + sname, (GClosureNotify) xfree, 0); + SAFE_FREE (); + + if (webkit_user_content_manager_register_script_message_handler (manager, sname)) + { + g_object_set_data (G_OBJECT (manager), XG_XWIDGET, xw); + } + else + { + g_signal_handlers_disconnect_matched (manager, + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, sname); + error ("Failed to register WebKit script message handler"); + } +#elif defined NS_IMPL_COCOA + nsxwidget_webkit_register_script_message (xw, sname); + xfree (sname); +#endif + return Qnil; +} + +DEFUN ("xwidget-webkit-unregister-script-message", + Fxwidget_webkit_unregister_script_message, Sxwidget_webkit_unregister_script_message, + 2, 2, 0, + doc: /* Unregister script message with NAME in Webkit XWIDGET. */) + (Lisp_Object xwidget, Lisp_Object name) +{ + WEBKIT_FN_INIT (); + CHECK_STRING (name); + + USE_SAFE_ALLOCA; + char *sname; + SAFE_ALLOCA_STRING (sname, name); + +#ifdef USE_GTK + WebKitWebView *view = WEBKIT_WEB_VIEW (xw->widget_osr); + WebKitUserContentManager *manager + = webkit_web_view_get_user_content_manager (view); + + webkit_user_content_manager_unregister_script_message_handler (manager, sname); + g_signal_handlers_disconnect_matched (manager, + G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DETAIL, + 0, g_quark_from_string (sname), 0, + G_CALLBACK (webkit_script_message_cb), 0); +#elif defined NS_IMPL_COCOA + nsxwidget_webkit_unregister_script_message(xw, sname); +#endif + + SAFE_FREE(); + return Qnil; +} + DEFUN ("xwidget-resize", Fxwidget_resize, Sxwidget_resize, 3, 3, 0, doc: /* Resize XWIDGET to NEW_WIDTH, NEW_HEIGHT. */ ) (Lisp_Object xwidget, Lisp_Object new_width, Lisp_Object new_height) @@ -3919,6 +4102,15 @@ syms_of_xwidget (void) defsubr (&Sxwidget_webkit_execute_script); DEFSYM (Qwebkit, "webkit"); + defsubr (&Sxwidget_webkit_add_user_script); + DEFSYM (Qstart, "start"); + DEFSYM (Qend, "end"); + defsubr (&Sxwidget_webkit_remove_all_user_scripts); + + DEFSYM (Qscript_message, "script-message"); + defsubr (&Sxwidget_webkit_register_script_message); + defsubr (&Sxwidget_webkit_unregister_script_message); + defsubr (&Sxwidget_size_request); defsubr (&Sdelete_xwidget_view); diff --git a/src/xwidget.h b/src/xwidget.h index 502beb6765..e835f700bc 100644 --- a/src/xwidget.h +++ b/src/xwidget.h @@ -203,6 +203,10 @@ #define XG_XWIDGET_VIEW "emacs_xwidget_view" Lisp_Object proc, Lisp_Object argument); +void store_xwidget_script_message_event (struct xwidget *xw, + const char *name, + Lisp_Object value); + extern struct xwidget *xwidget_from_id (uint32_t id); #ifdef HAVE_X_WINDOWS -- 2.32.1 (Apple Git-133)