Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 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 // Shim that simulates a <webview> tag via Mutation Observers.
      6 //
      7 // The actual tag is implemented via the browser plugin. The internals of this
      8 // are hidden via Shadow DOM.
      9 
     10 var addTagWatcher = require('tagWatcher').addTagWatcher;
     11 var eventBindings = require('event_bindings');
     12 
     13 /** @type {Array.<string>} */
     14 var WEB_VIEW_ATTRIBUTES = ['name', 'src', 'partition', 'autosize', 'minheight',
     15     'minwidth', 'maxheight', 'maxwidth'];
     16 
     17 var WEB_VIEW_EVENTS = {
     18   'sizechanged': ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'],
     19 };
     20 
     21 var webViewInstanceIdCounter = 0;
     22 
     23 var createEvent = function(name) {
     24   var eventOpts = {supportsListeners: true, supportsFilters: true};
     25   return new eventBindings.Event(name, undefined, eventOpts);
     26 };
     27 
     28 var WEB_VIEW_EXT_EVENTS = {
     29   'close': {
     30     evt: createEvent('webview.onClose'),
     31     fields: []
     32   },
     33   'consolemessage': {
     34     evt: createEvent('webview.onConsoleMessage'),
     35     fields: ['level', 'message', 'line', 'sourceId']
     36   },
     37   'contentload': {
     38     evt: createEvent('webview.onContentLoad'),
     39     fields: []
     40   },
     41   'exit': {
     42      evt: createEvent('webview.onExit'),
     43      fields: ['processId', 'reason']
     44   },
     45   'loadabort': {
     46     evt: createEvent('webview.onLoadAbort'),
     47     fields: ['url', 'isTopLevel', 'reason']
     48   },
     49   'loadcommit': {
     50     customHandler: function(webview, event, webviewEvent) {
     51       webview.currentEntryIndex_ = event.currentEntryIndex;
     52       webview.entryCount_ = event.entryCount;
     53       webview.processId_ = event.processId;
     54       if (event.isTopLevel) {
     55         webview.browserPluginNode_.setAttribute('src', event.url);
     56       }
     57       webview.webviewNode_.dispatchEvent(webviewEvent);
     58     },
     59     evt: createEvent('webview.onLoadCommit'),
     60     fields: ['url', 'isTopLevel']
     61   },
     62   'loadredirect': {
     63     evt: createEvent('webview.onLoadRedirect'),
     64     fields: ['isTopLevel', 'oldUrl', 'newUrl']
     65   },
     66   'loadstart': {
     67     evt: createEvent('webview.onLoadStart'),
     68     fields: ['url', 'isTopLevel']
     69   },
     70   'loadstop': {
     71     evt: createEvent('webview.onLoadStop'),
     72     fields: []
     73   },
     74   'newwindow': {
     75     cancelable: true,
     76     customHandler: function(webview, event, webviewEvent) {
     77       webview.setupExtNewWindowEvent_(event, webviewEvent);
     78     },
     79     evt: createEvent('webview.onNewWindow'),
     80     fields: [
     81       'initialHeight',
     82       'initialWidth',
     83       'targetUrl',
     84       'windowOpenDisposition',
     85       'name'
     86     ]
     87   },
     88   'permissionrequest': {
     89     cancelable: true,
     90     customHandler: function(webview, event, webviewEvent) {
     91       webview.setupExtPermissionEvent_(event, webviewEvent);
     92     },
     93     evt: createEvent('webview.onPermissionRequest'),
     94     fields: [
     95       'lastUnlockedBySelf',
     96       'permission',
     97       'requestMethod',
     98       'url',
     99       'userGesture'
    100     ]
    101   },
    102   'responsive': {
    103     evt: createEvent('webview.onResponsive'),
    104     fields: ['processId']
    105   },
    106   'unresponsive': {
    107     evt: createEvent('webview.onUnresponsive'),
    108     fields: ['processId']
    109   }
    110 };
    111 
    112 addTagWatcher('WEBVIEW', function(addedNode) { new WebView(addedNode); });
    113 
    114 /** @type {number} */
    115 WebView.prototype.entryCount_;
    116 
    117 /** @type {number} */
    118 WebView.prototype.currentEntryIndex_;
    119 
    120 /** @type {number} */
    121 WebView.prototype.processId_;
    122 
    123 /**
    124  * @constructor
    125  */
    126 function WebView(webviewNode) {
    127   this.webviewNode_ = webviewNode;
    128   this.browserPluginNode_ = this.createBrowserPluginNode_();
    129   var shadowRoot = this.webviewNode_.webkitCreateShadowRoot();
    130   shadowRoot.appendChild(this.browserPluginNode_);
    131 
    132   this.setupFocusPropagation_();
    133   this.setupWebviewNodeMethods_();
    134   this.setupWebviewNodeProperties_();
    135   this.setupWebviewNodeAttributes_();
    136   this.setupWebviewNodeEvents_();
    137 
    138   // Experimental API
    139   this.maybeSetupExperimentalAPI_();
    140 }
    141 
    142 /**
    143  * @private
    144  */
    145 WebView.prototype.createBrowserPluginNode_ = function() {
    146   var browserPluginNode = document.createElement('object');
    147   browserPluginNode.type = 'application/browser-plugin';
    148   // The <object> node fills in the <webview> container.
    149   browserPluginNode.style.width = '100%';
    150   browserPluginNode.style.height = '100%';
    151   $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
    152     // Only copy attributes that have been assigned values, rather than copying
    153     // a series of undefined attributes to BrowserPlugin.
    154     if (this.webviewNode_.hasAttribute(attributeName)) {
    155       browserPluginNode.setAttribute(
    156         attributeName, this.webviewNode_.getAttribute(attributeName));
    157     } else if (this.webviewNode_[attributeName]){
    158       // Reading property using has/getAttribute does not work on
    159       // document.DOMContentLoaded event (but works on
    160       // window.DOMContentLoaded event).
    161       // So copy from property if copying from attribute fails.
    162       browserPluginNode.setAttribute(
    163         attributeName, this.webviewNode_[attributeName]);
    164     }
    165   }, this);
    166 
    167   return browserPluginNode;
    168 };
    169 
    170 /**
    171  * @private
    172  */
    173 WebView.prototype.setupFocusPropagation_ = function() {
    174   if (!this.webviewNode_.hasAttribute('tabIndex')) {
    175     // <webview> needs a tabIndex in order to respond to keyboard focus.
    176     // TODO(fsamuel): This introduces unexpected tab ordering. We need to find
    177     // a way to take keyboard focus without messing with tab ordering.
    178     // See http://crbug.com/231664.
    179     this.webviewNode_.setAttribute('tabIndex', 0);
    180   }
    181   var self = this;
    182   this.webviewNode_.addEventListener('focus', function(e) {
    183     // Focus the BrowserPlugin when the <webview> takes focus.
    184     self.browserPluginNode_.focus();
    185   });
    186   this.webviewNode_.addEventListener('blur', function(e) {
    187     // Blur the BrowserPlugin when the <webview> loses focus.
    188     self.browserPluginNode_.blur();
    189   });
    190 };
    191 
    192 /**
    193  * @private
    194  */
    195 WebView.prototype.setupWebviewNodeMethods_ = function() {
    196   // this.browserPluginNode_[apiMethod] are not necessarily defined immediately
    197   // after the shadow object is appended to the shadow root.
    198   var webviewNode = this.webviewNode_;
    199   var browserPluginNode = this.browserPluginNode_;
    200   var self = this;
    201 
    202   webviewNode['canGoBack'] = function() {
    203     return self.entryCount_ > 1 && self.currentEntryIndex_ > 0;
    204   };
    205 
    206   webviewNode['canGoForward'] = function() {
    207     return self.currentEntryIndex_ >=0 &&
    208         self.currentEntryIndex_ < (self.entryCount_ - 1);
    209   };
    210 
    211   webviewNode['back'] = function() {
    212     webviewNode.go(-1);
    213   };
    214 
    215   webviewNode['forward'] = function() {
    216     webviewNode.go(1);
    217   };
    218 
    219   webviewNode['getProcessId'] = function() {
    220     return self.processId_;
    221   };
    222 
    223   webviewNode['go'] = function(relativeIndex) {
    224     var instanceId = browserPluginNode.getGuestInstanceId();
    225     if (!instanceId) {
    226       return;
    227     }
    228     chrome.webview.go(instanceId, relativeIndex);
    229   };
    230 
    231   webviewNode['reload'] = function() {
    232     var instanceId = browserPluginNode.getGuestInstanceId();
    233     if (!instanceId) {
    234       return;
    235     }
    236     chrome.webview.reload(instanceId);
    237   };
    238 
    239   webviewNode['stop'] = function() {
    240     var instanceId = browserPluginNode.getGuestInstanceId();
    241     if (!instanceId) {
    242       return;
    243     }
    244     chrome.webview.stop(instanceId);
    245   };
    246 
    247   webviewNode['terminate'] = function() {
    248     var instanceId = browserPluginNode.getGuestInstanceId();
    249     if (!instanceId) {
    250       return;
    251     }
    252     chrome.webview.terminate(instanceId);
    253   };
    254 
    255   this.setupExecuteCodeAPI_();
    256 };
    257 
    258 /**
    259  * @private
    260  */
    261 WebView.prototype.setupWebviewNodeProperties_ = function() {
    262   var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
    263     'contentWindow is not available at this time. It will become available ' +
    264         'when the page has finished loading.';
    265 
    266   var browserPluginNode = this.browserPluginNode_;
    267   // Expose getters and setters for the attributes.
    268   $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
    269     Object.defineProperty(this.webviewNode_, attributeName, {
    270       get: function() {
    271         return browserPluginNode[attributeName];
    272       },
    273       set: function(value) {
    274         browserPluginNode[attributeName] = value;
    275       },
    276       enumerable: true
    277     });
    278   }, this);
    279 
    280   // We cannot use {writable: true} property descriptor because we want dynamic
    281   // getter value.
    282   Object.defineProperty(this.webviewNode_, 'contentWindow', {
    283     get: function() {
    284       if (browserPluginNode.contentWindow)
    285         return browserPluginNode.contentWindow;
    286       console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
    287     },
    288     // No setter.
    289     enumerable: true
    290   });
    291 };
    292 
    293 /**
    294  * @private
    295  */
    296 WebView.prototype.setupWebviewNodeAttributes_ = function() {
    297   this.setupWebviewNodeObservers_();
    298   this.setupBrowserPluginNodeObservers_();
    299 };
    300 
    301 /**
    302  * @private
    303  */
    304 WebView.prototype.setupWebviewNodeObservers_ = function() {
    305   // Map attribute modifications on the <webview> tag to property changes in
    306   // the underlying <object> node.
    307   var handleMutation = $Function.bind(function(mutation) {
    308     this.handleWebviewAttributeMutation_(mutation);
    309   }, this);
    310   var observer = new MutationObserver(function(mutations) {
    311     $Array.forEach(mutations, handleMutation);
    312   });
    313   observer.observe(
    314       this.webviewNode_,
    315       {attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
    316 };
    317 
    318 /**
    319  * @private
    320  */
    321 WebView.prototype.setupBrowserPluginNodeObservers_ = function() {
    322   var handleMutation = $Function.bind(function(mutation) {
    323     this.handleBrowserPluginAttributeMutation_(mutation);
    324   }, this);
    325   var objectObserver = new MutationObserver(function(mutations) {
    326     $Array.forEach(mutations, handleMutation);
    327   });
    328   objectObserver.observe(
    329       this.browserPluginNode_,
    330       {attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
    331 };
    332 
    333 /**
    334  * @private
    335  */
    336 WebView.prototype.handleWebviewAttributeMutation_ = function(mutation) {
    337   // This observer monitors mutations to attributes of the <webview> and
    338   // updates the BrowserPlugin properties accordingly. In turn, updating
    339   // a BrowserPlugin property will update the corresponding BrowserPlugin
    340   // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
    341   // details.
    342   this.browserPluginNode_[mutation.attributeName] =
    343       this.webviewNode_.getAttribute(mutation.attributeName);
    344 };
    345 
    346 /**
    347  * @private
    348  */
    349 WebView.prototype.handleBrowserPluginAttributeMutation_ = function(mutation) {
    350   // This observer monitors mutations to attributes of the BrowserPlugin and
    351   // updates the <webview> attributes accordingly.
    352   if (!this.browserPluginNode_.hasAttribute(mutation.attributeName)) {
    353     // If an attribute is removed from the BrowserPlugin, then remove it
    354     // from the <webview> as well.
    355     this.webviewNode_.removeAttribute(mutation.attributeName);
    356   } else {
    357     // Update the <webview> attribute to match the BrowserPlugin attribute.
    358     // Note: Calling setAttribute on <webview> will trigger its mutation
    359     // observer which will then propagate that attribute to BrowserPlugin. In
    360     // cases where we permit assigning a BrowserPlugin attribute the same value
    361     // again (such as navigation when crashed), this could end up in an infinite
    362     // loop. Thus, we avoid this loop by only updating the <webview> attribute
    363     // if the BrowserPlugin attributes differs from it.
    364     var oldValue = this.webviewNode_.getAttribute(mutation.attributeName);
    365     var newValue = this.browserPluginNode_.getAttribute(mutation.attributeName);
    366     if (newValue != oldValue) {
    367       this.webviewNode_.setAttribute(mutation.attributeName, newValue);
    368     }
    369   }
    370 };
    371 
    372 /**
    373  * @private
    374  */
    375 WebView.prototype.getWebviewExtEvents_ = function() {
    376   var experimentalExtEvents = this.maybeGetWebviewExperimentalExtEvents_();
    377   for (var eventName in experimentalExtEvents) {
    378     WEB_VIEW_EXT_EVENTS[eventName] = experimentalExtEvents[eventName];
    379   }
    380   return WEB_VIEW_EXT_EVENTS;
    381 };
    382 
    383 /**
    384  * @private
    385  */
    386 WebView.prototype.setupWebviewNodeEvents_ = function() {
    387   var self = this;
    388   this.viewInstanceId_ = ++webViewInstanceIdCounter;
    389   var onInstanceIdAllocated = function(e) {
    390     var detail = e.detail ? JSON.parse(e.detail) : {};
    391     self.instanceId_ = detail.windowId;
    392     var params = {
    393       'api': 'webview',
    394       'instanceId': self.viewInstanceId_
    395     };
    396     self.browserPluginNode_['-internal-attach'](params);
    397 
    398     var extEvents = self.getWebviewExtEvents_();
    399     for (var eventName in extEvents) {
    400       self.setupExtEvent_(eventName, extEvents[eventName]);
    401     }
    402   };
    403   this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
    404                                            onInstanceIdAllocated);
    405 
    406   for (var eventName in WEB_VIEW_EVENTS) {
    407     this.setupEvent_(eventName, WEB_VIEW_EVENTS[eventName]);
    408   }
    409 };
    410 
    411 /**
    412  * @private
    413  */
    414 WebView.prototype.setupExtEvent_ = function(eventName, eventInfo) {
    415   var self = this;
    416   var webviewNode = this.webviewNode_;
    417   eventInfo.evt.addListener(function(event) {
    418     var details = {bubbles:true};
    419     if (eventInfo.cancelable)
    420       details.cancelable = true;
    421     var webviewEvent = new Event(eventName, details);
    422     $Array.forEach(eventInfo.fields, function(field) {
    423       if (event[field] !== undefined) {
    424         webviewEvent[field] = event[field];
    425       }
    426     });
    427     if (eventInfo.customHandler) {
    428       eventInfo.customHandler(self, event, webviewEvent);
    429       return;
    430     }
    431     webviewNode.dispatchEvent(webviewEvent);
    432   }, {instanceId: self.instanceId_});
    433 };
    434 
    435 /**
    436  * @private
    437  */
    438 WebView.prototype.setupEvent_ = function(eventName, attribs) {
    439   var webviewNode = this.webviewNode_;
    440   var internalname = '-internal-' + eventName;
    441   this.browserPluginNode_.addEventListener(internalname, function(e) {
    442     var evt = new Event(eventName, { bubbles: true });
    443     var detail = e.detail ? JSON.parse(e.detail) : {};
    444     $Array.forEach(attribs, function(attribName) {
    445       evt[attribName] = detail[attribName];
    446     });
    447     webviewNode.dispatchEvent(evt);
    448   });
    449 };
    450 
    451 /**
    452  * @private
    453  */
    454 WebView.prototype.setupExtNewWindowEvent_ = function(event, webviewEvent) {
    455   var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
    456       'An action has already been taken for this "newwindow" event.';
    457 
    458   var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
    459       'Unable to attach the new window to the provided webview.';
    460 
    461   var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
    462 
    463   var showWarningMessage = function() {
    464     var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
    465     console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
    466   };
    467 
    468   var self = this;
    469   var browserPluginNode = this.browserPluginNode_;
    470   var webviewNode = this.webviewNode_;
    471 
    472   var requestId = event.requestId;
    473   var actionTaken = false;
    474 
    475   var onTrackedObjectGone = function(requestId, e) {
    476     var detail = e.detail ? JSON.parse(e.detail) : {};
    477     if (detail.id != requestId) {
    478       return;
    479     }
    480 
    481     // Avoid showing a warning message if the decision has already been made.
    482     if (actionTaken) {
    483       return;
    484     }
    485 
    486     chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    487     showWarningMessage();
    488   };
    489 
    490 
    491   var validateCall = function () {
    492     if (actionTaken) {
    493       throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
    494     }
    495     actionTaken = true;
    496   };
    497 
    498   var window = {
    499     attach: function(webview) {
    500       validateCall();
    501       if (!webview)
    502         throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
    503       // Attach happens asynchronously to give the tagWatcher an opportunity
    504       // to pick up the new webview before attach operates on it, if it hasn't
    505       // been attached to the DOM already.
    506       // Note: Any subsequent errors cannot be exceptions because they happen
    507       // asynchronously.
    508       setTimeout(function() {
    509         var attached =
    510             browserPluginNode['-internal-attachWindowTo'](webview,
    511                                                           event.windowId);
    512         if (!attached) {
    513           console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
    514         }
    515         // If the object being passed into attach is not a valid <webview>
    516         // then we will fail and it will be treated as if the new window
    517         // was rejected. The permission API plumbing is used here to clean
    518         // up the state created for the new window if attaching fails.
    519         chrome.webview.setPermission(self.instanceId_, requestId, attached, '');
    520       }, 0);
    521     },
    522     discard: function() {
    523       validateCall();
    524       chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    525     }
    526   };
    527   webviewEvent.window = window;
    528 
    529   var defaultPrevented = !webviewNode.dispatchEvent(webviewEvent);
    530   if (actionTaken) {
    531     return;
    532   }
    533 
    534   if (defaultPrevented) {
    535     // Make browser plugin track lifetime of |window|.
    536     var onTrackedObjectGoneWithRequestId =
    537         $Function.bind(onTrackedObjectGone, self, requestId);
    538     browserPluginNode.addEventListener('-internal-trackedobjectgone',
    539         onTrackedObjectGoneWithRequestId);
    540     browserPluginNode['-internal-trackObjectLifetime'](window, requestId);
    541   } else {
    542     actionTaken = true;
    543     // The default action is to discard the window.
    544     chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    545     showWarningMessage();
    546   }
    547 };
    548 
    549 /**
    550  * @private
    551  */
    552 WebView.prototype.setupExecuteCodeAPI_ = function() {
    553   var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
    554       'Script cannot be injected into content until the page has loaded.';
    555 
    556   var self = this;
    557   var validateCall = function() {
    558     if (!self.browserPluginNode_.getGuestInstanceId()) {
    559       throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
    560     }
    561   };
    562 
    563   this.webviewNode_['executeScript'] = function(var_args) {
    564     validateCall();
    565     var args = $Array.concat([self.browserPluginNode_.getGuestInstanceId()],
    566                              $Array.slice(arguments));
    567     $Function.apply(chrome.webview.executeScript, null, args);
    568   }
    569   this.webviewNode_['insertCSS'] = function(var_args) {
    570     validateCall();
    571     var args = $Array.concat([self.browserPluginNode_.getGuestInstanceId()],
    572                              $Array.slice(arguments));
    573     $Function.apply(chrome.webview.insertCSS, null, args);
    574   }
    575 };
    576 
    577 /**
    578  * @private
    579  */
    580 WebView.prototype.getPermissionTypes_ = function() {
    581   return ['media', 'geolocation', 'pointerLock', 'download'];
    582 };
    583 
    584 WebView.prototype.setupExtPermissionEvent_ = function(event, webviewEvent) {
    585   var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
    586       'Permission has already been decided for this "permissionrequest" event.';
    587 
    588   var showWarningMessage = function(permission) {
    589     var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
    590         'The permission request for "%1" has been denied.';
    591     console.warn(WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
    592   };
    593 
    594   var PERMISSION_TYPES = this.getPermissionTypes_();
    595 
    596   var self = this;
    597   var browserPluginNode = this.browserPluginNode_;
    598   var webviewNode = this.webviewNode_;
    599 
    600   var requestId = event.requestId;
    601   var decisionMade = false;
    602 
    603   var onTrackedObjectGone = function(requestId, permission, e) {
    604     var detail = e.detail ? JSON.parse(e.detail) : {};
    605     if (detail.id != requestId) {
    606       return;
    607     }
    608 
    609     // Avoid showing a warning message if the decision has already been made.
    610     if (decisionMade) {
    611       return;
    612     }
    613 
    614     chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    615     showWarningMessage(permission);
    616   };
    617 
    618   var validateCall = function() {
    619     if (decisionMade) {
    620       throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
    621     }
    622     decisionMade = true;
    623   };
    624 
    625   // Construct the event.request object.
    626   var request = {
    627     allow: function() {
    628       validateCall();
    629       chrome.webview.setPermission(self.instanceId_, requestId, true, '');
    630     },
    631     deny: function() {
    632       validateCall();
    633       chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    634     }
    635   };
    636   webviewEvent.request = request;
    637 
    638   var defaultPrevented = !webviewNode.dispatchEvent(webviewEvent);
    639   if (decisionMade) {
    640     return;
    641   }
    642 
    643   if (defaultPrevented) {
    644     // Make browser plugin track lifetime of |request|.
    645     var onTrackedObjectGoneWithRequestId =
    646         $Function.bind(
    647             onTrackedObjectGone, self, requestId, event.permission);
    648     browserPluginNode.addEventListener('-internal-trackedobjectgone',
    649         onTrackedObjectGoneWithRequestId);
    650     browserPluginNode['-internal-trackObjectLifetime'](request, requestId);
    651   } else {
    652     decisionMade = true;
    653     chrome.webview.setPermission(self.instanceId_, requestId, false, '');
    654     showWarningMessage(event.permission);
    655   }
    656 };
    657 
    658 /**
    659  * Implemented when the experimental API is available.
    660  * @private
    661  */
    662 WebView.prototype.maybeSetupExperimentalAPI_ = function() {};
    663 
    664 /**
    665  * Implemented when the experimental API is available.
    666  * @private
    667  */
    668 WebView.prototype.maybeSetupExtDialogEvent_ = function() {};
    669 
    670 /**
    671  * Implemented when the experimental API is available.
    672  * @private
    673  */
    674 WebView.prototype.maybeGetWebviewExperimentalExtEvents_ = function() {};
    675 
    676 exports.WebView = WebView;
    677 exports.CreateEvent = createEvent;
    678