Added documentation to web_view.js/web_view_experimental.js regarding the webview...
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / web_view.js
bloba4176810795283a85644c5e53a5952037fbc91f1
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.
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.
10 'use strict';
12 var DocumentNatives = requireNative('document_natives');
13 var EventBindings = require('event_bindings');
14 var IdGenerator = requireNative('id_generator');
15 var MessagingNatives = requireNative('messaging_natives');
16 var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
17 var WebRequestSchema =
18 requireNative('schema_registry').GetSchema('webRequest');
19 var DeclarativeWebRequestSchema =
20 requireNative('schema_registry').GetSchema('declarativeWebRequest');
21 var WebView = require('binding').Binding.create('webview').generate();
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.
28 var secret = {};
30 var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
31 var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
32 var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
33 var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
35 /** @type {Array.<string>} */
36 var 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
47 var CreateEvent = function(name) {
48 var eventOpts = {supportsListeners: true, supportsFilters: true};
49 return new EventBindings.Event(name, undefined, eventOpts);
52 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
53 // associated extension event descriptor objects.
54 // An event listener will be attached to the extension event |evt| specified in
55 // the descriptor.
56 // |fields| specifies the public-facing fields in the DOM event that are
57 // accessible to <webview> developers.
58 // |customHandler| allows a handler function to be called each time an extension
59 // event is caught by its event listener. The DOM event should be dispatched
60 // within this handler function. With no handler function, the DOM event
61 // will be dispatched by default each time the extension event is caught.
62 // |cancelable| (default: false) specifies whether the event's default
63 // behavior can be canceled. If the default action associated with the event
64 // is prevented, then its dispatch function will return false in its event
65 // handler. The event must have a custom handler for this to be meaningful.
66 var WEB_VIEW_EVENTS = {
67 'close': {
68 evt: CreateEvent('webview.onClose'),
69 fields: []
71 'consolemessage': {
72 evt: CreateEvent('webview.onConsoleMessage'),
73 fields: ['level', 'message', 'line', 'sourceId']
75 'contentload': {
76 evt: CreateEvent('webview.onContentLoad'),
77 fields: []
79 'exit': {
80 evt: CreateEvent('webview.onExit'),
81 fields: ['processId', 'reason']
83 'loadabort': {
84 cancelable: true,
85 customHandler: function(webViewInternal, event, webViewEvent) {
86 webViewInternal.handleLoadAbortEvent_(event, webViewEvent);
88 evt: CreateEvent('webview.onLoadAbort'),
89 fields: ['url', 'isTopLevel', 'reason']
91 'loadcommit': {
92 customHandler: function(webViewInternal, event, webViewEvent) {
93 webViewInternal.handleLoadCommitEvent_(event, webViewEvent);
95 evt: CreateEvent('webview.onLoadCommit'),
96 fields: ['url', 'isTopLevel']
98 'loadprogress': {
99 evt: CreateEvent('webview.onLoadProgress'),
100 fields: ['url', 'progress']
102 'loadredirect': {
103 evt: CreateEvent('webview.onLoadRedirect'),
104 fields: ['isTopLevel', 'oldUrl', 'newUrl']
106 'loadstart': {
107 evt: CreateEvent('webview.onLoadStart'),
108 fields: ['url', 'isTopLevel']
110 'loadstop': {
111 evt: CreateEvent('webview.onLoadStop'),
112 fields: []
114 'newwindow': {
115 cancelable: true,
116 customHandler: function(webViewInternal, event, webViewEvent) {
117 webViewInternal.handleNewWindowEvent_(event, webViewEvent);
119 evt: CreateEvent('webview.onNewWindow'),
120 fields: [
121 'initialHeight',
122 'initialWidth',
123 'targetUrl',
124 'windowOpenDisposition',
125 'name'
128 'permissionrequest': {
129 cancelable: true,
130 customHandler: function(webViewInternal, event, webViewEvent) {
131 webViewInternal.handlePermissionEvent_(event, webViewEvent);
133 evt: CreateEvent('webview.onPermissionRequest'),
134 fields: [
135 'identifier',
136 'lastUnlockedBySelf',
137 'name',
138 'permission',
139 'requestMethod',
140 'url',
141 'userGesture'
144 'responsive': {
145 evt: CreateEvent('webview.onResponsive'),
146 fields: ['processId']
148 'sizechanged': {
149 evt: CreateEvent('webview.onSizeChanged'),
150 customHandler: function(webViewInternal, event, webViewEvent) {
151 webViewInternal.handleSizeChangedEvent_(event, webViewEvent);
153 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
155 'unresponsive': {
156 evt: CreateEvent('webview.onUnresponsive'),
157 fields: ['processId']
161 // Implemented when the experimental API is available.
162 WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
165 * @constructor
167 function WebViewInternal(webviewNode) {
168 this.webviewNode_ = webviewNode;
169 this.browserPluginNode_ = this.createBrowserPluginNode_();
170 var shadowRoot = this.webviewNode_.webkitCreateShadowRoot();
171 shadowRoot.appendChild(this.browserPluginNode_);
173 this.setupWebviewNodeAttributes_();
174 this.setupFocusPropagation_();
175 this.setupWebviewNodeProperties_();
176 this.setupWebviewNodeEvents_();
180 * @private
182 WebViewInternal.prototype.createBrowserPluginNode_ = function() {
183 // We create BrowserPlugin as a custom element in order to observe changes
184 // to attributes synchronously.
185 var browserPluginNode = new WebViewInternal.BrowserPlugin();
186 Object.defineProperty(browserPluginNode, 'internal_', {
187 enumerable: false,
188 writable: false,
189 value: function(key) {
190 if (key !== secret) {
191 return null;
193 return this;
194 }.bind(this)
197 var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']);
198 $Array.forEach(ALL_ATTRIBUTES, function(attributeName) {
199 // Only copy attributes that have been assigned values, rather than copying
200 // a series of undefined attributes to BrowserPlugin.
201 if (this.webviewNode_.hasAttribute(attributeName)) {
202 browserPluginNode.setAttribute(
203 attributeName, this.webviewNode_.getAttribute(attributeName));
204 } else if (this.webviewNode_[attributeName]){
205 // Reading property using has/getAttribute does not work on
206 // document.DOMContentLoaded event (but works on
207 // window.DOMContentLoaded event).
208 // So copy from property if copying from attribute fails.
209 browserPluginNode.setAttribute(
210 attributeName, this.webviewNode_[attributeName]);
212 }, this);
214 return browserPluginNode;
218 * @private
220 WebViewInternal.prototype.setupFocusPropagation_ = function() {
221 if (!this.webviewNode_.hasAttribute('tabIndex')) {
222 // <webview> needs a tabIndex in order to be focusable.
223 // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
224 // to allow <webview> to be focusable.
225 // See http://crbug.com/231664.
226 this.webviewNode_.setAttribute('tabIndex', -1);
228 var self = this;
229 this.webviewNode_.addEventListener('focus', function(e) {
230 // Focus the BrowserPlugin when the <webview> takes focus.
231 self.browserPluginNode_.focus();
233 this.webviewNode_.addEventListener('blur', function(e) {
234 // Blur the BrowserPlugin when the <webview> loses focus.
235 self.browserPluginNode_.blur();
240 * @private
242 WebViewInternal.prototype.canGoBack_ = function() {
243 return this.entryCount_ > 1 && this.currentEntryIndex_ > 0;
247 * @private
249 WebViewInternal.prototype.canGoForward_ = function() {
250 return this.currentEntryIndex_ >= 0 &&
251 this.currentEntryIndex_ < (this.entryCount_ - 1);
255 * @private
257 WebViewInternal.prototype.getProcessId_ = function() {
258 return this.processId_;
262 * @private
264 WebViewInternal.prototype.go_ = function(relativeIndex) {
265 if (!this.instanceId_) {
266 return;
268 WebView.go(this.instanceId_, relativeIndex);
272 * @private
274 WebViewInternal.prototype.reload_ = function() {
275 if (!this.instanceId_) {
276 return;
278 WebView.reload(this.instanceId_);
282 * @private
284 WebViewInternal.prototype.stop_ = function() {
285 if (!this.instanceId_) {
286 return;
288 WebView.stop(this.instanceId_);
292 * @private
294 WebViewInternal.prototype.terminate_ = function() {
295 if (!this.instanceId_) {
296 return;
298 WebView.terminate(this.instanceId_);
302 * @private
304 WebViewInternal.prototype.validateExecuteCodeCall_ = function() {
305 var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
306 'Script cannot be injected into content until the page has loaded.';
307 if (!this.instanceId_) {
308 throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
313 * @private
315 WebViewInternal.prototype.executeScript_ = function(var_args) {
316 this.validateExecuteCodeCall_();
317 var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
318 $Function.apply(WebView.executeScript, null, args);
322 * @private
324 WebViewInternal.prototype.insertCSS_ = function(var_args) {
325 this.validateExecuteCodeCall_();
326 var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
327 $Function.apply(WebView.insertCSS, null, args);
331 * @private
333 WebViewInternal.prototype.setupWebviewNodeProperties_ = function() {
334 var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
335 'contentWindow is not available at this time. It will become available ' +
336 'when the page has finished loading.';
338 var self = this;
339 var browserPluginNode = this.browserPluginNode_;
340 // Expose getters and setters for the attributes.
341 $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
342 Object.defineProperty(this.webviewNode_, attributeName, {
343 get: function() {
344 if (browserPluginNode.hasOwnProperty(attributeName)) {
345 return browserPluginNode[attributeName];
346 } else {
347 return browserPluginNode.getAttribute(attributeName);
350 set: function(value) {
351 if (browserPluginNode.hasOwnProperty(attributeName)) {
352 // Give the BrowserPlugin first stab at the attribute so that it can
353 // throw an exception if there is a problem. This attribute will then
354 // be propagated back to the <webview>.
355 browserPluginNode[attributeName] = value;
356 } else {
357 browserPluginNode.setAttribute(attributeName, value);
360 enumerable: true
362 }, this);
364 // <webview> src does not quite behave the same as BrowserPlugin src, and so
365 // we don't simply keep the two in sync.
366 this.src_ = this.webviewNode_.getAttribute('src');
367 Object.defineProperty(this.webviewNode_, 'src', {
368 get: function() {
369 return self.src_;
371 set: function(value) {
372 self.webviewNode_.setAttribute('src', value);
374 // No setter.
375 enumerable: true
378 // We cannot use {writable: true} property descriptor because we want a
379 // dynamic getter value.
380 Object.defineProperty(this.webviewNode_, 'contentWindow', {
381 get: function() {
382 if (browserPluginNode.contentWindow)
383 return browserPluginNode.contentWindow;
384 window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
386 // No setter.
387 enumerable: true
392 * @private
394 WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() {
395 Object.defineProperty(this.webviewNode_, 'internal_', {
396 enumerable: false,
397 writable: false,
398 value: function(key) {
399 if (key !== secret) {
400 return null;
402 return this;
403 }.bind(this)
405 this.setupWebViewSrcAttributeMutationObserver_();
409 * @private
411 WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ =
412 function() {
413 // The purpose of this mutation observer is to catch assignment to the src
414 // attribute without any changes to its value. This is useful in the case
415 // where the webview guest has crashed and navigating to the same address
416 // spawns off a new process.
417 var self = this;
418 this.srcObserver_ = new MutationObserver(function(mutations) {
419 $Array.forEach(mutations, function(mutation) {
420 var oldValue = mutation.oldValue;
421 var newValue = self.webviewNode_.getAttribute(mutation.attributeName);
422 if (oldValue != newValue) {
423 return;
425 self.handleWebviewAttributeMutation_(
426 mutation.attributeName, oldValue, newValue);
429 var params = {
430 attributes: true,
431 attributeOldValue: true,
432 attributeFilter: ['src']
434 this.srcObserver_.observe(this.webviewNode_, params);
438 * @private
440 WebViewInternal.prototype.handleWebviewAttributeMutation_ =
441 function(name, oldValue, newValue) {
442 // This observer monitors mutations to attributes of the <webview> and
443 // updates the BrowserPlugin properties accordingly. In turn, updating
444 // a BrowserPlugin property will update the corresponding BrowserPlugin
445 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
446 // details.
447 if (name == 'src') {
448 // We treat null attribute (attribute removed) and the empty string as
449 // one case.
450 oldValue = oldValue || '';
451 newValue = newValue || '';
452 // Once we have navigated, we don't allow clearing the src attribute.
453 // Once <webview> enters a navigated state, it cannot be return back to a
454 // placeholder state.
455 if (newValue == '' && oldValue != '') {
456 // src attribute changes normally initiate a navigation. We suppress
457 // the next src attribute handler call to avoid reloading the page
458 // on every guest-initiated navigation.
459 this.ignoreNextSrcAttributeChange_ = true;
460 this.webviewNode_.setAttribute('src', oldValue);
461 return;
463 this.src_ = newValue;
464 if (this.ignoreNextSrcAttributeChange_) {
465 // Don't allow the src mutation observer to see this change.
466 this.srcObserver_.takeRecords();
467 this.ignoreNextSrcAttributeChange_ = false;
468 return;
471 if (this.browserPluginNode_.hasOwnProperty(name)) {
472 this.browserPluginNode_[name] = newValue;
473 } else {
474 this.browserPluginNode_.setAttribute(name, newValue);
479 * @private
481 WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ =
482 function(name, newValue) {
483 // This observer monitors mutations to attributes of the BrowserPlugin and
484 // updates the <webview> attributes accordingly.
485 // |newValue| is null if the attribute |name| has been removed.
486 if (newValue != null) {
487 // Update the <webview> attribute to match the BrowserPlugin attribute.
488 // Note: Calling setAttribute on <webview> will trigger its mutation
489 // observer which will then propagate that attribute to BrowserPlugin. In
490 // cases where we permit assigning a BrowserPlugin attribute the same value
491 // again (such as navigation when crashed), this could end up in an infinite
492 // loop. Thus, we avoid this loop by only updating the <webview> attribute
493 // if the BrowserPlugin attributes differs from it.
494 if (newValue != this.webviewNode_.getAttribute(name)) {
495 this.webviewNode_.setAttribute(name, newValue);
497 } else {
498 // If an attribute is removed from the BrowserPlugin, then remove it
499 // from the <webview> as well.
500 this.webviewNode_.removeAttribute(name);
505 * @private
507 WebViewInternal.prototype.getEvents_ = function() {
508 var experimentalEvents = this.maybeGetExperimentalEvents_();
509 for (var eventName in experimentalEvents) {
510 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
512 return WEB_VIEW_EVENTS;
515 WebViewInternal.prototype.handleSizeChangedEvent_ =
516 function(event, webViewEvent) {
517 var node = this.webviewNode_;
519 var width = node.offsetWidth;
520 var height = node.offsetHeight;
522 // Check the current bounds to make sure we do not resize <webview>
523 // outside of current constraints.
524 var maxWidth;
525 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
526 node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
527 maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
528 } else {
529 maxWidth = width;
532 var minWidth;
533 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
534 node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
535 minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
536 } else {
537 minWidth = width;
539 if (minWidth > maxWidth) {
540 minWidth = maxWidth;
543 var maxHeight;
544 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
545 node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
546 maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
547 } else {
548 maxHeight = height;
550 var minHeight;
551 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
552 node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
553 minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
554 } else {
555 minHeight = height;
557 if (minHeight > maxHeight) {
558 minHeight = maxHeight;
561 if (webViewEvent.newWidth >= minWidth &&
562 webViewEvent.newWidth <= maxWidth &&
563 webViewEvent.newHeight >= minHeight &&
564 webViewEvent.newHeight <= maxHeight) {
565 node.style.width = webViewEvent.newWidth + 'px';
566 node.style.height = webViewEvent.newHeight + 'px';
568 node.dispatchEvent(webViewEvent);
572 * @private
574 WebViewInternal.prototype.setupWebviewNodeEvents_ = function() {
575 var self = this;
576 this.viewInstanceId_ = IdGenerator.GetNextId();
577 var onInstanceIdAllocated = function(e) {
578 var detail = e.detail ? JSON.parse(e.detail) : {};
579 self.instanceId_ = detail.windowId;
580 var params = {
581 'api': 'webview',
582 'instanceId': self.viewInstanceId_
584 if (self.userAgentOverride_) {
585 params['userAgentOverride'] = self.userAgentOverride_;
587 self.browserPluginNode_['-internal-attach'](params);
589 var events = self.getEvents_();
590 for (var eventName in events) {
591 self.setupEvent_(eventName, events[eventName]);
594 this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
595 onInstanceIdAllocated);
596 this.setupWebRequestEvents_();
598 this.on_ = {};
599 var events = self.getEvents_();
600 for (var eventName in events) {
601 this.setupEventProperty_(eventName);
606 * @private
608 WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) {
609 var self = this;
610 var webviewNode = this.webviewNode_;
611 eventInfo.evt.addListener(function(event) {
612 var details = {bubbles:true};
613 if (eventInfo.cancelable)
614 details.cancelable = true;
615 var webViewEvent = new Event(eventName, details);
616 $Array.forEach(eventInfo.fields, function(field) {
617 if (event[field] !== undefined) {
618 webViewEvent[field] = event[field];
621 if (eventInfo.customHandler) {
622 eventInfo.customHandler(self, event, webViewEvent);
623 return;
625 webviewNode.dispatchEvent(webViewEvent);
626 }, {instanceId: self.instanceId_});
630 * Adds an 'on<event>' property on the webview, which can be used to set/unset
631 * an event handler.
632 * @private
634 WebViewInternal.prototype.setupEventProperty_ = function(eventName) {
635 var propertyName = 'on' + eventName.toLowerCase();
636 var self = this;
637 var webviewNode = this.webviewNode_;
638 Object.defineProperty(webviewNode, propertyName, {
639 get: function() {
640 return self.on_[propertyName];
642 set: function(value) {
643 if (self.on_[propertyName])
644 webviewNode.removeEventListener(eventName, self.on_[propertyName]);
645 self.on_[propertyName] = value;
646 if (value)
647 webviewNode.addEventListener(eventName, value);
649 enumerable: true
654 * @private
656 WebViewInternal.prototype.getPermissionTypes_ = function() {
657 return ['media', 'geolocation', 'pointerLock', 'download', 'loadplugin'];
661 * @private
663 WebViewInternal.prototype.handleLoadAbortEvent_ =
664 function(event, webViewEvent) {
665 var showWarningMessage = function(reason) {
666 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
667 'The load has aborted with reason "%1".';
668 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
670 if (this.webviewNode_.dispatchEvent(webViewEvent)) {
671 showWarningMessage(event.reason);
676 * @private
678 WebViewInternal.prototype.handleLoadCommitEvent_ =
679 function(event, webViewEvent) {
680 this.currentEntryIndex_ = event.currentEntryIndex;
681 this.entryCount_ = event.entryCount;
682 this.processId_ = event.processId;
683 var oldValue = this.webviewNode_.getAttribute('src');
684 var newValue = event.url;
685 if (event.isTopLevel && (oldValue != newValue)) {
686 // Touching the src attribute triggers a navigation. To avoid
687 // triggering a page reload on every guest-initiated navigation,
688 // we use the flag ignoreNextSrcAttributeChange_ here.
689 this.ignoreNextSrcAttributeChange_ = true;
690 this.webviewNode_.setAttribute('src', newValue);
692 this.webviewNode_.dispatchEvent(webViewEvent);
696 * @private
698 WebViewInternal.prototype.handleNewWindowEvent_ =
699 function(event, webViewEvent) {
700 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
701 'An action has already been taken for this "newwindow" event.';
703 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
704 'Unable to attach the new window to the provided webview.';
706 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
708 var showWarningMessage = function() {
709 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
710 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
713 var self = this;
714 var browserPluginNode = this.browserPluginNode_;
715 var webviewNode = this.webviewNode_;
717 var requestId = event.requestId;
718 var actionTaken = false;
720 var validateCall = function () {
721 if (actionTaken) {
722 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
724 actionTaken = true;
727 var windowObj = {
728 attach: function(webview) {
729 validateCall();
730 if (!webview)
731 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
732 // Attach happens asynchronously to give the tagWatcher an opportunity
733 // to pick up the new webview before attach operates on it, if it hasn't
734 // been attached to the DOM already.
735 // Note: Any subsequent errors cannot be exceptions because they happen
736 // asynchronously.
737 setTimeout(function() {
738 var attached =
739 browserPluginNode['-internal-attachWindowTo'](webview,
740 event.windowId);
741 if (!attached) {
742 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
744 // If the object being passed into attach is not a valid <webview>
745 // then we will fail and it will be treated as if the new window
746 // was rejected. The permission API plumbing is used here to clean
747 // up the state created for the new window if attaching fails.
748 WebView.setPermission(
749 self.instanceId_, requestId, attached ? 'allow' : 'deny');
750 }, 0);
752 discard: function() {
753 validateCall();
754 WebView.setPermission(self.instanceId_, requestId, 'deny');
757 webViewEvent.window = windowObj;
759 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
760 if (actionTaken) {
761 return;
764 if (defaultPrevented) {
765 // Make browser plugin track lifetime of |windowObj|.
766 MessagingNatives.BindToGC(windowObj, function() {
767 // Avoid showing a warning message if the decision has already been made.
768 if (actionTaken) {
769 return;
771 WebView.setPermission(
772 self.instanceId_, requestId, 'default', '', function(allowed) {
773 if (allowed) {
774 return;
776 showWarningMessage();
779 } else {
780 actionTaken = true;
781 // The default action is to discard the window.
782 WebView.setPermission(
783 self.instanceId_, requestId, 'default', '', function(allowed) {
784 if (allowed) {
785 return;
787 showWarningMessage();
792 WebViewInternal.prototype.handlePermissionEvent_ =
793 function(event, webViewEvent) {
794 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
795 'Permission has already been decided for this "permissionrequest" event.';
797 var showWarningMessage = function(permission) {
798 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
799 'The permission request for "%1" has been denied.';
800 window.console.warn(
801 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
804 var requestId = event.requestId;
805 var self = this;
807 var PERMISSION_TYPES = this.getPermissionTypes_().concat(
808 this.maybeGetExperimentalPermissions_());
809 if (PERMISSION_TYPES.indexOf(event.permission) < 0) {
810 // The permission type is not allowed. Trigger the default response.
811 WebView.setPermission(
812 self.instanceId_, requestId, 'default', '', function(allowed) {
813 if (allowed) {
814 return;
816 showWarningMessage(event.permission);
818 return;
821 var browserPluginNode = this.browserPluginNode_;
822 var webviewNode = this.webviewNode_;
824 var decisionMade = false;
826 var validateCall = function() {
827 if (decisionMade) {
828 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
830 decisionMade = true;
833 // Construct the event.request object.
834 var request = {
835 allow: function() {
836 validateCall();
837 WebView.setPermission(self.instanceId_, requestId, 'allow');
839 deny: function() {
840 validateCall();
841 WebView.setPermission(self.instanceId_, requestId, 'deny');
844 webViewEvent.request = request;
846 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
847 if (decisionMade) {
848 return;
851 if (defaultPrevented) {
852 // Make browser plugin track lifetime of |request|.
853 MessagingNatives.BindToGC(request, function() {
854 // Avoid showing a warning message if the decision has already been made.
855 if (decisionMade) {
856 return;
858 WebView.setPermission(
859 self.instanceId_, requestId, 'default', '', function(allowed) {
860 if (allowed) {
861 return;
863 showWarningMessage(event.permission);
866 } else {
867 decisionMade = true;
868 WebView.setPermission(
869 self.instanceId_, requestId, 'default', '', function(allowed) {
870 if (allowed) {
871 return;
873 showWarningMessage(event.permission);
879 * @private
881 WebViewInternal.prototype.setupWebRequestEvents_ = function() {
882 var self = this;
883 var request = {};
884 var createWebRequestEvent = function(webRequestEvent) {
885 return function() {
886 if (!self[webRequestEvent.name + '_']) {
887 self[webRequestEvent.name + '_'] =
888 new WebRequestEvent(
889 'webview.' + webRequestEvent.name,
890 webRequestEvent.parameters,
891 webRequestEvent.extraParameters, webRequestEvent.options,
892 self.viewInstanceId_);
894 return self[webRequestEvent.name + '_'];
898 for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
899 var eventSchema = DeclarativeWebRequestSchema.events[i];
900 var webRequestEvent = createWebRequestEvent(eventSchema);
901 this.maybeAttachWebRequestEventToObject_(request,
902 eventSchema.name,
903 webRequestEvent);
906 // Populate the WebRequest events from the API definition.
907 for (var i = 0; i < WebRequestSchema.events.length; ++i) {
908 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
909 Object.defineProperty(
910 request,
911 WebRequestSchema.events[i].name,
913 get: webRequestEvent,
914 enumerable: true
917 this.maybeAttachWebRequestEventToObject_(this.webviewNode_,
918 WebRequestSchema.events[i].name,
919 webRequestEvent);
921 Object.defineProperty(
922 this.webviewNode_,
923 'request',
925 value: request,
926 enumerable: true,
927 writable: false
932 /** @private */
933 WebViewInternal.prototype.getUserAgent_ = function() {
934 return this.userAgentOverride_ || navigator.userAgent;
937 /** @private */
938 WebViewInternal.prototype.isUserAgentOverridden_ = function() {
939 return !!this.userAgentOverride_ &&
940 this.userAgentOverride_ != navigator.userAgent;
943 /** @private */
944 WebViewInternal.prototype.setUserAgentOverride_ = function(userAgentOverride) {
945 this.userAgentOverride_ = userAgentOverride;
946 if (!this.instanceId_) {
947 // If we are not attached yet, then we will pick up the user agent on
948 // attachment.
949 return;
951 WebView.overrideUserAgent(this.instanceId_, userAgentOverride);
954 // Registers browser plugin <object> custom element.
955 function registerBrowserPluginElement() {
956 var proto = Object.create(HTMLObjectElement.prototype);
958 proto.createdCallback = function() {
959 this.setAttribute('type', 'application/browser-plugin');
960 // The <object> node fills in the <webview> container.
961 this.style.width = '100%';
962 this.style.height = '100%';
965 proto.attributeChangedCallback = function(name, oldValue, newValue) {
966 if (!this.internal_) {
967 return;
969 var internal = this.internal_(secret);
970 internal.handleBrowserPluginAttributeMutation_(name, newValue);
973 proto.attachedCallback = function() {
974 // Load the plugin immediately.
975 var unused = this.nonExistentAttribute;
978 WebViewInternal.BrowserPlugin =
979 DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
980 prototype: proto});
982 delete proto.createdCallback;
983 delete proto.attachedCallback;
984 delete proto.detachedCallback;
985 delete proto.attributeChangedCallback;
988 // Registers <webview> custom element.
989 function registerWebViewElement() {
990 var proto = Object.create(HTMLElement.prototype);
992 proto.createdCallback = function() {
993 new WebViewInternal(this);
996 proto.attributeChangedCallback = function(name, oldValue, newValue) {
997 if (!this.internal_) {
998 return;
1000 var internal = this.internal_(secret);
1001 internal.handleWebviewAttributeMutation_(name, oldValue, newValue);
1004 proto.back = function() {
1005 this.go(-1);
1008 proto.forward = function() {
1009 this.go(1);
1012 proto.canGoBack = function() {
1013 return this.internal_(secret).canGoBack_();
1016 proto.canGoForward = function() {
1017 return this.internal_(secret).canGoForward_();
1020 proto.getProcessId = function() {
1021 return this.internal_(secret).getProcessId_();
1024 proto.go = function(relativeIndex) {
1025 this.internal_(secret).go_(relativeIndex);
1028 proto.reload = function() {
1029 this.internal_(secret).reload_();
1032 proto.stop = function() {
1033 this.internal_(secret).stop_();
1036 proto.terminate = function() {
1037 this.internal_(secret).terminate_();
1040 proto.executeScript = function(var_args) {
1041 var internal = this.internal_(secret);
1042 $Function.apply(internal.executeScript_, internal, arguments);
1045 proto.insertCSS = function(var_args) {
1046 var internal = this.internal_(secret);
1047 $Function.apply(internal.insertCSS_, internal, arguments);
1050 proto.getUserAgent = function() {
1051 return this.internal_(secret).getUserAgent_();
1054 proto.isUserAgentOverridden = function() {
1055 return this.internal_(secret).isUserAgentOverridden_();
1058 proto.setUserAgentOverride = function(userAgentOverride) {
1059 this.internal_(secret).setUserAgentOverride_(userAgentOverride);
1061 WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret);
1063 window.WebView =
1064 DocumentNatives.RegisterElement('webview', {prototype: proto});
1066 // Delete the callbacks so developers cannot call them and produce unexpected
1067 // behavior.
1068 delete proto.createdCallback;
1069 delete proto.attachedCallback;
1070 delete proto.detachedCallback;
1071 delete proto.attributeChangedCallback;
1074 var useCapture = true;
1075 window.addEventListener('readystatechange', function listener(event) {
1076 if (document.readyState == 'loading')
1077 return;
1079 registerBrowserPluginElement();
1080 registerWebViewElement();
1081 window.removeEventListener(event.type, listener, useCapture);
1082 }, useCapture);
1085 * Implemented when the experimental API is available.
1086 * @private
1088 WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {};
1091 * Implemented when the experimental API is available.
1092 * @private
1094 WebViewInternal.prototype.maybeAttachWebRequestEventToObject_ = function() {};
1097 * Implemented when the experimental API is available.
1098 * @private
1100 WebViewInternal.prototype.maybeGetExperimentalPermissions_ = function() {
1101 return [];
1104 exports.WebView = WebView;
1105 exports.WebViewInternal = WebViewInternal;
1106 exports.CreateEvent = CreateEvent;