Home | History | Annotate | Download | only in extensions
      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 
      7 var DeclarativeWebRequestSchema =
      8     requireNative('schema_registry').GetSchema('declarativeWebRequest');
      9 var EventBindings = require('event_bindings');
     10 var IdGenerator = requireNative('id_generator');
     11 var MessagingNatives = requireNative('messaging_natives');
     12 var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
     13 var WebRequestSchema =
     14     requireNative('schema_registry').GetSchema('webRequest');
     15 var WebView = require('webview').WebView;
     16 
     17 var CreateEvent = function(name) {
     18   var eventOpts = {supportsListeners: true, supportsFilters: true};
     19   return new EventBindings.Event(name, undefined, eventOpts);
     20 };
     21 
     22 var FrameNameChangedEvent = CreateEvent('webview.onFrameNameChanged');
     23 var 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.
     39 var 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 
    150 function 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 
    166 DeclarativeWebRequestEvent.prototype = {
    167   __proto__: EventBindings.Event.prototype
    168 };
    169 
    170 // Constructor.
    171 function WebViewEvents(webViewInternal, viewInstanceId) {
    172   this.webViewInternal = webViewInternal;
    173   this.viewInstanceId = viewInstanceId;
    174   this.setup();
    175 }
    176 
    177 // Sets up events.
    178 WebViewEvents.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 
    189 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
    190   var self = this;
    191   FrameNameChangedEvent.addListener(function(e) {
    192     self.webViewInternal.onFrameNameChanged(e.name);
    193   }, {instanceId: self.viewInstanceId});
    194 };
    195 
    196 WebViewEvents.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 
    261 WebViewEvents.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 
    269 WebViewEvents.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.
    293 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
    294   this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
    295 };
    296 
    297 WebViewEvents.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 
    372 WebViewEvents.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 
    383 WebViewEvents.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 
    390 WebViewEvents.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 
    488 WebViewEvents.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 
    500 WebViewEvents.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 
    583 WebViewEvents.prototype.handleSizeChangedEvent = function(
    584     event, webViewEvent) {
    585   this.webViewInternal.onSizeChanged(webViewEvent.newWidth,
    586                                      webViewEvent.newHeight);
    587   this.webViewInternal.dispatchEvent(webViewEvent);
    588 };
    589 
    590 exports.WebViewEvents = WebViewEvents;
    591 exports.CreateEvent = CreateEvent;
    592