// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Shim that simulates a tag via Mutation Observers. // // The actual tag is implemented via the browser plugin. The internals of this // are hidden via Shadow DOM. 'use strict'; var DocumentNatives = requireNative('document_natives'); var EventBindings = require('event_bindings'); var IdGenerator = requireNative('id_generator'); var MessagingNatives = requireNative('messaging_natives'); var WebRequestEvent = require('webRequestInternal').WebRequestEvent; var WebRequestSchema = requireNative('schema_registry').GetSchema('webRequest'); var DeclarativeWebRequestSchema = requireNative('schema_registry').GetSchema('declarativeWebRequest'); var WebView = require('binding').Binding.create('webview').generate(); // This secret enables hiding private members from the outside scope. // Outside of this file, |secret| is inaccessible. The only way to access the // element's internal members is via the |secret|. Since it's only // accessible by code here (and in web_view_experimental), only 's // API can access it and not external developers. var secret = {}; var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight'; var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth'; var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight'; var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth'; /** @type {Array.} */ var WEB_VIEW_ATTRIBUTES = [ 'allowtransparency', 'autosize', 'name', 'partition', WEB_VIEW_ATTRIBUTE_MINHEIGHT, WEB_VIEW_ATTRIBUTE_MINWIDTH, WEB_VIEW_ATTRIBUTE_MAXHEIGHT, WEB_VIEW_ATTRIBUTE_MAXWIDTH ]; var CreateEvent = function(name) { var eventOpts = {supportsListeners: true, supportsFilters: true}; return new EventBindings.Event(name, undefined, eventOpts); }; var WEB_VIEW_EVENTS = { 'close': { evt: CreateEvent('webview.onClose'), fields: [] }, 'consolemessage': { evt: CreateEvent('webview.onConsoleMessage'), fields: ['level', 'message', 'line', 'sourceId'] }, 'contentload': { evt: CreateEvent('webview.onContentLoad'), fields: [] }, 'exit': { evt: CreateEvent('webview.onExit'), fields: ['processId', 'reason'] }, 'loadabort': { cancelable: true, customHandler: function(webViewInternal, event, webViewEvent) { webViewInternal.handleLoadAbortEvent_(event, webViewEvent); }, evt: CreateEvent('webview.onLoadAbort'), fields: ['url', 'isTopLevel', 'reason'] }, 'loadcommit': { customHandler: function(webViewInternal, event, webViewEvent) { webViewInternal.handleLoadCommitEvent_(event, webViewEvent); }, evt: CreateEvent('webview.onLoadCommit'), fields: ['url', 'isTopLevel'] }, 'loadprogress': { evt: CreateEvent('webview.onLoadProgress'), fields: ['url', 'progress'] }, 'loadredirect': { evt: CreateEvent('webview.onLoadRedirect'), fields: ['isTopLevel', 'oldUrl', 'newUrl'] }, 'loadstart': { evt: CreateEvent('webview.onLoadStart'), fields: ['url', 'isTopLevel'] }, 'loadstop': { evt: CreateEvent('webview.onLoadStop'), fields: [] }, 'newwindow': { cancelable: true, customHandler: function(webViewInternal, event, webViewEvent) { webViewInternal.handleNewWindowEvent_(event, webViewEvent); }, evt: CreateEvent('webview.onNewWindow'), fields: [ 'initialHeight', 'initialWidth', 'targetUrl', 'windowOpenDisposition', 'name' ] }, 'permissionrequest': { cancelable: true, customHandler: function(webViewInternal, event, webViewEvent) { webViewInternal.handlePermissionEvent_(event, webViewEvent); }, evt: CreateEvent('webview.onPermissionRequest'), fields: [ 'identifier', 'lastUnlockedBySelf', 'name', 'permission', 'requestMethod', 'url', 'userGesture' ] }, 'responsive': { evt: CreateEvent('webview.onResponsive'), fields: ['processId'] }, 'sizechanged': { evt: CreateEvent('webview.onSizeChanged'), customHandler: function(webViewInternal, event, webViewEvent) { webViewInternal.handleSizeChangedEvent_(event, webViewEvent); }, fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] }, 'unresponsive': { evt: CreateEvent('webview.onUnresponsive'), fields: ['processId'] } }; // Implemented when the experimental API is available. WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {} /** * @constructor */ function WebViewInternal(webviewNode) { this.webviewNode_ = webviewNode; this.browserPluginNode_ = this.createBrowserPluginNode_(); var shadowRoot = this.webviewNode_.webkitCreateShadowRoot(); shadowRoot.appendChild(this.browserPluginNode_); this.setupWebviewNodeAttributes_(); this.setupFocusPropagation_(); this.setupWebviewNodeProperties_(); this.setupWebviewNodeEvents_(); } /** * @private */ WebViewInternal.prototype.createBrowserPluginNode_ = function() { // We create BrowserPlugin as a custom element in order to observe changes // to attributes synchronously. var browserPluginNode = new WebViewInternal.BrowserPlugin(); Object.defineProperty(browserPluginNode, 'internal_', { enumerable: false, writable: false, value: function(key) { if (key !== secret) { return null; } return this; }.bind(this) }); var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']); $Array.forEach(ALL_ATTRIBUTES, function(attributeName) { // Only copy attributes that have been assigned values, rather than copying // a series of undefined attributes to BrowserPlugin. if (this.webviewNode_.hasAttribute(attributeName)) { browserPluginNode.setAttribute( attributeName, this.webviewNode_.getAttribute(attributeName)); } else if (this.webviewNode_[attributeName]){ // Reading property using has/getAttribute does not work on // document.DOMContentLoaded event (but works on // window.DOMContentLoaded event). // So copy from property if copying from attribute fails. browserPluginNode.setAttribute( attributeName, this.webviewNode_[attributeName]); } }, this); return browserPluginNode; }; /** * @private */ WebViewInternal.prototype.setupFocusPropagation_ = function() { if (!this.webviewNode_.hasAttribute('tabIndex')) { // needs a tabIndex in order to be focusable. // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute // to allow to be focusable. // See http://crbug.com/231664. this.webviewNode_.setAttribute('tabIndex', -1); } var self = this; this.webviewNode_.addEventListener('focus', function(e) { // Focus the BrowserPlugin when the takes focus. self.browserPluginNode_.focus(); }); this.webviewNode_.addEventListener('blur', function(e) { // Blur the BrowserPlugin when the loses focus. self.browserPluginNode_.blur(); }); }; /** * @private */ WebViewInternal.prototype.canGoBack_ = function() { return this.entryCount_ > 1 && this.currentEntryIndex_ > 0; }; /** * @private */ WebViewInternal.prototype.canGoForward_ = function() { return this.currentEntryIndex_ >= 0 && this.currentEntryIndex_ < (this.entryCount_ - 1); }; /** * @private */ WebViewInternal.prototype.getProcessId_ = function() { return this.processId_; }; /** * @private */ WebViewInternal.prototype.go_ = function(relativeIndex) { if (!this.instanceId_) { return; } WebView.go(this.instanceId_, relativeIndex); }; /** * @private */ WebViewInternal.prototype.reload_ = function() { if (!this.instanceId_) { return; } WebView.reload(this.instanceId_); }; /** * @private */ WebViewInternal.prototype.stop_ = function() { if (!this.instanceId_) { return; } WebView.stop(this.instanceId_); }; /** * @private */ WebViewInternal.prototype.terminate_ = function() { if (!this.instanceId_) { return; } WebView.terminate(this.instanceId_); }; /** * @private */ WebViewInternal.prototype.validateExecuteCodeCall_ = function() { var ERROR_MSG_CANNOT_INJECT_SCRIPT = ': ' + 'Script cannot be injected into content until the page has loaded.'; if (!this.instanceId_) { throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); } }; /** * @private */ WebViewInternal.prototype.executeScript_ = function(var_args) { this.validateExecuteCodeCall_(); var args = $Array.concat([this.instanceId_], $Array.slice(arguments)); $Function.apply(WebView.executeScript, null, args); }; /** * @private */ WebViewInternal.prototype.insertCSS_ = function(var_args) { this.validateExecuteCodeCall_(); var args = $Array.concat([this.instanceId_], $Array.slice(arguments)); $Function.apply(WebView.insertCSS, null, args); }; /** * @private */ WebViewInternal.prototype.setupWebviewNodeProperties_ = function() { var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = ': ' + 'contentWindow is not available at this time. It will become available ' + 'when the page has finished loading.'; var self = this; var browserPluginNode = this.browserPluginNode_; // Expose getters and setters for the attributes. $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) { Object.defineProperty(this.webviewNode_, attributeName, { get: function() { if (browserPluginNode.hasOwnProperty(attributeName)) { return browserPluginNode[attributeName]; } else { return browserPluginNode.getAttribute(attributeName); } }, set: function(value) { if (browserPluginNode.hasOwnProperty(attributeName)) { // Give the BrowserPlugin first stab at the attribute so that it can // throw an exception if there is a problem. This attribute will then // be propagated back to the . browserPluginNode[attributeName] = value; } else { browserPluginNode.setAttribute(attributeName, value); } }, enumerable: true }); }, this); // src does not quite behave the same as BrowserPlugin src, and so // we don't simply keep the two in sync. this.src_ = this.webviewNode_.getAttribute('src'); Object.defineProperty(this.webviewNode_, 'src', { get: function() { return self.src_; }, set: function(value) { self.webviewNode_.setAttribute('src', value); }, // No setter. enumerable: true }); // We cannot use {writable: true} property descriptor because we want a // dynamic getter value. Object.defineProperty(this.webviewNode_, 'contentWindow', { get: function() { if (browserPluginNode.contentWindow) return browserPluginNode.contentWindow; window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE); }, // No setter. enumerable: true }); }; /** * @private */ WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() { Object.defineProperty(this.webviewNode_, 'internal_', { enumerable: false, writable: false, value: function(key) { if (key !== secret) { return null; } return this; }.bind(this) }); this.setupWebViewSrcAttributeMutationObserver_(); }; /** * @private */ WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ = function() { // The purpose of this mutation observer is to catch assignment to the src // attribute without any changes to its value. This is useful in the case // where the webview guest has crashed and navigating to the same address // spawns off a new process. var self = this; this.srcObserver_ = new MutationObserver(function(mutations) { $Array.forEach(mutations, function(mutation) { var oldValue = mutation.oldValue; var newValue = self.webviewNode_.getAttribute(mutation.attributeName); if (oldValue != newValue) { return; } self.handleWebviewAttributeMutation_( mutation.attributeName, oldValue, newValue); }); }); var params = { attributes: true, attributeOldValue: true, attributeFilter: ['src'] }; this.srcObserver_.observe(this.webviewNode_, params); }; /** * @private */ WebViewInternal.prototype.handleWebviewAttributeMutation_ = function(name, oldValue, newValue) { // This observer monitors mutations to attributes of the and // updates the BrowserPlugin properties accordingly. In turn, updating // a BrowserPlugin property will update the corresponding BrowserPlugin // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more // details. if (name == 'src') { // We treat null attribute (attribute removed) and the empty string as // one case. oldValue = oldValue || ''; newValue = newValue || ''; // Once we have navigated, we don't allow clearing the src attribute. // Once enters a navigated state, it cannot be return back to a // placeholder state. if (newValue == '' && oldValue != '') { // src attribute changes normally initiate a navigation. We suppress // the next src attribute handler call to avoid reloading the page // on every guest-initiated navigation. this.ignoreNextSrcAttributeChange_ = true; this.webviewNode_.setAttribute('src', oldValue); return; } this.src_ = newValue; if (this.ignoreNextSrcAttributeChange_) { // Don't allow the src mutation observer to see this change. this.srcObserver_.takeRecords(); this.ignoreNextSrcAttributeChange_ = false; return; } } if (this.browserPluginNode_.hasOwnProperty(name)) { this.browserPluginNode_[name] = newValue; } else { this.browserPluginNode_.setAttribute(name, newValue); } }; /** * @private */ WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ = function(name, newValue) { // This observer monitors mutations to attributes of the BrowserPlugin and // updates the attributes accordingly. // |newValue| is null if the attribute |name| has been removed. if (newValue != null) { // Update the attribute to match the BrowserPlugin attribute. // Note: Calling setAttribute on will trigger its mutation // observer which will then propagate that attribute to BrowserPlugin. In // cases where we permit assigning a BrowserPlugin attribute the same value // again (such as navigation when crashed), this could end up in an infinite // loop. Thus, we avoid this loop by only updating the attribute // if the BrowserPlugin attributes differs from it. if (newValue != this.webviewNode_.getAttribute(name)) { this.webviewNode_.setAttribute(name, newValue); } } else { // If an attribute is removed from the BrowserPlugin, then remove it // from the as well. this.webviewNode_.removeAttribute(name); } }; /** * @private */ WebViewInternal.prototype.getEvents_ = function() { var experimentalEvents = this.maybeGetExperimentalEvents_(); for (var eventName in experimentalEvents) { WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; } return WEB_VIEW_EVENTS; }; WebViewInternal.prototype.handleSizeChangedEvent_ = function(event, webViewEvent) { var node = this.webviewNode_; var width = node.offsetWidth; var height = node.offsetHeight; // Check the current bounds to make sure we do not resize // outside of current constraints. var maxWidth; if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) && node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) { maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]; } else { maxWidth = width; } var minWidth; if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) && node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) { minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH]; } else { minWidth = width; } if (minWidth > maxWidth) { minWidth = maxWidth; } var maxHeight; if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) && node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) { maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]; } else { maxHeight = height; } var minHeight; if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) && node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) { minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]; } else { minHeight = height; } if (minHeight > maxHeight) { minHeight = maxHeight; } if (webViewEvent.newWidth >= minWidth && webViewEvent.newWidth <= maxWidth && webViewEvent.newHeight >= minHeight && webViewEvent.newHeight <= maxHeight) { node.style.width = webViewEvent.newWidth + 'px'; node.style.height = webViewEvent.newHeight + 'px'; } node.dispatchEvent(webViewEvent); }; /** * @private */ WebViewInternal.prototype.setupWebviewNodeEvents_ = function() { var self = this; this.viewInstanceId_ = IdGenerator.GetNextId(); var onInstanceIdAllocated = function(e) { var detail = e.detail ? JSON.parse(e.detail) : {}; self.instanceId_ = detail.windowId; var params = { 'api': 'webview', 'instanceId': self.viewInstanceId_ }; if (self.userAgentOverride_) { params['userAgentOverride'] = self.userAgentOverride_; } self.browserPluginNode_['-internal-attach'](params); var events = self.getEvents_(); for (var eventName in events) { self.setupEvent_(eventName, events[eventName]); } }; this.browserPluginNode_.addEventListener('-internal-instanceid-allocated', onInstanceIdAllocated); this.setupWebRequestEvents_(); this.on_ = {}; var events = self.getEvents_(); for (var eventName in events) { this.setupEventProperty_(eventName); } }; /** * @private */ WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) { var self = this; var webviewNode = this.webviewNode_; eventInfo.evt.addListener(function(event) { var details = {bubbles:true}; if (eventInfo.cancelable) details.cancelable = true; var webViewEvent = new Event(eventName, details); $Array.forEach(eventInfo.fields, function(field) { if (event[field] !== undefined) { webViewEvent[field] = event[field]; } }); if (eventInfo.customHandler) { eventInfo.customHandler(self, event, webViewEvent); return; } webviewNode.dispatchEvent(webViewEvent); }, {instanceId: self.instanceId_}); }; /** * Adds an 'on' property on the webview, which can be used to set/unset * an event handler. * @private */ WebViewInternal.prototype.setupEventProperty_ = function(eventName) { var propertyName = 'on' + eventName.toLowerCase(); var self = this; var webviewNode = this.webviewNode_; Object.defineProperty(webviewNode, propertyName, { get: function() { return self.on_[propertyName]; }, set: function(value) { if (self.on_[propertyName]) webviewNode.removeEventListener(eventName, self.on_[propertyName]); self.on_[propertyName] = value; if (value) webviewNode.addEventListener(eventName, value); }, enumerable: true }); }; /** * @private */ WebViewInternal.prototype.getPermissionTypes_ = function() { return ['media', 'geolocation', 'pointerLock', 'download', 'loadplugin']; }; /** * @private */ WebViewInternal.prototype.handleLoadAbortEvent_ = function(event, webViewEvent) { var showWarningMessage = function(reason) { var WARNING_MSG_LOAD_ABORTED = ': ' + 'The load has aborted with reason "%1".'; window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason)); }; if (this.webviewNode_.dispatchEvent(webViewEvent)) { showWarningMessage(event.reason); } }; /** * @private */ WebViewInternal.prototype.handleLoadCommitEvent_ = function(event, webViewEvent) { this.currentEntryIndex_ = event.currentEntryIndex; this.entryCount_ = event.entryCount; this.processId_ = event.processId; var oldValue = this.webviewNode_.getAttribute('src'); var newValue = event.url; if (event.isTopLevel && (oldValue != newValue)) { // Touching the src attribute triggers a navigation. To avoid // triggering a page reload on every guest-initiated navigation, // we use the flag ignoreNextSrcAttributeChange_ here. this.ignoreNextSrcAttributeChange_ = true; this.webviewNode_.setAttribute('src', newValue); } this.webviewNode_.dispatchEvent(webViewEvent); } /** * @private */ WebViewInternal.prototype.handleNewWindowEvent_ = function(event, webViewEvent) { var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = ': ' + 'An action has already been taken for this "newwindow" event.'; var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = ': ' + 'Unable to attach the new window to the provided webview.'; var ERROR_MSG_WEBVIEW_EXPECTED = ' element expected.'; var showWarningMessage = function() { var WARNING_MSG_NEWWINDOW_BLOCKED = ': A new window was blocked.'; window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED); }; var self = this; var browserPluginNode = this.browserPluginNode_; var webviewNode = this.webviewNode_; var requestId = event.requestId; var actionTaken = false; var validateCall = function () { if (actionTaken) { throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN); } actionTaken = true; }; var windowObj = { attach: function(webview) { validateCall(); if (!webview) throw new Error(ERROR_MSG_WEBVIEW_EXPECTED); // Attach happens asynchronously to give the tagWatcher an opportunity // to pick up the new webview before attach operates on it, if it hasn't // been attached to the DOM already. // Note: Any subsequent errors cannot be exceptions because they happen // asynchronously. setTimeout(function() { var attached = browserPluginNode['-internal-attachWindowTo'](webview, event.windowId); if (!attached) { window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); } // If the object being passed into attach is not a valid // then we will fail and it will be treated as if the new window // was rejected. The permission API plumbing is used here to clean // up the state created for the new window if attaching fails. WebView.setPermission( self.instanceId_, requestId, attached ? 'allow' : 'deny'); }, 0); }, discard: function() { validateCall(); WebView.setPermission(self.instanceId_, requestId, 'deny'); } }; webViewEvent.window = windowObj; var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); if (actionTaken) { return; } if (defaultPrevented) { // Make browser plugin track lifetime of |windowObj|. MessagingNatives.BindToGC(windowObj, function() { // Avoid showing a warning message if the decision has already been made. if (actionTaken) { return; } WebView.setPermission( self.instanceId_, requestId, 'default', '', function(allowed) { if (allowed) { return; } showWarningMessage(); }); }); } else { actionTaken = true; // The default action is to discard the window. WebView.setPermission( self.instanceId_, requestId, 'default', '', function(allowed) { if (allowed) { return; } showWarningMessage(); }); } }; WebViewInternal.prototype.handlePermissionEvent_ = function(event, webViewEvent) { var ERROR_MSG_PERMISSION_ALREADY_DECIDED = ': ' + 'Permission has already been decided for this "permissionrequest" event.'; var showWarningMessage = function(permission) { var WARNING_MSG_PERMISSION_DENIED = ': ' + 'The permission request for "%1" has been denied.'; window.console.warn( WARNING_MSG_PERMISSION_DENIED.replace('%1', permission)); }; var requestId = event.requestId; var self = this; var PERMISSION_TYPES = this.getPermissionTypes_().concat( this.maybeGetExperimentalPermissions_()); if (PERMISSION_TYPES.indexOf(event.permission) < 0) { // The permission type is not allowed. Trigger the default response. WebView.setPermission( self.instanceId_, requestId, 'default', '', function(allowed) { if (allowed) { return; } showWarningMessage(event.permission); }); return; } var browserPluginNode = this.browserPluginNode_; var webviewNode = this.webviewNode_; var decisionMade = false; var validateCall = function() { if (decisionMade) { throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED); } decisionMade = true; }; // Construct the event.request object. var request = { allow: function() { validateCall(); WebView.setPermission(self.instanceId_, requestId, 'allow'); }, deny: function() { validateCall(); WebView.setPermission(self.instanceId_, requestId, 'deny'); } }; webViewEvent.request = request; var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); if (decisionMade) { return; } if (defaultPrevented) { // Make browser plugin track lifetime of |request|. MessagingNatives.BindToGC(request, function() { // Avoid showing a warning message if the decision has already been made. if (decisionMade) { return; } WebView.setPermission( self.instanceId_, requestId, 'default', '', function(allowed) { if (allowed) { return; } showWarningMessage(event.permission); }); }); } else { decisionMade = true; WebView.setPermission( self.instanceId_, requestId, 'default', '', function(allowed) { if (allowed) { return; } showWarningMessage(event.permission); }); } }; /** * @private */ WebViewInternal.prototype.setupWebRequestEvents_ = function() { var self = this; var request = {}; var createWebRequestEvent = function(webRequestEvent) { return function() { if (!self[webRequestEvent.name + '_']) { self[webRequestEvent.name + '_'] = new WebRequestEvent( 'webview.' + webRequestEvent.name, webRequestEvent.parameters, webRequestEvent.extraParameters, webRequestEvent.options, self.viewInstanceId_); } return self[webRequestEvent.name + '_']; }; }; for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { var eventSchema = DeclarativeWebRequestSchema.events[i]; var webRequestEvent = createWebRequestEvent(eventSchema); this.maybeAttachWebRequestEventToObject_(request, eventSchema.name, webRequestEvent); } // Populate the WebRequest events from the API definition. for (var i = 0; i < WebRequestSchema.events.length; ++i) { var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]); Object.defineProperty( request, WebRequestSchema.events[i].name, { get: webRequestEvent, enumerable: true } ); this.maybeAttachWebRequestEventToObject_(this.webviewNode_, WebRequestSchema.events[i].name, webRequestEvent); } Object.defineProperty( this.webviewNode_, 'request', { value: request, enumerable: true, writable: false } ); }; // Registers browser plugin custom element. function registerBrowserPluginElement() { var proto = Object.create(HTMLObjectElement.prototype); proto.createdCallback = function() { this.setAttribute('type', 'application/browser-plugin'); // The node fills in the container. this.style.width = '100%'; this.style.height = '100%'; }; proto.attributeChangedCallback = function(name, oldValue, newValue) { if (!this.internal_) { return; } var internal = this.internal_(secret); internal.handleBrowserPluginAttributeMutation_(name, newValue); }; proto.attachedCallback = function() { // Load the plugin immediately. var unused = this.nonExistentAttribute; }; // TODO(dominicc): Remove this line once Custom Elements renames // enteredViewCallback to attachedCallback proto.enteredViewCallback = proto.attachedCallback; WebViewInternal.BrowserPlugin = DocumentNatives.RegisterElement('browser-plugin', {extends: 'object', prototype: proto}); delete proto.createdCallback; delete proto.attachedCallback; delete proto.detachedCallback; delete proto.attributeChangedCallback; // TODO(dominicc): Remove these lines once Custom Elements renames // enteredView, leftView callbacks to attached, detached // respectively. delete proto.enteredViewCallback; delete proto.leftViewCallback; } // Registers custom element. function registerWebViewElement() { var proto = Object.create(HTMLElement.prototype); proto.createdCallback = function() { new WebViewInternal(this); }; proto.attributeChangedCallback = function(name, oldValue, newValue) { var internal = this.internal_(secret); internal.handleWebviewAttributeMutation_(name, oldValue, newValue); }; proto.back = function() { this.go(-1); }; proto.forward = function() { this.go(1); }; proto.canGoBack = function() { return this.internal_(secret).canGoBack_(); }; proto.canGoForward = function() { return this.internal_(secret).canGoForward_(); }; proto.getProcessId = function() { return this.internal_(secret).getProcessId_(); }; proto.go = function(relativeIndex) { this.internal_(secret).go_(relativeIndex); }; proto.reload = function() { this.internal_(secret).reload_(); }; proto.stop = function() { this.internal_(secret).stop_(); }; proto.terminate = function() { this.internal_(secret).terminate_(); }; proto.executeScript = function(var_args) { var internal = this.internal_(secret); $Function.apply(internal.executeScript_, internal, arguments); }; proto.insertCSS = function(var_args) { var internal = this.internal_(secret); $Function.apply(internal.insertCSS_, internal, arguments); }; WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret); window.WebView = DocumentNatives.RegisterElement('webview', {prototype: proto}); // Delete the callbacks so developers cannot call them and produce unexpected // behavior. delete proto.createdCallback; delete proto.attachedCallback; delete proto.detachedCallback; delete proto.attributeChangedCallback; // TODO(dominicc): Remove these lines once Custom Elements renames // enteredView, leftView callbacks to attached, detached // respectively. delete proto.enteredViewCallback; delete proto.leftViewCallback; } var useCapture = true; window.addEventListener('readystatechange', function listener(event) { if (document.readyState == 'loading') return; registerBrowserPluginElement(); registerWebViewElement(); window.removeEventListener(event.type, listener, useCapture); }, useCapture); /** * Implemented when the experimental API is available. * @private */ WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {}; /** * Implemented when the experimental API is available. * @private */ WebViewInternal.prototype.maybeAttachWebRequestEventToObject_ = function() {}; /** * Implemented when the experimental API is available. * @private */ WebViewInternal.prototype.maybeGetExperimentalPermissions_ = function() { return []; }; exports.WebView = WebView; exports.WebViewInternal = WebViewInternal; exports.CreateEvent = CreateEvent;