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