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