• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Event management for WebViewInternal.
6
7var DeclarativeWebRequestSchema =
8    requireNative('schema_registry').GetSchema('declarativeWebRequest');
9var EventBindings = require('event_bindings');
10var IdGenerator = requireNative('id_generator');
11var MessagingNatives = requireNative('messaging_natives');
12var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
13var WebRequestSchema =
14    requireNative('schema_registry').GetSchema('webRequest');
15var WebView = require('webview').WebView;
16
17var CreateEvent = function(name) {
18  var eventOpts = {supportsListeners: true, supportsFilters: true};
19  return new EventBindings.Event(name, undefined, eventOpts);
20};
21
22var FrameNameChangedEvent = CreateEvent('webview.onFrameNameChanged');
23var WebRequestMessageEvent = CreateEvent('webview.onMessage');
24
25// WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
26//     associated extension event descriptor objects.
27// An event listener will be attached to the extension event |evt| specified in
28//     the descriptor.
29// |fields| specifies the public-facing fields in the DOM event that are
30//     accessible to <webview> developers.
31// |customHandler| allows a handler function to be called each time an extension
32//     event is caught by its event listener. The DOM event should be dispatched
33//     within this handler function. With no handler function, the DOM event
34//     will be dispatched by default each time the extension event is caught.
35// |cancelable| (default: false) specifies whether the event's default
36//     behavior can be canceled. If the default action associated with the event
37//     is prevented, then its dispatch function will return false in its event
38//     handler. The event must have a custom handler for this to be meaningful.
39var WEB_VIEW_EVENTS = {
40  'close': {
41    evt: CreateEvent('webview.onClose'),
42    fields: []
43  },
44  'consolemessage': {
45    evt: CreateEvent('webview.onConsoleMessage'),
46    fields: ['level', 'message', 'line', 'sourceId']
47  },
48  'contentload': {
49    evt: CreateEvent('webview.onContentLoad'),
50    fields: []
51  },
52  'contextmenu': {
53    evt: CreateEvent('webview.contextmenu'),
54    cancelable: true,
55    customHandler: function(handler, event, webViewEvent) {
56      handler.handleContextMenu(event, webViewEvent);
57    },
58    fields: ['items']
59  },
60  'dialog': {
61    cancelable: true,
62    customHandler: function(handler, event, webViewEvent) {
63      handler.handleDialogEvent(event, webViewEvent);
64    },
65    evt: CreateEvent('webview.onDialog'),
66    fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
67  },
68  'exit': {
69     evt: CreateEvent('webview.onExit'),
70     fields: ['processId', 'reason']
71  },
72  'loadabort': {
73    cancelable: true,
74    customHandler: function(handler, event, webViewEvent) {
75      handler.handleLoadAbortEvent(event, webViewEvent);
76    },
77    evt: CreateEvent('webview.onLoadAbort'),
78    fields: ['url', 'isTopLevel', 'reason']
79  },
80  'loadcommit': {
81    customHandler: function(handler, event, webViewEvent) {
82      handler.handleLoadCommitEvent(event, webViewEvent);
83    },
84    evt: CreateEvent('webview.onLoadCommit'),
85    fields: ['url', 'isTopLevel']
86  },
87  'loadprogress': {
88    evt: CreateEvent('webview.onLoadProgress'),
89    fields: ['url', 'progress']
90  },
91  'loadredirect': {
92    evt: CreateEvent('webview.onLoadRedirect'),
93    fields: ['isTopLevel', 'oldUrl', 'newUrl']
94  },
95  'loadstart': {
96    evt: CreateEvent('webview.onLoadStart'),
97    fields: ['url', 'isTopLevel']
98  },
99  'loadstop': {
100    evt: CreateEvent('webview.onLoadStop'),
101    fields: []
102  },
103  'newwindow': {
104    cancelable: true,
105    customHandler: function(handler, event, webViewEvent) {
106      handler.handleNewWindowEvent(event, webViewEvent);
107    },
108    evt: CreateEvent('webview.onNewWindow'),
109    fields: [
110      'initialHeight',
111      'initialWidth',
112      'targetUrl',
113      'windowOpenDisposition',
114      'name'
115    ]
116  },
117  'permissionrequest': {
118    cancelable: true,
119    customHandler: function(handler, event, webViewEvent) {
120      handler.handlePermissionEvent(event, webViewEvent);
121    },
122    evt: CreateEvent('webview.onPermissionRequest'),
123    fields: [
124      'identifier',
125      'lastUnlockedBySelf',
126      'name',
127      'permission',
128      'requestMethod',
129      'url',
130      'userGesture'
131    ]
132  },
133  'responsive': {
134    evt: CreateEvent('webview.onResponsive'),
135    fields: ['processId']
136  },
137  'sizechanged': {
138    evt: CreateEvent('webview.onSizeChanged'),
139    customHandler: function(handler, event, webViewEvent) {
140      handler.handleSizeChangedEvent(event, webViewEvent);
141    },
142    fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
143  },
144  'unresponsive': {
145    evt: CreateEvent('webview.onUnresponsive'),
146    fields: ['processId']
147  }
148};
149
150function DeclarativeWebRequestEvent(opt_eventName,
151                                    opt_argSchemas,
152                                    opt_eventOptions,
153                                    opt_webViewInstanceId) {
154  var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
155  EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
156      opt_webViewInstanceId);
157
158  var self = this;
159  // TODO(lazyboy): When do we dispose this listener?
160  WebRequestMessageEvent.addListener(function() {
161    // Re-dispatch to subEvent's listeners.
162    $Function.apply(self.dispatch, self, $Array.slice(arguments));
163  }, {instanceId: opt_webViewInstanceId || 0});
164}
165
166DeclarativeWebRequestEvent.prototype = {
167  __proto__: EventBindings.Event.prototype
168};
169
170// Constructor.
171function WebViewEvents(webViewInternal, viewInstanceId) {
172  this.webViewInternal = webViewInternal;
173  this.viewInstanceId = viewInstanceId;
174  this.setup();
175}
176
177// Sets up events.
178WebViewEvents.prototype.setup = function() {
179  this.setupFrameNameChangedEvent();
180  this.setupWebRequestEvents();
181  this.webViewInternal.setupExperimentalContextMenus();
182
183  var events = this.getEvents();
184  for (var eventName in events) {
185    this.setupEvent(eventName, events[eventName]);
186  }
187};
188
189WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
190  var self = this;
191  FrameNameChangedEvent.addListener(function(e) {
192    self.webViewInternal.onFrameNameChanged(e.name);
193  }, {instanceId: self.viewInstanceId});
194};
195
196WebViewEvents.prototype.setupWebRequestEvents = function() {
197  var self = this;
198  var request = {};
199  var createWebRequestEvent = function(webRequestEvent) {
200    return function() {
201      if (!self[webRequestEvent.name]) {
202        self[webRequestEvent.name] =
203            new WebRequestEvent(
204                'webview.' + webRequestEvent.name,
205                webRequestEvent.parameters,
206                webRequestEvent.extraParameters, webRequestEvent.options,
207                self.viewInstanceId);
208      }
209      return self[webRequestEvent.name];
210    };
211  };
212
213  var createDeclarativeWebRequestEvent = function(webRequestEvent) {
214    return function() {
215      if (!self[webRequestEvent.name]) {
216        // The onMessage event gets a special event type because we want
217        // the listener to fire only for messages targeted for this particular
218        // <webview>.
219        var EventClass = webRequestEvent.name === 'onMessage' ?
220            DeclarativeWebRequestEvent : EventBindings.Event;
221        self[webRequestEvent.name] =
222            new EventClass(
223                'webview.' + webRequestEvent.name,
224                webRequestEvent.parameters,
225                webRequestEvent.options,
226                self.viewInstanceId);
227      }
228      return self[webRequestEvent.name];
229    };
230  };
231
232  for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
233    var eventSchema = DeclarativeWebRequestSchema.events[i];
234    var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
235    Object.defineProperty(
236        request,
237        eventSchema.name,
238        {
239          get: webRequestEvent,
240          enumerable: true
241        }
242    );
243  }
244
245  // Populate the WebRequest events from the API definition.
246  for (var i = 0; i < WebRequestSchema.events.length; ++i) {
247    var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
248    Object.defineProperty(
249        request,
250        WebRequestSchema.events[i].name,
251        {
252          get: webRequestEvent,
253          enumerable: true
254        }
255    );
256  }
257
258  this.webViewInternal.setRequestPropertyOnWebViewNode(request);
259};
260
261WebViewEvents.prototype.getEvents = function() {
262  var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
263  for (var eventName in experimentalEvents) {
264    WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
265  }
266  return WEB_VIEW_EVENTS;
267};
268
269WebViewEvents.prototype.setupEvent = function(name, info) {
270  var self = this;
271  info.evt.addListener(function(e) {
272    var details = {bubbles:true};
273    if (info.cancelable)
274      details.cancelable = true;
275    var webViewEvent = new Event(name, details);
276    $Array.forEach(info.fields, function(field) {
277      if (e[field] !== undefined) {
278        webViewEvent[field] = e[field];
279      }
280    });
281    if (info.customHandler) {
282      info.customHandler(self, e, webViewEvent);
283      return;
284    }
285    self.webViewInternal.dispatchEvent(webViewEvent);
286  }, {instanceId: self.viewInstanceId});
287
288  this.webViewInternal.setupEventProperty(name);
289};
290
291
292// Event handlers.
293WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
294  this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
295};
296
297WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
298  var showWarningMessage = function(dialogType) {
299    var VOWELS = ['a', 'e', 'i', 'o', 'u'];
300    var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
301    var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
302    var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
303    output = output.replace('%2', dialogType);
304    window.console.warn(output);
305  };
306
307  var self = this;
308  var requestId = event.requestId;
309  var actionTaken = false;
310
311  var validateCall = function() {
312    var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
313        'An action has already been taken for this "dialog" event.';
314
315    if (actionTaken) {
316      throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
317    }
318    actionTaken = true;
319  };
320
321  var getInstanceId = function() {
322    return self.webViewInternal.getInstanceId();
323  };
324
325  var dialog = {
326    ok: function(user_input) {
327      validateCall();
328      user_input = user_input || '';
329      WebView.setPermission(getInstanceId(), requestId, 'allow', user_input);
330    },
331    cancel: function() {
332      validateCall();
333      WebView.setPermission(getInstanceId(), requestId, 'deny');
334    }
335  };
336  webViewEvent.dialog = dialog;
337
338  var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
339  if (actionTaken) {
340    return;
341  }
342
343  if (defaultPrevented) {
344    // Tell the JavaScript garbage collector to track lifetime of |dialog| and
345    // call back when the dialog object has been collected.
346    MessagingNatives.BindToGC(dialog, function() {
347      // Avoid showing a warning message if the decision has already been made.
348      if (actionTaken) {
349        return;
350      }
351      WebView.setPermission(
352          getInstanceId(), requestId, 'default', '', function(allowed) {
353        if (allowed) {
354          return;
355        }
356        showWarningMessage(event.messageType);
357      });
358    });
359  } else {
360    actionTaken = true;
361    // The default action is equivalent to canceling the dialog.
362    WebView.setPermission(
363        getInstanceId(), requestId, 'default', '', function(allowed) {
364      if (allowed) {
365        return;
366      }
367      showWarningMessage(event.messageType);
368    });
369  }
370};
371
372WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
373  var showWarningMessage = function(reason) {
374    var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
375        'The load has aborted with reason "%1".';
376    window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
377  };
378  if (this.webViewInternal.dispatchEvent(webViewEvent)) {
379    showWarningMessage(event.reason);
380  }
381};
382
383WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
384  this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
385                                    event.processId, event.url,
386                                    event.isTopLevel);
387  this.webViewInternal.dispatchEvent(webViewEvent);
388};
389
390WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
391  var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
392      'An action has already been taken for this "newwindow" event.';
393
394  var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
395      'Unable to attach the new window to the provided webview.';
396
397  var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
398
399  var showWarningMessage = function() {
400    var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
401    window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
402  };
403
404  var requestId = event.requestId;
405  var actionTaken = false;
406  var self = this;
407  var getInstanceId = function() {
408    return self.webViewInternal.getInstanceId();
409  };
410
411  var validateCall = function () {
412    if (actionTaken) {
413      throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
414    }
415    actionTaken = true;
416  };
417
418  var windowObj = {
419    attach: function(webview) {
420      validateCall();
421      if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
422        throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
423      // Attach happens asynchronously to give the tagWatcher an opportunity
424      // to pick up the new webview before attach operates on it, if it hasn't
425      // been attached to the DOM already.
426      // Note: Any subsequent errors cannot be exceptions because they happen
427      // asynchronously.
428      setTimeout(function() {
429        var webViewInternal = privates(webview).internal;
430        // Update the partition.
431        if (event.storagePartitionId) {
432          webViewInternal.onAttach(event.storagePartitionId);
433        }
434
435        var attached = webViewInternal.attachWindow(event.windowId, true);
436
437        if (!attached) {
438          window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
439        }
440        // If the object being passed into attach is not a valid <webview>
441        // then we will fail and it will be treated as if the new window
442        // was rejected. The permission API plumbing is used here to clean
443        // up the state created for the new window if attaching fails.
444        WebView.setPermission(
445            getInstanceId(), requestId, attached ? 'allow' : 'deny');
446      }, 0);
447    },
448    discard: function() {
449      validateCall();
450      WebView.setPermission(getInstanceId(), requestId, 'deny');
451    }
452  };
453  webViewEvent.window = windowObj;
454
455  var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
456  if (actionTaken) {
457    return;
458  }
459
460  if (defaultPrevented) {
461    // Make browser plugin track lifetime of |windowObj|.
462    MessagingNatives.BindToGC(windowObj, function() {
463      // Avoid showing a warning message if the decision has already been made.
464      if (actionTaken) {
465        return;
466      }
467      WebView.setPermission(
468          getInstanceId(), requestId, 'default', '', function(allowed) {
469        if (allowed) {
470          return;
471        }
472        showWarningMessage();
473      });
474    });
475  } else {
476    actionTaken = true;
477    // The default action is to discard the window.
478    WebView.setPermission(
479        getInstanceId(), requestId, 'default', '', function(allowed) {
480      if (allowed) {
481        return;
482      }
483      showWarningMessage();
484    });
485  }
486};
487
488WebViewEvents.prototype.getPermissionTypes = function() {
489  var permissions =
490      ['media',
491      'geolocation',
492      'pointerLock',
493      'download',
494      'loadplugin',
495      'filesystem'];
496  return permissions.concat(
497      this.webViewInternal.maybeGetExperimentalPermissions());
498};
499
500WebViewEvents.prototype.handlePermissionEvent =
501    function(event, webViewEvent) {
502  var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
503      'Permission has already been decided for this "permissionrequest" event.';
504
505  var showWarningMessage = function(permission) {
506    var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
507        'The permission request for "%1" has been denied.';
508    window.console.warn(
509        WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
510  };
511
512  var requestId = event.requestId;
513  var self = this;
514  var getInstanceId = function() {
515    return self.webViewInternal.getInstanceId();
516  };
517
518  if (this.getPermissionTypes().indexOf(event.permission) < 0) {
519    // The permission type is not allowed. Trigger the default response.
520    WebView.setPermission(
521        getInstanceId(), requestId, 'default', '', function(allowed) {
522      if (allowed) {
523        return;
524      }
525      showWarningMessage(event.permission);
526    });
527    return;
528  }
529
530  var decisionMade = false;
531  var validateCall = function() {
532    if (decisionMade) {
533      throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
534    }
535    decisionMade = true;
536  };
537
538  // Construct the event.request object.
539  var request = {
540    allow: function() {
541      validateCall();
542      WebView.setPermission(getInstanceId(), requestId, 'allow');
543    },
544    deny: function() {
545      validateCall();
546      WebView.setPermission(getInstanceId(), requestId, 'deny');
547    }
548  };
549  webViewEvent.request = request;
550
551  var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
552  if (decisionMade) {
553    return;
554  }
555
556  if (defaultPrevented) {
557    // Make browser plugin track lifetime of |request|.
558    MessagingNatives.BindToGC(request, function() {
559      // Avoid showing a warning message if the decision has already been made.
560      if (decisionMade) {
561        return;
562      }
563      WebView.setPermission(
564          getInstanceId(), requestId, 'default', '', function(allowed) {
565        if (allowed) {
566          return;
567        }
568        showWarningMessage(event.permission);
569      });
570    });
571  } else {
572    decisionMade = true;
573    WebView.setPermission(
574        getInstanceId(), requestId, 'default', '', function(allowed) {
575      if (allowed) {
576        return;
577      }
578      showWarningMessage(event.permission);
579    });
580  }
581};
582
583WebViewEvents.prototype.handleSizeChangedEvent = function(
584    event, webViewEvent) {
585  this.webViewInternal.onSizeChanged(webViewEvent.newWidth,
586                                     webViewEvent.newHeight);
587  this.webViewInternal.dispatchEvent(webViewEvent);
588};
589
590exports.WebViewEvents = WebViewEvents;
591exports.CreateEvent = CreateEvent;
592