1/* NS Cocoa part implementation of xwidget and webkit widget. 2 3Copyright (C) 2019-2021 Free Software Foundation, Inc. 4 5This file is part of GNU Emacs. 6 7GNU Emacs is free software: you can redistribute it and/or modify 8it under the terms of the GNU General Public License as published by 9the Free Software Foundation, either version 3 of the License, or (at 10your option) any later version. 11 12GNU Emacs is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU General Public License for more details. 16 17You should have received a copy of the GNU General Public License 18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ 19 20#include <config.h> 21 22#include "lisp.h" 23#include "blockinput.h" 24#include "dispextern.h" 25#include "buffer.h" 26#include "frame.h" 27#include "nsterm.h" 28#include "xwidget.h" 29 30#import <AppKit/AppKit.h> 31#import <WebKit/WebKit.h> 32 33/* Thoughts on NS Cocoa xwidget and webkit2: 34 35 Webkit2 process architecture seems to be very hostile for offscreen 36 rendering techniques, which is used by GTK xwidget implementation; 37 Specifically NSView level view sharing / copying is not working. 38 39 *** So only one view can be associated with a model. *** 40 41 With this decision, implementation is plain and can expect best out 42 of webkit2's rationale. But process and session structures will 43 diverge from GTK xwidget. Though, cosmetically similar usages can 44 be presented and will be preferred, if agreeable. 45 46 For other widget types, OSR seems possible, but will not care for a 47 while. */ 48 49/* Xwidget webkit. */ 50 51@interface XwWebView : WKWebView 52<WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler> 53@property struct xwidget *xw; 54/* Map url to whether javascript is blocked by 55 'Content-Security-Policy' sandbox without allow-scripts. */ 56@property(retain) NSMutableDictionary *urlScriptBlocked; 57@end 58@implementation XwWebView : WKWebView 59 60- (id)initWithFrame:(CGRect)frame 61 configuration:(WKWebViewConfiguration *)configuration 62 xwidget:(struct xwidget *)xw 63{ 64 /* Script controller to add script message handler and user script. */ 65 WKUserContentController *scriptor = [[WKUserContentController alloc] init]; 66 configuration.userContentController = scriptor; 67 68 /* Enable inspect element context menu item for debugging. */ 69 [configuration.preferences setValue:@YES 70 forKey:@"developerExtrasEnabled"]; 71 72 Lisp_Object enablePlugins = 73 Fintern (build_string ("xwidget-webkit-enable-plugins"), Qnil); 74 if (!EQ (Fsymbol_value (enablePlugins), Qnil)) 75 configuration.preferences.plugInsEnabled = YES; 76 77 self = [super initWithFrame:frame configuration:configuration]; 78 if (self) 79 { 80 self.xw = xw; 81 self.urlScriptBlocked = [[NSMutableDictionary alloc] init]; 82 self.navigationDelegate = self; 83 self.UIDelegate = self; 84 self.customUserAgent = 85 @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)" 86 @" AppleWebKit/603.3.8 (KHTML, like Gecko)" 87 @" Version/11.0.1 Safari/603.3.8"; 88 [scriptor addScriptMessageHandler:self name:@"keyDown"]; 89 [scriptor addUserScript:[[WKUserScript alloc] 90 initWithSource:xwScript 91 injectionTime: 92 WKUserScriptInjectionTimeAtDocumentStart 93 forMainFrameOnly:NO]]; 94 } 95 return self; 96} 97 98- (void)webView:(WKWebView *)webView 99didFinishNavigation:(WKNavigation *)navigation 100{ 101 if (EQ (Fbuffer_live_p (self.xw->buffer), Qt)) 102 store_xwidget_event_string (self.xw, "load-changed", ""); 103} 104 105- (void)webView:(WKWebView *)webView 106decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 107decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 108{ 109 switch (navigationAction.navigationType) { 110 case WKNavigationTypeLinkActivated: 111 decisionHandler (WKNavigationActionPolicyAllow); 112 break; 113 default: 114 // decisionHandler (WKNavigationActionPolicyCancel); 115 decisionHandler (WKNavigationActionPolicyAllow); 116 break; 117 } 118} 119 120- (void)webView:(WKWebView *)webView 121decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse 122decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler 123{ 124 if (!navigationResponse.canShowMIMEType) 125 { 126 NSString *url = navigationResponse.response.URL.absoluteString; 127 NSString *mimetype = navigationResponse.response.MIMEType; 128 NSString *filename = navigationResponse.response.suggestedFilename; 129 decisionHandler (WKNavigationResponsePolicyCancel); 130 store_xwidget_download_callback_event (self.xw, 131 url.UTF8String, 132 mimetype.UTF8String, 133 filename.UTF8String); 134 return; 135 } 136 decisionHandler (WKNavigationResponsePolicyAllow); 137 138 self.urlScriptBlocked[navigationResponse.response.URL] = 139 [NSNumber numberWithBool:NO]; 140 if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) 141 { 142 NSDictionary *headers = 143 ((NSHTTPURLResponse *) navigationResponse.response).allHeaderFields; 144 NSString *value = headers[@"Content-Security-Policy"]; 145 if (value) 146 { 147 /* TODO: Sloppy parsing of 'Content-Security-Policy' value. */ 148 NSRange sandbox = [value rangeOfString:@"sandbox"]; 149 if (sandbox.location != NSNotFound 150 && (sandbox.location == 0 151 || [value characterAtIndex:(sandbox.location - 1)] == ' ' 152 || [value characterAtIndex:(sandbox.location - 1)] == ';')) 153 { 154 NSRange allowScripts = [value rangeOfString:@"allow-scripts"]; 155 if (allowScripts.location == NSNotFound 156 || allowScripts.location < sandbox.location) 157 self.urlScriptBlocked[navigationResponse.response.URL] = 158 [NSNumber numberWithBool:YES]; 159 } 160 } 161 } 162} 163 164/* No additional new webview or emacs window will be created 165 for <a ... target="_blank">. */ 166- (WKWebView *)webView:(WKWebView *)webView 167createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration 168 forNavigationAction:(WKNavigationAction *)navigationAction 169 windowFeatures:(WKWindowFeatures *)windowFeatures 170{ 171 if (!navigationAction.targetFrame.isMainFrame) 172 [webView loadRequest:navigationAction.request]; 173 return nil; 174} 175 176/* Open panel for file upload. */ 177- (void)webView:(WKWebView *)webView 178runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters 179initiatedByFrame:(WKFrameInfo *)frame 180completionHandler:(void (^)(NSArray<NSURL *> *URLs))completionHandler 181{ 182 NSOpenPanel *openPanel = [NSOpenPanel openPanel]; 183 openPanel.canChooseFiles = YES; 184 openPanel.canChooseDirectories = NO; 185 openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection; 186 if ([openPanel runModal] == NSModalResponseOK) 187 completionHandler (openPanel.URLs); 188 else 189 completionHandler (nil); 190} 191 192/* By forwarding mouse events to emacs view (frame) 193 - Mouse click in webview selects the window contains the webview. 194 - Correct mouse hand/arrow/I-beam is displayed (TODO: not perfect yet). 195*/ 196 197- (void)mouseDown:(NSEvent *)event 198{ 199 [self.xw->xv->emacswindow mouseDown:event]; 200 [super mouseDown:event]; 201} 202 203- (void)mouseUp:(NSEvent *)event 204{ 205 [self.xw->xv->emacswindow mouseUp:event]; 206 [super mouseUp:event]; 207} 208 209/* Basically we want keyboard events handled by emacs unless an input 210 element has focus. Especially, while incremental search, we set 211 emacs as first responder to avoid focus held in an input element 212 with matching text. */ 213 214- (void)keyDown:(NSEvent *)event 215{ 216 Lisp_Object var = Fintern (build_string ("isearch-mode"), Qnil); 217 Lisp_Object val = buffer_local_value (var, Fcurrent_buffer ()); 218 if (!EQ (val, Qunbound) && !EQ (val, Qnil)) 219 { 220 [self.window makeFirstResponder:self.xw->xv->emacswindow]; 221 [self.xw->xv->emacswindow keyDown:event]; 222 return; 223 } 224 225 /* Emacs handles keyboard events when javascript is blocked. */ 226 if ([self.urlScriptBlocked[self.URL] boolValue]) 227 { 228 [self.xw->xv->emacswindow keyDown:event]; 229 return; 230 } 231 232 [self evaluateJavaScript:@"xwHasFocus()" 233 completionHandler:^(id result, NSError *error) { 234 if (error) 235 { 236 NSLog (@"xwHasFocus: %@", error); 237 [self.xw->xv->emacswindow keyDown:event]; 238 } 239 else if (result) 240 { 241 NSNumber *hasFocus = result; /* __NSCFBoolean */ 242 if (!hasFocus.boolValue) 243 [self.xw->xv->emacswindow keyDown:event]; 244 else 245 [super keyDown:event]; 246 } 247 }]; 248} 249 250- (void)interpretKeyEvents:(NSArray<NSEvent *> *)eventArray 251{ 252 /* We should do nothing and do not forward (default implementation 253 if we not override here) to let emacs collect key events and ask 254 interpretKeyEvents to its superclass. */ 255} 256 257static NSString *xwScript; 258+ (void)initialize 259{ 260 /* Find out if an input element has focus. 261 Message to script message handler when 'C-g' key down. */ 262 if (!xwScript) 263 xwScript = 264 @"function xwHasFocus() {" 265 @" var ae = document.activeElement;" 266 @" if (ae) {" 267 @" var name = ae.nodeName;" 268 @" return name == 'INPUT' || name == 'TEXTAREA';" 269 @" } else {" 270 @" return false;" 271 @" }" 272 @"}" 273 @"function xwKeyDown(event) {" 274 @" if (event.ctrlKey && event.key == 'g') {" 275 @" window.webkit.messageHandlers.keyDown.postMessage('C-g');" 276 @" }" 277 @"}" 278 @"document.addEventListener('keydown', xwKeyDown);" 279 ; 280} 281 282/* Confirming to WKScriptMessageHandler, listens concerning keyDown in 283 webkit. Currently 'C-g'. */ 284- (void)userContentController:(WKUserContentController *)userContentController 285 didReceiveScriptMessage:(WKScriptMessage *)message 286{ 287 if ([message.body isEqualToString:@"C-g"]) 288 { 289 /* Just give up focus, no relay "C-g" to emacs, another "C-g" 290 follows will be handled by emacs. */ 291 [self.window makeFirstResponder:self.xw->xv->emacswindow]; 292 } 293} 294 295@end 296 297/* Xwidget webkit commands. */ 298 299bool 300nsxwidget_is_web_view (struct xwidget *xw) 301{ 302 return xw->xwWidget != NULL && 303 [xw->xwWidget isKindOfClass:WKWebView.class]; 304} 305 306Lisp_Object 307nsxwidget_webkit_uri (struct xwidget *xw) 308{ 309 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 310 return [xwWebView.URL.absoluteString lispString]; 311} 312 313Lisp_Object 314nsxwidget_webkit_title (struct xwidget *xw) 315{ 316 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 317 return [xwWebView.title lispString]; 318} 319 320/* @Note ATS - Need application transport security in 'Info.plist' or 321 remote pages will not loaded. */ 322void 323nsxwidget_webkit_goto_uri (struct xwidget *xw, const char *uri) 324{ 325 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 326 NSString *urlString = [NSString stringWithUTF8String:uri]; 327 NSURL *url = [NSURL URLWithString:urlString]; 328 NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url]; 329 [xwWebView loadRequest:urlRequest]; 330} 331 332void 333nsxwidget_webkit_goto_history (struct xwidget *xw, int rel_pos) 334{ 335 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 336 switch (rel_pos) { 337 case -1: [xwWebView goBack]; break; 338 case 0: [xwWebView reload]; break; 339 case 1: [xwWebView goForward]; break; 340 } 341} 342 343void 344nsxwidget_webkit_zoom (struct xwidget *xw, double zoom_change) 345{ 346 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 347 xwWebView.magnification += zoom_change; 348 /* TODO: setMagnification:centeredAtPoint. */ 349} 350 351/* Recursively convert an objc native type JavaScript value to a Lisp 352 value. Mostly copied from GTK xwidget 'webkit_js_to_lisp'. */ 353static Lisp_Object 354js_to_lisp (id value) 355{ 356 if (value == nil || [value isKindOfClass:NSNull.class]) 357 return Qnil; 358 else if ([value isKindOfClass:NSString.class]) 359 return [(NSString *) value lispString]; 360 else if ([value isKindOfClass:NSNumber.class]) 361 { 362 NSNumber *nsnum = (NSNumber *) value; 363 char type = nsnum.objCType[0]; 364 if (type == 'c') /* __NSCFBoolean has type character 'c'. */ 365 return nsnum.boolValue? Qt : Qnil; 366 else 367 { 368 if (type == 'i' || type == 'l') 369 return make_int (nsnum.longValue); 370 else if (type == 'f' || type == 'd') 371 return make_float (nsnum.doubleValue); 372 /* else fall through. */ 373 } 374 } 375 else if ([value isKindOfClass:NSArray.class]) 376 { 377 NSArray *nsarr = (NSArray *) value; 378 EMACS_INT n = nsarr.count; 379 Lisp_Object obj; 380 struct Lisp_Vector *p = allocate_nil_vector (n); 381 382 for (ptrdiff_t i = 0; i < n; ++i) 383 p->contents[i] = js_to_lisp ([nsarr objectAtIndex:i]); 384 XSETVECTOR (obj, p); 385 return obj; 386 } 387 else if ([value isKindOfClass:NSDictionary.class]) 388 { 389 NSDictionary *nsdict = (NSDictionary *) value; 390 NSArray *keys = nsdict.allKeys; 391 ptrdiff_t n = keys.count; 392 Lisp_Object obj; 393 struct Lisp_Vector *p = allocate_nil_vector (n); 394 395 for (ptrdiff_t i = 0; i < n; ++i) 396 { 397 NSString *prop_key = (NSString *) [keys objectAtIndex:i]; 398 id prop_value = [nsdict valueForKey:prop_key]; 399 p->contents[i] = Fcons ([prop_key lispString], 400 js_to_lisp (prop_value)); 401 } 402 XSETVECTOR (obj, p); 403 return obj; 404 } 405 NSLog (@"Unhandled type in javascript result"); 406 return Qnil; 407} 408 409void 410nsxwidget_webkit_execute_script (struct xwidget *xw, const char *script, 411 Lisp_Object fun) 412{ 413 XwWebView *xwWebView = (XwWebView *) xw->xwWidget; 414 if ([xwWebView.urlScriptBlocked[xwWebView.URL] boolValue]) 415 { 416 message ("Javascript is blocked by 'CSP: sandbox'."); 417 return; 418 } 419 420 NSString *javascriptString = [NSString stringWithUTF8String:script]; 421 [xwWebView evaluateJavaScript:javascriptString 422 completionHandler:^(id result, NSError *error) { 423 if (error) 424 { 425 NSLog (@"evaluateJavaScript error : %@", error.localizedDescription); 426 NSLog (@"error script=%@", javascriptString); 427 } 428 else if (result && FUNCTIONP (fun)) 429 { 430 // NSLog (@"result=%@, type=%@", result, [result class]); 431 Lisp_Object lisp_value = js_to_lisp (result); 432 store_xwidget_js_callback_event (xw, fun, lisp_value); 433 } 434 }]; 435} 436 437/* Window containing an xwidget. */ 438 439@implementation XwWindow 440- (BOOL)isFlipped { return YES; } 441@end 442 443/* Xwidget model, macOS Cocoa part. */ 444 445void 446nsxwidget_init(struct xwidget *xw) 447{ 448 block_input (); 449 NSRect rect = NSMakeRect (0, 0, xw->width, xw->height); 450 xw->xwWidget = [[XwWebView alloc] 451 initWithFrame:rect 452 configuration:[[WKWebViewConfiguration alloc] init] 453 xwidget:xw]; 454 xw->xwWindow = [[XwWindow alloc] 455 initWithFrame:rect]; 456 [xw->xwWindow addSubview:xw->xwWidget]; 457 xw->xv = NULL; /* for 1 to 1 relationship of webkit2. */ 458 unblock_input (); 459} 460 461void 462nsxwidget_kill (struct xwidget *xw) 463{ 464 if (xw) 465 { 466 WKUserContentController *scriptor = 467 ((XwWebView *) xw->xwWidget).configuration.userContentController; 468 [scriptor removeAllUserScripts]; 469 [scriptor removeScriptMessageHandlerForName:@"keyDown"]; 470 [scriptor release]; 471 if (xw->xv) 472 xw->xv->model = Qnil; /* Make sure related view stale. */ 473 474 /* This stops playing audio when a xwidget-webkit buffer is 475 killed. I could not find other solution. */ 476 nsxwidget_webkit_goto_uri (xw, "about:blank"); 477 478 [((XwWebView *) xw->xwWidget).urlScriptBlocked release]; 479 [xw->xwWidget removeFromSuperviewWithoutNeedingDisplay]; 480 [xw->xwWidget release]; 481 [xw->xwWindow removeFromSuperviewWithoutNeedingDisplay]; 482 [xw->xwWindow release]; 483 xw->xwWidget = nil; 484 } 485} 486 487void 488nsxwidget_resize (struct xwidget *xw) 489{ 490 if (xw->xwWidget) 491 { 492 [xw->xwWindow setFrameSize:NSMakeSize(xw->width, xw->height)]; 493 [xw->xwWidget setFrameSize:NSMakeSize(xw->width, xw->height)]; 494 } 495} 496 497Lisp_Object 498nsxwidget_get_size (struct xwidget *xw) 499{ 500 return list2i (xw->xwWidget.frame.size.width, 501 xw->xwWidget.frame.size.height); 502} 503 504/* Xwidget view, macOS Cocoa part. */ 505 506@implementation XvWindow : NSView 507- (BOOL)isFlipped { return YES; } 508@end 509 510void 511nsxwidget_init_view (struct xwidget_view *xv, 512 struct xwidget *xw, 513 struct glyph_string *s, 514 int x, int y) 515{ 516 /* 'x_draw_xwidget_glyph_string' will calculate correct position and 517 size of clip to draw in emacs buffer window. Thus, just begin at 518 origin with no crop. */ 519 xv->x = x; 520 xv->y = y; 521 xv->clip_left = 0; 522 xv->clip_right = xw->width; 523 xv->clip_top = 0; 524 xv->clip_bottom = xw->height; 525 526 xv->xvWindow = [[XvWindow alloc] 527 initWithFrame:NSMakeRect (x, y, xw->width, xw->height)]; 528 xv->xvWindow.xw = xw; 529 xv->xvWindow.xv = xv; 530 531 xw->xv = xv; /* For 1 to 1 relationship of webkit2. */ 532 [xv->xvWindow addSubview:xw->xwWindow]; 533 534 xv->emacswindow = FRAME_NS_VIEW (s->f); 535 [xv->emacswindow addSubview:xv->xvWindow]; 536} 537 538void 539nsxwidget_delete_view (struct xwidget_view *xv) 540{ 541 if (!EQ (xv->model, Qnil)) 542 { 543 struct xwidget *xw = XXWIDGET (xv->model); 544 [xw->xwWindow removeFromSuperviewWithoutNeedingDisplay]; 545 xw->xv = NULL; /* Now model has no view. */ 546 } 547 [xv->xvWindow removeFromSuperviewWithoutNeedingDisplay]; 548 [xv->xvWindow release]; 549} 550 551void 552nsxwidget_show_view (struct xwidget_view *xv) 553{ 554 xv->hidden = NO; 555 [xv->xvWindow setFrameOrigin:NSMakePoint(xv->x + xv->clip_left, 556 xv->y + xv->clip_top)]; 557} 558 559void 560nsxwidget_hide_view (struct xwidget_view *xv) 561{ 562 xv->hidden = YES; 563 [xv->xvWindow setFrameOrigin:NSMakePoint(10000, 10000)]; 564} 565 566void 567nsxwidget_resize_view (struct xwidget_view *xv, int width, int height) 568{ 569 [xv->xvWindow setFrameSize:NSMakeSize(width, height)]; 570} 571 572void 573nsxwidget_move_view (struct xwidget_view *xv, int x, int y) 574{ 575 [xv->xvWindow setFrameOrigin:NSMakePoint (x, y)]; 576} 577 578/* Move model window in container (view window). */ 579void 580nsxwidget_move_widget_in_view (struct xwidget_view *xv, int x, int y) 581{ 582 struct xwidget *xww = xv->xvWindow.xw; 583 [xww->xwWindow setFrameOrigin:NSMakePoint (x, y)]; 584} 585 586void 587nsxwidget_set_needsdisplay (struct xwidget_view *xv) 588{ 589 xv->xvWindow.needsDisplay = YES; 590} 591