web_view.js revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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'use strict';
11
12var DocumentNatives = requireNative('document_natives');
13var EventBindings = require('event_bindings');
14var MessagingNatives = requireNative('messaging_natives');
15var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
16var WebRequestSchema =
17    requireNative('schema_registry').GetSchema('webRequest');
18var WebView = require('binding').Binding.create('webview').generate();
19var WebViewNatives = requireNative('webview_natives');
20
21// This secret enables hiding <webview> private members from the outside scope.
22// Outside of this file, |secret| is inaccessible. The only way to access the
23// <webview> element's internal members is via the |secret|. Since it's only
24// accessible by code here (and in web_view_experimental), only <webview>'s
25// API can access it and not external developers.
26var secret = {};
27
28var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
29var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
30var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
31var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
32
33/** @type {Array.<string>} */
34var WEB_VIEW_ATTRIBUTES = [
35    'name',
36    'partition',
37    'autosize',
38    WEB_VIEW_ATTRIBUTE_MINHEIGHT,
39    WEB_VIEW_ATTRIBUTE_MINWIDTH,
40    WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
41    WEB_VIEW_ATTRIBUTE_MAXWIDTH
42];
43
44var CreateEvent = function(name) {
45  var eventOpts = {supportsListeners: true, supportsFilters: true};
46  return new EventBindings.Event(name, undefined, eventOpts);
47};
48
49var WEB_VIEW_EVENTS = {
50  'close': {
51    evt: CreateEvent('webview.onClose'),
52    fields: []
53  },
54  'consolemessage': {
55    evt: CreateEvent('webview.onConsoleMessage'),
56    fields: ['level', 'message', 'line', 'sourceId']
57  },
58  'contentload': {
59    evt: CreateEvent('webview.onContentLoad'),
60    fields: []
61  },
62  'exit': {
63     evt: CreateEvent('webview.onExit'),
64     fields: ['processId', 'reason']
65  },
66  'loadabort': {
67    evt: CreateEvent('webview.onLoadAbort'),
68    fields: ['url', 'isTopLevel', 'reason']
69  },
70  'loadcommit': {
71    customHandler: function(webViewInternal, event, webViewEvent) {
72      webViewInternal.handleLoadCommitEvent_(event, webViewEvent);
73    },
74    evt: CreateEvent('webview.onLoadCommit'),
75    fields: ['url', 'isTopLevel']
76  },
77  'loadprogress': {
78    evt: CreateEvent('webview.onLoadProgress'),
79    fields: ['url', 'progress']
80  },
81  'loadredirect': {
82    evt: CreateEvent('webview.onLoadRedirect'),
83    fields: ['isTopLevel', 'oldUrl', 'newUrl']
84  },
85  'loadstart': {
86    evt: CreateEvent('webview.onLoadStart'),
87    fields: ['url', 'isTopLevel']
88  },
89  'loadstop': {
90    evt: CreateEvent('webview.onLoadStop'),
91    fields: []
92  },
93  'newwindow': {
94    cancelable: true,
95    customHandler: function(webViewInternal, event, webViewEvent) {
96      webViewInternal.handleNewWindowEvent_(event, webViewEvent);
97    },
98    evt: CreateEvent('webview.onNewWindow'),
99    fields: [
100      'initialHeight',
101      'initialWidth',
102      'targetUrl',
103      'windowOpenDisposition',
104      'name'
105    ]
106  },
107  'permissionrequest': {
108    cancelable: true,
109    customHandler: function(webViewInternal, event, webViewEvent) {
110      webViewInternal.handlePermissionEvent_(event, webViewEvent);
111    },
112    evt: CreateEvent('webview.onPermissionRequest'),
113    fields: [
114      'lastUnlockedBySelf',
115      'permission',
116      'requestMethod',
117      'url',
118      'userGesture'
119    ]
120  },
121  'responsive': {
122    evt: CreateEvent('webview.onResponsive'),
123    fields: ['processId']
124  },
125  'sizechanged': {
126    evt: CreateEvent('webview.onSizeChanged'),
127    customHandler: function(webViewInternal, event, webViewEvent) {
128      webViewInternal.handleSizeChangedEvent_(event, webViewEvent);
129    },
130    fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
131  },
132  'unresponsive': {
133    evt: CreateEvent('webview.onUnresponsive'),
134    fields: ['processId']
135  }
136};
137
138// Implemented when the experimental API is available.
139WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
140
141/**
142 * @constructor
143 */
144function WebViewInternal(webviewNode) {
145  this.webviewNode_ = webviewNode;
146  this.browserPluginNode_ = this.createBrowserPluginNode_();
147  var shadowRoot = this.webviewNode_.webkitCreateShadowRoot();
148  shadowRoot.appendChild(this.browserPluginNode_);
149
150  this.setupWebviewNodeAttributes_();
151  this.setupFocusPropagation_();
152  this.setupWebviewNodeProperties_();
153  this.setupWebviewNodeEvents_();
154}
155
156/**
157 * @private
158 */
159WebViewInternal.prototype.createBrowserPluginNode_ = function() {
160  // We create BrowserPlugin as a custom element in order to observe changes
161  // to attributes synchronously.
162  var browserPluginNode = new WebViewInternal.BrowserPlugin();
163  Object.defineProperty(browserPluginNode, 'internal_', {
164    enumerable: false,
165    writable: false,
166    value: function(key) {
167      if (key !== secret) {
168        return null;
169      }
170      return this;
171    }.bind(this)
172  });
173
174  var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']);
175  $Array.forEach(ALL_ATTRIBUTES, function(attributeName) {
176    // Only copy attributes that have been assigned values, rather than copying
177    // a series of undefined attributes to BrowserPlugin.
178    if (this.webviewNode_.hasAttribute(attributeName)) {
179      browserPluginNode.setAttribute(
180        attributeName, this.webviewNode_.getAttribute(attributeName));
181    } else if (this.webviewNode_[attributeName]){
182      // Reading property using has/getAttribute does not work on
183      // document.DOMContentLoaded event (but works on
184      // window.DOMContentLoaded event).
185      // So copy from property if copying from attribute fails.
186      browserPluginNode.setAttribute(
187        attributeName, this.webviewNode_[attributeName]);
188    }
189  }, this);
190
191  return browserPluginNode;
192};
193
194/**
195 * @private
196 */
197WebViewInternal.prototype.setupFocusPropagation_ = function() {
198  if (!this.webviewNode_.hasAttribute('tabIndex')) {
199    // <webview> needs a tabIndex in order to respond to keyboard focus.
200    // TODO(fsamuel): This introduces unexpected tab ordering. We need to find
201    // a way to take keyboard focus without messing with tab ordering.
202    // See http://crbug.com/231664.
203    this.webviewNode_.setAttribute('tabIndex', 0);
204  }
205  var self = this;
206  this.webviewNode_.addEventListener('focus', function(e) {
207    // Focus the BrowserPlugin when the <webview> takes focus.
208    self.browserPluginNode_.focus();
209  });
210  this.webviewNode_.addEventListener('blur', function(e) {
211    // Blur the BrowserPlugin when the <webview> loses focus.
212    self.browserPluginNode_.blur();
213  });
214};
215
216/**
217 * @private
218 */
219WebViewInternal.prototype.canGoBack_ = function() {
220  return this.entryCount_ > 1 && this.currentEntryIndex_ > 0;
221};
222
223/**
224 * @private
225 */
226WebViewInternal.prototype.canGoForward_ = function() {
227  return this.currentEntryIndex_ >= 0 &&
228      this.currentEntryIndex_ < (this.entryCount_ - 1);
229};
230
231/**
232 * @private
233 */
234WebViewInternal.prototype.getProcessId_ = function() {
235  return this.processId_;
236};
237
238/**
239 * @private
240 */
241WebViewInternal.prototype.go_ = function(relativeIndex) {
242  if (!this.instanceId_) {
243    return;
244  }
245  WebView.go(this.instanceId_, relativeIndex);
246};
247
248/**
249 * @private
250 */
251WebViewInternal.prototype.reload_ = function() {
252  if (!this.instanceId_) {
253    return;
254  }
255  WebView.reload(this.instanceId_);
256};
257
258/**
259 * @private
260 */
261WebViewInternal.prototype.stop_ = function() {
262  if (!this.instanceId_) {
263    return;
264  }
265  WebView.stop(this.instanceId_);
266};
267
268/**
269 * @private
270 */
271WebViewInternal.prototype.terminate_ = function() {
272  if (!this.instanceId_) {
273    return;
274  }
275  WebView.terminate(this.instanceId_);
276};
277
278/**
279 * @private
280 */
281WebViewInternal.prototype.validateExecuteCodeCall_  = function() {
282  var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
283      'Script cannot be injected into content until the page has loaded.';
284  if (!this.instanceId_) {
285    throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
286  }
287};
288
289/**
290 * @private
291 */
292WebViewInternal.prototype.executeScript_ = function(var_args) {
293  this.validateExecuteCodeCall_();
294  var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
295  $Function.apply(WebView.executeScript, null, args);
296};
297
298/**
299 * @private
300 */
301WebViewInternal.prototype.insertCSS_ = function(var_args) {
302  this.validateExecuteCodeCall_();
303  var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
304  $Function.apply(WebView.insertCSS, null, args);
305};
306
307/**
308 * @private
309 */
310WebViewInternal.prototype.setupWebviewNodeProperties_ = function() {
311  var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
312    'contentWindow is not available at this time. It will become available ' +
313        'when the page has finished loading.';
314
315  var self = this;
316  var browserPluginNode = this.browserPluginNode_;
317  // Expose getters and setters for the attributes.
318  $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
319    Object.defineProperty(this.webviewNode_, attributeName, {
320      get: function() {
321        if (browserPluginNode.hasOwnProperty(attributeName)) {
322          return browserPluginNode[attributeName];
323        } else {
324          return browserPluginNode.getAttribute(attributeName);
325        }
326      },
327      set: function(value) {
328        if (browserPluginNode.hasOwnProperty(attributeName)) {
329          // Give the BrowserPlugin first stab at the attribute so that it can
330          // throw an exception if there is a problem. This attribute will then
331          // be propagated back to the <webview>.
332          browserPluginNode[attributeName] = value;
333        } else {
334          browserPluginNode.setAttribute(attributeName, value);
335        }
336      },
337      enumerable: true
338    });
339  }, this);
340
341  // <webview> src does not quite behave the same as BrowserPlugin src, and so
342  // we don't simply keep the two in sync.
343  this.src_ = this.webviewNode_.getAttribute('src');
344  Object.defineProperty(this.webviewNode_, 'src', {
345    get: function() {
346      return self.src_;
347    },
348    set: function(value) {
349      self.webviewNode_.setAttribute('src', value);
350    },
351    // No setter.
352    enumerable: true
353  });
354
355  // We cannot use {writable: true} property descriptor because we want a
356  // dynamic getter value.
357  Object.defineProperty(this.webviewNode_, 'contentWindow', {
358    get: function() {
359      if (browserPluginNode.contentWindow)
360        return browserPluginNode.contentWindow;
361      console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
362    },
363    // No setter.
364    enumerable: true
365  });
366};
367
368/**
369 * @private
370 */
371WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() {
372  Object.defineProperty(this.webviewNode_, 'internal_', {
373    enumerable: false,
374    writable: false,
375    value: function(key) {
376      if (key !== secret) {
377        return null;
378      }
379      return this;
380    }.bind(this)
381  });
382  this.setupWebViewSrcAttributeMutationObserver_();
383};
384
385/**
386 * @private
387 */
388WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ =
389    function() {
390  // The purpose of this mutation observer is to catch assignment to the src
391  // attribute without any changes to its value. This is useful in the case
392  // where the webview guest has crashed and navigating to the same address
393  // spawns off a new process.
394  var self = this;
395  this.srcObserver_ = new MutationObserver(function(mutations) {
396    $Array.forEach(mutations, function(mutation) {
397      var oldValue = mutation.oldValue;
398      var newValue = self.webviewNode_.getAttribute(mutation.attributeName);
399      if (oldValue != newValue) {
400        return;
401      }
402      self.handleWebviewAttributeMutation_(
403          mutation.attributeName, oldValue, newValue);
404    });
405  });
406  var params = {
407    attributes: true,
408    attributeOldValue: true,
409    attributeFilter: ['src']
410  };
411  this.srcObserver_.observe(this.webviewNode_, params);
412};
413
414/**
415 * @private
416 */
417WebViewInternal.prototype.handleWebviewAttributeMutation_ =
418      function(name, oldValue, newValue) {
419  // This observer monitors mutations to attributes of the <webview> and
420  // updates the BrowserPlugin properties accordingly. In turn, updating
421  // a BrowserPlugin property will update the corresponding BrowserPlugin
422  // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
423  // details.
424  if (name == 'src') {
425    // We treat null attribute (attribute removed) and the empty string as
426    // one case.
427    oldValue = oldValue || '';
428    newValue = newValue || '';
429    // Once we have navigated, we don't allow clearing the src attribute.
430    // Once <webview> enters a navigated state, it cannot be return back to a
431    // placeholder state.
432    if (newValue == '' && oldValue != '') {
433      // src attribute changes normally initiate a navigation. We suppress
434      // the next src attribute handler call to avoid reloading the page
435      // on every guest-initiated navigation.
436      this.ignoreNextSrcAttributeChange_ = true;
437      this.webviewNode_.setAttribute('src', oldValue);
438      return;
439    }
440    this.src_ = newValue;
441    if (this.ignoreNextSrcAttributeChange_) {
442      // Don't allow the src mutation observer to see this change.
443      this.srcObserver_.takeRecords();
444      this.ignoreNextSrcAttributeChange_ = false;
445      return;
446    }
447  }
448  if (this.browserPluginNode_.hasOwnProperty(name)) {
449    this.browserPluginNode_[name] = newValue;
450  } else {
451    this.browserPluginNode_.setAttribute(name, newValue);
452  }
453};
454
455/**
456 * @private
457 */
458WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ =
459    function(name, newValue) {
460  // This observer monitors mutations to attributes of the BrowserPlugin and
461  // updates the <webview> attributes accordingly.
462  // |newValue| is null if the attribute |name| has been removed.
463  if (newValue != null) {
464    // Update the <webview> attribute to match the BrowserPlugin attribute.
465    // Note: Calling setAttribute on <webview> will trigger its mutation
466    // observer which will then propagate that attribute to BrowserPlugin. In
467    // cases where we permit assigning a BrowserPlugin attribute the same value
468    // again (such as navigation when crashed), this could end up in an infinite
469    // loop. Thus, we avoid this loop by only updating the <webview> attribute
470    // if the BrowserPlugin attributes differs from it.
471    if (newValue != this.webviewNode_.getAttribute(name)) {
472      this.webviewNode_.setAttribute(name, newValue);
473    }
474  } else {
475    // If an attribute is removed from the BrowserPlugin, then remove it
476    // from the <webview> as well.
477    this.webviewNode_.removeAttribute(name);
478  }
479};
480
481/**
482 * @private
483 */
484WebViewInternal.prototype.getEvents_ = function() {
485  var experimentalEvents = this.maybeGetExperimentalEvents_();
486  for (var eventName in experimentalEvents) {
487    WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
488  }
489  return WEB_VIEW_EVENTS;
490};
491
492WebViewInternal.prototype.handleSizeChangedEvent_ =
493    function(event, webViewEvent) {
494  var node = this.webviewNode_;
495
496  var width = node.offsetWidth;
497  var height = node.offsetHeight;
498
499  // Check the current bounds to make sure we do not resize <webview>
500  // outside of current constraints.
501  var maxWidth;
502  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
503      node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
504    maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
505  } else {
506    maxWidth = width;
507  }
508
509  var minWidth;
510  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
511      node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
512    minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
513  } else {
514    minWidth = width;
515  }
516  if (minWidth > maxWidth) {
517    minWidth = maxWidth;
518  }
519
520  var maxHeight;
521  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
522      node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
523    maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
524  } else {
525    maxHeight = height;
526  }
527  var minHeight;
528  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
529      node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
530    minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
531  } else {
532    minHeight = height;
533  }
534  if (minHeight > maxHeight) {
535    minHeight = maxHeight;
536  }
537
538  if (webViewEvent.newWidth >= minWidth &&
539      webViewEvent.newWidth <= maxWidth &&
540      webViewEvent.newHeight >= minHeight &&
541      webViewEvent.newHeight <= maxHeight) {
542    node.style.width = webViewEvent.newWidth + 'px';
543    node.style.height = webViewEvent.newHeight + 'px';
544  }
545  node.dispatchEvent(webViewEvent);
546};
547
548/**
549 * @private
550 */
551WebViewInternal.prototype.setupWebviewNodeEvents_ = function() {
552  var self = this;
553  this.viewInstanceId_ = WebViewNatives.GetNextInstanceID();
554  var onInstanceIdAllocated = function(e) {
555    var detail = e.detail ? JSON.parse(e.detail) : {};
556    self.instanceId_ = detail.windowId;
557    var params = {
558      'api': 'webview',
559      'instanceId': self.viewInstanceId_
560    };
561    self.browserPluginNode_['-internal-attach'](params);
562
563    var events = self.getEvents_();
564    for (var eventName in events) {
565      self.setupEvent_(eventName, events[eventName]);
566    }
567  };
568  this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
569                                           onInstanceIdAllocated);
570  this.setupWebRequestEvents_();
571};
572
573/**
574 * @private
575 */
576WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) {
577  var self = this;
578  var webviewNode = this.webviewNode_;
579  eventInfo.evt.addListener(function(event) {
580    var details = {bubbles:true};
581    if (eventInfo.cancelable)
582      details.cancelable = true;
583    var webViewEvent = new Event(eventName, details);
584    $Array.forEach(eventInfo.fields, function(field) {
585      if (event[field] !== undefined) {
586        webViewEvent[field] = event[field];
587      }
588    });
589    if (eventInfo.customHandler) {
590      eventInfo.customHandler(self, event, webViewEvent);
591      return;
592    }
593    webviewNode.dispatchEvent(webViewEvent);
594  }, {instanceId: self.instanceId_});
595};
596
597/**
598 * @private
599 */
600WebViewInternal.prototype.getPermissionTypes_ = function() {
601  return ['media', 'geolocation', 'pointerLock', 'download'];
602};
603
604/**
605 * @private
606 */
607WebViewInternal.prototype.handleLoadCommitEvent_ =
608    function(event, webViewEvent) {
609  this.currentEntryIndex_ = event.currentEntryIndex;
610  this.entryCount_ = event.entryCount;
611  this.processId_ = event.processId;
612  var oldValue = this.webviewNode_.getAttribute('src');
613  var newValue = event.url;
614  if (event.isTopLevel && (oldValue != newValue)) {
615    // Touching the src attribute triggers a navigation. To avoid
616    // triggering a page reload on every guest-initiated navigation,
617    // we use the flag ignoreNextSrcAttributeChange_ here.
618    this.ignoreNextSrcAttributeChange_ = true;
619    this.webviewNode_.setAttribute('src', newValue);
620  }
621  this.webviewNode_.dispatchEvent(webViewEvent);
622}
623
624/**
625 * @private
626 */
627WebViewInternal.prototype.handleNewWindowEvent_ =
628    function(event, webViewEvent) {
629  var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
630      'An action has already been taken for this "newwindow" event.';
631
632  var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
633      'Unable to attach the new window to the provided webview.';
634
635  var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
636
637  var showWarningMessage = function() {
638    var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
639    console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
640  };
641
642  var self = this;
643  var browserPluginNode = this.browserPluginNode_;
644  var webviewNode = this.webviewNode_;
645
646  var requestId = event.requestId;
647  var actionTaken = false;
648
649  var validateCall = function () {
650    if (actionTaken) {
651      throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
652    }
653    actionTaken = true;
654  };
655
656  var window = {
657    attach: function(webview) {
658      validateCall();
659      if (!webview)
660        throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
661      // Attach happens asynchronously to give the tagWatcher an opportunity
662      // to pick up the new webview before attach operates on it, if it hasn't
663      // been attached to the DOM already.
664      // Note: Any subsequent errors cannot be exceptions because they happen
665      // asynchronously.
666      setTimeout(function() {
667        var attached =
668            browserPluginNode['-internal-attachWindowTo'](webview,
669                                                          event.windowId);
670        if (!attached) {
671          console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
672        }
673        // If the object being passed into attach is not a valid <webview>
674        // then we will fail and it will be treated as if the new window
675        // was rejected. The permission API plumbing is used here to clean
676        // up the state created for the new window if attaching fails.
677        WebView.setPermission(self.instanceId_, requestId, attached, '');
678      }, 0);
679    },
680    discard: function() {
681      validateCall();
682      WebView.setPermission(self.instanceId_, requestId, false, '');
683    }
684  };
685  webViewEvent.window = window;
686
687  var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
688  if (actionTaken) {
689    return;
690  }
691
692  if (defaultPrevented) {
693    // Make browser plugin track lifetime of |window|.
694    MessagingNatives.BindToGC(window, function() {
695      // Avoid showing a warning message if the decision has already been made.
696      if (actionTaken) {
697        return;
698      }
699      WebView.setPermission(self.instanceId_, requestId, false, '');
700      showWarningMessage();
701    });
702  } else {
703    actionTaken = true;
704    // The default action is to discard the window.
705    WebView.setPermission(self.instanceId_, requestId, false, '');
706    showWarningMessage();
707  }
708};
709
710WebViewInternal.prototype.handlePermissionEvent_ =
711    function(event, webViewEvent) {
712  var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
713      'Permission has already been decided for this "permissionrequest" event.';
714
715  var showWarningMessage = function(permission) {
716    var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
717        'The permission request for "%1" has been denied.';
718    console.warn(WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
719  };
720
721  var PERMISSION_TYPES = this.getPermissionTypes_();
722
723  var self = this;
724  var browserPluginNode = this.browserPluginNode_;
725  var webviewNode = this.webviewNode_;
726
727  var requestId = event.requestId;
728  var decisionMade = false;
729
730  var validateCall = function() {
731    if (decisionMade) {
732      throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
733    }
734    decisionMade = true;
735  };
736
737  // Construct the event.request object.
738  var request = {
739    allow: function() {
740      validateCall();
741      WebView.setPermission(self.instanceId_, requestId, true, '');
742    },
743    deny: function() {
744      validateCall();
745      WebView.setPermission(self.instanceId_, requestId, false, '');
746    }
747  };
748  webViewEvent.request = request;
749
750  var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
751  if (decisionMade) {
752    return;
753  }
754
755  if (defaultPrevented) {
756    // Make browser plugin track lifetime of |request|.
757    MessagingNatives.BindToGC(request, function() {
758      // Avoid showing a warning message if the decision has already been made.
759      if (decisionMade) {
760        return;
761      }
762      WebView.setPermission(self.instanceId_, requestId, false, '');
763      showWarningMessage(event.permission);
764    });
765  } else {
766    decisionMade = true;
767    WebView.setPermission(self.instanceId_, requestId, false, '');
768    showWarningMessage(event.permission);
769  }
770};
771
772/**
773 * @private
774 */
775WebViewInternal.prototype.setupWebRequestEvents_ = function() {
776  var self = this;
777  var request = {};
778  var createWebRequestEvent = function(webRequestEvent) {
779    return function() {
780      if (!self[webRequestEvent.name + '_']) {
781        self[webRequestEvent.name + '_'] =
782            new WebRequestEvent(
783                'webview.' + webRequestEvent.name,
784                webRequestEvent.parameters,
785                webRequestEvent.extraParameters, null,
786                self.viewInstanceId_);
787      }
788      return self[webRequestEvent.name + '_'];
789    };
790  };
791
792  // Populate the WebRequest events from the API definition.
793  for (var i = 0; i < WebRequestSchema.events.length; ++i) {
794    var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
795    Object.defineProperty(
796        request,
797        WebRequestSchema.events[i].name,
798        {
799          get: webRequestEvent,
800          enumerable: true
801        }
802    );
803    this.maybeAttachWebRequestEventToWebview_(WebRequestSchema.events[i].name,
804                                              webRequestEvent);
805  }
806  Object.defineProperty(
807      this.webviewNode_,
808      'request',
809      {
810        value: request,
811        enumerable: true,
812        writable: false
813      }
814  );
815};
816
817// Registers browser plugin <object> custom element.
818function registerBrowserPluginElement() {
819  var proto = Object.create(HTMLObjectElement.prototype);
820
821  proto.createdCallback = function() {
822    this.setAttribute('type', 'application/browser-plugin');
823    // The <object> node fills in the <webview> container.
824    this.style.width = '100%';
825    this.style.height = '100%';
826  };
827
828  proto.attributeChangedCallback = function(name, oldValue, newValue) {
829    if (!this.internal_) {
830      return;
831    }
832    var internal = this.internal_(secret);
833    internal.handleBrowserPluginAttributeMutation_(name, newValue);
834  };
835
836  WebViewInternal.BrowserPlugin =
837      DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
838                                                         prototype: proto});
839
840  delete proto.createdCallback;
841  delete proto.enteredDocumentCallback;
842  delete proto.leftDocumentCallback;
843  delete proto.attributeChangedCallback;
844}
845
846// Registers <webview> custom element.
847function registerWebViewElement() {
848  var proto = Object.create(HTMLElement.prototype);
849
850  proto.createdCallback = function() {
851    new WebViewInternal(this);
852  };
853
854  proto.attributeChangedCallback = function(name, oldValue, newValue) {
855    var internal = this.internal_(secret);
856    internal.handleWebviewAttributeMutation_(name, oldValue, newValue);
857  };
858
859  proto.back = function() {
860    this.go(-1);
861  };
862
863  proto.forward = function() {
864    this.go(1);
865  };
866
867  proto.canGoBack = function() {
868    return this.internal_(secret).canGoBack_();
869  };
870
871  proto.canGoForward = function() {
872    return this.internal_(secret).canGoForward_();
873  };
874
875  proto.getProcessId = function() {
876    return this.internal_(secret).getProcessId_();
877  };
878
879  proto.go = function(relativeIndex) {
880    this.internal_(secret).go_(relativeIndex);
881  };
882
883  proto.reload = function() {
884    this.internal_(secret).reload_();
885  };
886
887  proto.stop = function() {
888    this.internal_(secret).stop_();
889  };
890
891  proto.terminate = function() {
892    this.internal_(secret).terminate_();
893  };
894
895  proto.executeScript = function(var_args) {
896    var internal = this.internal_(secret);
897    $Function.apply(internal.executeScript_, internal, arguments);
898  };
899
900  proto.insertCSS = function(var_args) {
901    var internal = this.internal_(secret);
902    $Function.apply(internal.insertCSS_, internal, arguments);
903  };
904  WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret);
905
906  window.WebView =
907      DocumentNatives.RegisterElement('webview', {prototype: proto});
908
909  // Delete the callbacks so developers cannot call them and produce unexpected
910  // behavior.
911  delete proto.createdCallback;
912  delete proto.enteredDocumentCallback;
913  delete proto.leftDocumentCallback;
914  delete proto.attributeChangedCallback;
915}
916
917var useCapture = true;
918window.addEventListener('readystatechange', function listener(event) {
919  if (document.readyState == 'loading')
920    return;
921
922  registerBrowserPluginElement();
923  registerWebViewElement();
924  window.removeEventListener(event.type, listener, useCapture);
925}, useCapture);
926
927/**
928 * Implemented when the experimental API is available.
929 * @private
930 */
931WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {};
932
933/**
934 * Implemented when the experimental API is available.
935 * @private
936 */
937WebViewInternal.prototype.maybeAttachWebRequestEventToWebview_ = function() {};
938
939exports.WebView = WebView;
940exports.WebViewInternal = WebViewInternal;
941exports.CreateEvent = CreateEvent;
942