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