diff options
author | lfg <lfg@chromium.org> | 2014-09-11 18:54:25 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-09-12 01:57:08 +0000 |
commit | a71ea760fdfc9ca27ce29e160fb393c961555465 (patch) | |
tree | 349cc1193729e9ccb8f9e044b2e548fed08f0615 /extensions/renderer | |
parent | 81d302348beed094f26e2cd8b647266eac8ef534 (diff) | |
download | chromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.zip chromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.tar.gz chromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.tar.bz2 |
Moving web_view.js to extensions.
BUG=352293
Review URL: https://codereview.chromium.org/564913003
Cr-Commit-Position: refs/heads/master@{#294519}
Diffstat (limited to 'extensions/renderer')
-rw-r--r-- | extensions/renderer/dispatcher.cc | 7 | ||||
-rw-r--r-- | extensions/renderer/resources/extensions_renderer_resources.grd | 4 | ||||
-rw-r--r-- | extensions/renderer/resources/web_view.js | 1041 | ||||
-rw-r--r-- | extensions/renderer/resources/web_view_deny.js | 38 | ||||
-rw-r--r-- | extensions/renderer/resources/web_view_events.js | 621 | ||||
-rw-r--r-- | extensions/renderer/resources/web_view_experimental.js | 31 |
6 files changed, 1742 insertions, 0 deletions
diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc index 10b26c5..163218c 100644 --- a/extensions/renderer/dispatcher.cc +++ b/extensions/renderer/dispatcher.cc @@ -519,8 +519,15 @@ std::vector<std::pair<std::string, int> > Dispatcher::GetJsResources() { IDR_UNCAUGHT_EXCEPTION_HANDLER_JS)); resources.push_back(std::make_pair("unload_event", IDR_UNLOAD_EVENT_JS)); resources.push_back(std::make_pair("utils", IDR_UTILS_JS)); + // Note: webView not webview so that this doesn't interfere with the + // chrome.webview API bindings. + resources.push_back(std::make_pair("webView", IDR_WEB_VIEW_JS)); + resources.push_back(std::make_pair("webViewEvents", IDR_WEB_VIEW_EVENTS_JS)); + resources.push_back( + std::make_pair("webViewExperimental", IDR_WEB_VIEW_EXPERIMENTAL_JS)); resources.push_back(std::make_pair("webViewInternal", IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS)); + resources.push_back(std::make_pair("denyWebView", IDR_WEB_VIEW_DENY_JS)); resources.push_back( std::make_pair(mojo::kBufferModuleName, IDR_MOJO_BUFFER_JS)); resources.push_back( diff --git a/extensions/renderer/resources/extensions_renderer_resources.grd b/extensions/renderer/resources/extensions_renderer_resources.grd index 488443c..3f5d610 100644 --- a/extensions/renderer/resources/extensions_renderer_resources.grd +++ b/extensions/renderer/resources/extensions_renderer_resources.grd @@ -31,7 +31,11 @@ <include name="IDR_UNCAUGHT_EXCEPTION_HANDLER_JS" file="uncaught_exception_handler.js" type="BINDATA" /> <include name="IDR_UNLOAD_EVENT_JS" file="unload_event.js" type="BINDATA" /> <include name="IDR_UTILS_JS" file="utils.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_DENY_JS" file="web_view_deny.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_EVENTS_JS" file="web_view_events.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_EXPERIMENTAL_JS" file="web_view_experimental.js" type="BINDATA" /> <include name="IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS" file="web_view_internal.js" type="BINDATA" /> + <include name="IDR_WEB_VIEW_JS" file="web_view.js" type="BINDATA" /> <!-- Custom bindings for APIs. --> <include name="IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS" file="app_runtime_custom_bindings.js" type="BINDATA" /> diff --git a/extensions/renderer/resources/web_view.js b/extensions/renderer/resources/web_view.js new file mode 100644 index 0000000..610c65f --- /dev/null +++ b/extensions/renderer/resources/web_view.js @@ -0,0 +1,1041 @@ +// 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. + +// This module implements Webview (<webview>) as a custom element that wraps a +// BrowserPlugin object element. The object element is hidden within +// the shadow DOM of the Webview element. + +var DocumentNatives = requireNative('document_natives'); +var GuestViewInternal = + require('binding').Binding.create('guestViewInternal').generate(); +var IdGenerator = requireNative('id_generator'); +// TODO(lazyboy): Rename this to WebViewInternal and call WebViewInternal +// something else. +var WebView = require('webViewInternal').WebView; +var WebViewEvents = require('webViewEvents').WebViewEvents; +var guestViewInternalNatives = requireNative('guest_view_internal'); + +var WEB_VIEW_ATTRIBUTE_AUTOSIZE = 'autosize'; +var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight'; +var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth'; +var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight'; +var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth'; +var AUTO_SIZE_ATTRIBUTES = [ + WEB_VIEW_ATTRIBUTE_AUTOSIZE, + WEB_VIEW_ATTRIBUTE_MAXHEIGHT, + WEB_VIEW_ATTRIBUTE_MAXWIDTH, + WEB_VIEW_ATTRIBUTE_MINHEIGHT, + WEB_VIEW_ATTRIBUTE_MINWIDTH +]; + +var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition'; + +var ERROR_MSG_ALREADY_NAVIGATED = + 'The object has already navigated, so its partition cannot be changed.'; +var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.'; + +/** @type {Array.<string>} */ +var WEB_VIEW_ATTRIBUTES = [ + 'allowtransparency', +]; + +/** @class representing state of storage partition. */ +function Partition() { + this.validPartitionId = true; + this.persistStorage = false; + this.storagePartitionId = ''; +}; + +Partition.prototype.toAttribute = function() { + if (!this.validPartitionId) { + return ''; + } + return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId; +}; + +Partition.prototype.fromAttribute = function(value, hasNavigated) { + var result = {}; + if (hasNavigated) { + result.error = ERROR_MSG_ALREADY_NAVIGATED; + return result; + } + if (!value) { + value = ''; + } + + var LEN = 'persist:'.length; + if (value.substr(0, LEN) == 'persist:') { + value = value.substr(LEN); + if (!value) { + this.validPartitionId = false; + result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; + return result; + } + this.persistStorage = true; + } else { + this.persistStorage = false; + } + + this.storagePartitionId = value; + return result; +}; + +// Implemented when the experimental API is available. +WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {} + +/** + * @constructor + */ +function WebViewInternal(webviewNode) { + privates(webviewNode).internal = this; + this.webviewNode = webviewNode; + this.attached = false; + this.elementAttached = false; + + this.beforeFirstNavigation = true; + this.validPartitionId = true; + // Used to save some state upon deferred attachment. + // If <object> bindings is not available, we defer attachment. + // This state contains whether or not the attachment request was for + // newwindow. + this.deferredAttachState = null; + + // on* Event handlers. + this.on = {}; + + this.browserPluginNode = this.createBrowserPluginNode(); + var shadowRoot = this.webviewNode.createShadowRoot(); + this.partition = new Partition(); + + this.setupWebviewNodeAttributes(); + this.setupFocusPropagation(); + this.setupWebviewNodeProperties(); + + this.viewInstanceId = IdGenerator.GetNextId(); + + new WebViewEvents(this, this.viewInstanceId); + + shadowRoot.appendChild(this.browserPluginNode); +} + +/** + * @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(); + privates(browserPluginNode).internal = this; + + $Array.forEach(WEB_VIEW_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; +}; + +WebViewInternal.prototype.getGuestInstanceId = function() { + return this.guestInstanceId; +}; + +/** + * Resets some state upon reattaching <webview> element to the DOM. + */ +WebViewInternal.prototype.reset = function() { + // If guestInstanceId is defined then the <webview> has navigated and has + // already picked up a partition ID. Thus, we need to reset the initialization + // state. However, it may be the case that beforeFirstNavigation is false BUT + // guestInstanceId has yet to be initialized. This means that we have not + // heard back from createGuest yet. We will not reset the flag in this case so + // that we don't end up allocating a second guest. + if (this.guestInstanceId) { + this.guestInstanceId = undefined; + this.beforeFirstNavigation = true; + this.validPartitionId = true; + this.partition.validPartitionId = true; + } + this.internalInstanceId = 0; +}; + +// Sets <webview>.request property. +WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) { + Object.defineProperty( + this.webviewNode, + 'request', + { + value: request, + enumerable: true + } + ); +}; + +WebViewInternal.prototype.setupFocusPropagation = function() { + if (!this.webviewNode.hasAttribute('tabIndex')) { + // <webview> needs a tabIndex in order to be focusable. + // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute + // to allow <webview> 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 <webview> takes focus. + self.browserPluginNode.focus(); + }); + this.webviewNode.addEventListener('blur', function(e) { + // Blur the BrowserPlugin when the <webview> loses focus. + self.browserPluginNode.blur(); + }); +}; + +/** + * @private + */ +WebViewInternal.prototype.back = function() { + return this.go(-1); +}; + +/** + * @private + */ +WebViewInternal.prototype.forward = function() { + return this.go(1); +}; + +/** + * @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.clearData = function() { + if (!this.guestInstanceId) { + return; + } + var args = $Array.concat([this.guestInstanceId], $Array.slice(arguments)); + $Function.apply(WebView.clearData, null, args); +}; + +/** + * @private + */ +WebViewInternal.prototype.getProcessId = function() { + return this.processId; +}; + +/** + * @private + */ +WebViewInternal.prototype.go = function(relativeIndex) { + if (!this.guestInstanceId) { + return; + } + WebView.go(this.guestInstanceId, relativeIndex); +}; + +/** + * @private + */ +WebViewInternal.prototype.print = function() { + this.executeScript({code: 'window.print();'}); +}; + +/** + * @private + */ +WebViewInternal.prototype.reload = function() { + if (!this.guestInstanceId) { + return; + } + WebView.reload(this.guestInstanceId); +}; + +/** + * @private + */ +WebViewInternal.prototype.stop = function() { + if (!this.guestInstanceId) { + return; + } + WebView.stop(this.guestInstanceId); +}; + +/** + * @private + */ +WebViewInternal.prototype.terminate = function() { + if (!this.guestInstanceId) { + return; + } + WebView.terminate(this.guestInstanceId); +}; + +/** + * @private + */ +WebViewInternal.prototype.validateExecuteCodeCall = function() { + var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' + + 'Script cannot be injected into content until the page has loaded.'; + if (!this.guestInstanceId) { + throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); + } +}; + +/** + * @private + */ +WebViewInternal.prototype.executeScript = function(var_args) { + this.validateExecuteCodeCall(); + var args = $Array.concat([this.guestInstanceId, this.src], + $Array.slice(arguments)); + $Function.apply(WebView.executeScript, null, args); +}; + +/** + * @private + */ +WebViewInternal.prototype.insertCSS = function(var_args) { + this.validateExecuteCodeCall(); + var args = $Array.concat([this.guestInstanceId, this.src], + $Array.slice(arguments)); + $Function.apply(WebView.insertCSS, null, args); +}; + +WebViewInternal.prototype.setupAutoSizeProperties = function() { + var self = this; + $Array.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) { + this[attributeName] = this.webviewNode.getAttribute(attributeName); + Object.defineProperty(this.webviewNode, attributeName, { + get: function() { + return self[attributeName]; + }, + set: function(value) { + self.webviewNode.setAttribute(attributeName, value); + }, + enumerable: true + }); + }, this); +}; + +/** + * @private + */ +WebViewInternal.prototype.setupWebviewNodeProperties = function() { + var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' + + 'contentWindow is not available at this time. It will become available ' + + 'when the page has finished loading.'; + + this.setupAutoSizeProperties(); + 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 <webview>. + browserPluginNode[attributeName] = value; + } else { + browserPluginNode.setAttribute(attributeName, value); + } + }, + enumerable: true + }); + }, this); + + // <webview> 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 + }); + + Object.defineProperty(this.webviewNode, 'name', { + get: function() { + return self.name; + }, + set: function(value) { + self.webviewNode.setAttribute('name', value); + }, + enumerable: true + }); + + Object.defineProperty(this.webviewNode, 'partition', { + get: function() { + return self.partition.toAttribute(); + }, + set: function(value) { + var result = self.partition.fromAttribute(value, self.hasNavigated()); + if (result.error) { + throw result.error; + } + self.webviewNode.setAttribute('partition', value); + }, + 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() { + 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. + this.srcAndPartitionObserver = new MutationObserver(function(mutations) { + $Array.forEach(mutations, function(mutation) { + var oldValue = mutation.oldValue; + var newValue = this.webviewNode.getAttribute(mutation.attributeName); + if (oldValue != newValue) { + return; + } + this.handleWebviewAttributeMutation( + mutation.attributeName, oldValue, newValue); + }.bind(this)); + }.bind(this)); + var params = { + attributes: true, + attributeOldValue: true, + attributeFilter: ['src', 'partition'] + }; + this.srcAndPartitionObserver.observe(this.webviewNode, params); +}; + +/** + * @private + */ +WebViewInternal.prototype.handleWebviewAttributeMutation = + function(name, oldValue, newValue) { + // This observer monitors mutations to attributes of the <webview> 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 (AUTO_SIZE_ATTRIBUTES.indexOf(name) > -1) { + this[name] = newValue; + if (!this.guestInstanceId) { + return; + } + // Convert autosize attribute to boolean. + var autosize = this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE); + GuestViewInternal.setAutoSize(this.guestInstanceId, { + 'enableAutoSize': autosize, + 'min': { + 'width': parseInt(this.minwidth || 0), + 'height': parseInt(this.minheight || 0) + }, + 'max': { + 'width': parseInt(this.maxwidth || 0), + 'height': parseInt(this.maxheight || 0) + } + }); + return; + } else if (name == 'name') { + // We treat null attribute (attribute removed) and the empty string as + // one case. + oldValue = oldValue || ''; + newValue = newValue || ''; + + if (oldValue === newValue) { + return; + } + this.name = newValue; + if (!this.guestInstanceId) { + return; + } + WebView.setName(this.guestInstanceId, newValue); + return; + } else 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 <webview> 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.srcAndPartitionObserver.takeRecords(); + this.ignoreNextSrcAttributeChange = false; + return; + } + var result = {}; + this.parseSrcAttribute(result); + + if (result.error) { + throw result.error; + } + } else if (name == 'partition') { + // Note that throwing error here won't synchronously propagate. + this.partition.fromAttribute(newValue, this.hasNavigated()); + } + + // No <webview> -> <object> mutation propagation for these attributes. + if (name == 'src' || name == 'partition') { + return; + } + + if (this.browserPluginNode.hasOwnProperty(name)) { + this.browserPluginNode[name] = newValue; + } else { + this.browserPluginNode.setAttribute(name, newValue); + } +}; + +/** + * @private + */ +WebViewInternal.prototype.handleBrowserPluginAttributeMutation = + function(name, oldValue, newValue) { + if (name == 'internalinstanceid' && !oldValue && !!newValue) { + this.browserPluginNode.removeAttribute('internalinstanceid'); + this.internalInstanceId = parseInt(newValue); + + if (!this.deferredAttachState) { + this.parseAttributes(); + return; + } + + if (!!this.guestInstanceId && this.guestInstanceId != 0) { + window.setTimeout(function() { + var isNewWindow = this.deferredAttachState ? + this.deferredAttachState.isNewWindow : false; + var params = this.buildAttachParams(isNewWindow); + guestViewInternalNatives.AttachGuest( + this.internalInstanceId, + this.guestInstanceId, + params); + }.bind(this), 0); + } + + return; + } + + // This observer monitors mutations to attributes of the BrowserPlugin and + // updates the <webview> attributes accordingly. + // |newValue| is null if the attribute |name| has been removed. + if (newValue != null) { + // Update the <webview> attribute to match the BrowserPlugin attribute. + // Note: Calling setAttribute on <webview> 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 <webview> 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 <webview> as well. + this.webviewNode.removeAttribute(name); + } +}; + +WebViewInternal.prototype.onSizeChanged = function(webViewEvent) { + var newWidth = webViewEvent.newWidth; + var newHeight = webViewEvent.newHeight; + + var node = this.webviewNode; + + var width = node.offsetWidth; + var height = node.offsetHeight; + + // Check the current bounds to make sure we do not resize <webview> + // 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 (!this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE) || + (newWidth >= minWidth && + newWidth <= maxWidth && + newHeight >= minHeight && + newHeight <= maxHeight)) { + node.style.width = newWidth + 'px'; + node.style.height = newHeight + 'px'; + // Only fire the DOM event if the size of the <webview> has actually + // changed. + this.dispatchEvent(webViewEvent); + } +}; + +// Returns if <object> is in the render tree. +WebViewInternal.prototype.isPluginInRenderTree = function() { + return !!this.internalInstanceId && this.internalInstanceId != 0; +}; + +WebViewInternal.prototype.hasNavigated = function() { + return !this.beforeFirstNavigation; +}; + +/** @return {boolean} */ +WebViewInternal.prototype.parseSrcAttribute = function(result) { + if (!this.partition.validPartitionId) { + result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; + return false; + } + this.src = this.webviewNode.getAttribute('src'); + + if (!this.src) { + return true; + } + + if (!this.elementAttached) { + return true; + } + + if (!this.hasGuestInstanceID()) { + if (this.beforeFirstNavigation) { + this.beforeFirstNavigation = false; + this.allocateInstanceId(); + } + return true; + } + + // Navigate to this.src. + WebView.navigate(this.guestInstanceId, this.src); + return true; +}; + +/** @return {boolean} */ +WebViewInternal.prototype.parseAttributes = function() { + var hasNavigated = this.hasNavigated(); + var attributeValue = this.webviewNode.getAttribute('partition'); + var result = this.partition.fromAttribute(attributeValue, hasNavigated); + return this.parseSrcAttribute(result); +}; + +WebViewInternal.prototype.hasGuestInstanceID = function() { + return this.guestInstanceId != undefined; +}; + +WebViewInternal.prototype.allocateInstanceId = function() { + var storagePartitionId = + this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTE_PARTITION) || + this.webviewNode[WEB_VIEW_ATTRIBUTE_PARTITION]; + var params = { + 'storagePartitionId': storagePartitionId, + }; + var self = this; + GuestViewInternal.createGuest( + 'webview', + params, + function(guestInstanceId) { + // TODO(lazyboy): Make sure this.autoNavigate_ stuff correctly updated + // |self.src| at this point. + self.attachWindow(guestInstanceId, false); + }); +}; + +WebViewInternal.prototype.onFrameNameChanged = function(name) { + this.name = name || ''; + if (this.name === '') { + this.webviewNode.removeAttribute('name'); + } else { + this.webviewNode.setAttribute('name', this.name); + } +}; + +WebViewInternal.prototype.onPluginDestroyed = function() { + this.reset(); +}; + +WebViewInternal.prototype.dispatchEvent = function(webViewEvent) { + return this.webviewNode.dispatchEvent(webViewEvent); +}; + +/** + * Adds an 'on<event>' property on the webview, which can be used to set/unset + * an event handler. + */ +WebViewInternal.prototype.setupEventProperty = function(eventName) { + var propertyName = 'on' + eventName.toLowerCase(); + Object.defineProperty(this.webviewNode, propertyName, { + get: function() { + return this.on[propertyName]; + }.bind(this), + set: function(value) { + if (this.on[propertyName]) + this.webviewNode.removeEventListener(eventName, self.on[propertyName]); + this.on[propertyName] = value; + if (value) + this.webviewNode.addEventListener(eventName, value); + }.bind(this), + enumerable: true + }); +}; + +// Updates state upon loadcommit. +WebViewInternal.prototype.onLoadCommit = function( + currentEntryIndex, entryCount, processId, url, isTopLevel) { + this.currentEntryIndex = currentEntryIndex; + this.entryCount = entryCount; + this.processId = processId; + var oldValue = this.webviewNode.getAttribute('src'); + var newValue = url; + if (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); + } +}; + +WebViewInternal.prototype.onAttach = function(storagePartitionId) { + this.webviewNode.setAttribute('partition', storagePartitionId); + this.partition.fromAttribute(storagePartitionId, this.hasNavigated()); +}; + + +/** @private */ +WebViewInternal.prototype.getUserAgent = function() { + return this.userAgentOverride || navigator.userAgent; +}; + +/** @private */ +WebViewInternal.prototype.isUserAgentOverridden = function() { + return !!this.userAgentOverride && + this.userAgentOverride != navigator.userAgent; +}; + +/** @private */ +WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) { + this.userAgentOverride = userAgentOverride; + if (!this.guestInstanceId) { + // If we are not attached yet, then we will pick up the user agent on + // attachment. + return; + } + WebView.overrideUserAgent(this.guestInstanceId, userAgentOverride); +}; + +/** @private */ +WebViewInternal.prototype.find = function(search_text, options, callback) { + if (!this.guestInstanceId) { + return; + } + WebView.find(this.guestInstanceId, search_text, options, callback); +}; + +/** @private */ +WebViewInternal.prototype.stopFinding = function(action) { + if (!this.guestInstanceId) { + return; + } + WebView.stopFinding(this.guestInstanceId, action); +}; + +/** @private */ +WebViewInternal.prototype.setZoom = function(zoomFactor, callback) { + if (!this.guestInstanceId) { + return; + } + WebView.setZoom(this.guestInstanceId, zoomFactor, callback); +}; + +WebViewInternal.prototype.getZoom = function(callback) { + if (!this.guestInstanceId) { + return; + } + WebView.getZoom(this.guestInstanceId, callback); +}; + +WebViewInternal.prototype.buildAttachParams = function(isNewWindow) { + var params = { + 'autosize': this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE), + 'instanceId': this.viewInstanceId, + 'maxheight': parseInt(this.maxheight || 0), + 'maxwidth': parseInt(this.maxwidth || 0), + 'minheight': parseInt(this.minheight || 0), + 'minwidth': parseInt(this.minwidth || 0), + 'name': this.name, + // We don't need to navigate new window from here. + 'src': isNewWindow ? undefined : this.src, + // If we have a partition from the opener, that will also be already + // set via this.onAttach(). + 'storagePartitionId': this.partition.toAttribute(), + 'userAgentOverride': this.userAgentOverride + }; + return params; +}; + +WebViewInternal.prototype.attachWindow = function(guestInstanceId, + isNewWindow) { + this.guestInstanceId = guestInstanceId; + var params = this.buildAttachParams(isNewWindow); + + if (!this.isPluginInRenderTree()) { + this.deferredAttachState = {isNewWindow: isNewWindow}; + return true; + } + + this.deferredAttachState = null; + return guestViewInternalNatives.AttachGuest( + this.internalInstanceId, + this.guestInstanceId, + params); +}; + +// Registers browser plugin <object> custom element. +function registerBrowserPluginElement() { + var proto = Object.create(HTMLObjectElement.prototype); + + proto.createdCallback = function() { + this.setAttribute('type', 'application/browser-plugin'); + this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId()); + // The <object> node fills in the <webview> container. + this.style.width = '100%'; + this.style.height = '100%'; + }; + + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue); + }; + + proto.attachedCallback = function() { + // Load the plugin immediately. + var unused = this.nonExistentAttribute; + }; + + WebViewInternal.BrowserPlugin = + DocumentNatives.RegisterElement('browserplugin', {extends: 'object', + prototype: proto}); + + delete proto.createdCallback; + delete proto.attachedCallback; + delete proto.detachedCallback; + delete proto.attributeChangedCallback; +} + +// Registers <webview> custom element. +function registerWebViewElement() { + var proto = Object.create(HTMLElement.prototype); + + proto.createdCallback = function() { + new WebViewInternal(this); + }; + + proto.attributeChangedCallback = function(name, oldValue, newValue) { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.handleWebviewAttributeMutation(name, oldValue, newValue); + }; + + proto.detachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.elementAttached = false; + internal.reset(); + }; + + proto.attachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + if (!internal.elementAttached) { + internal.elementAttached = true; + internal.parseAttributes(); + } + }; + + var methods = [ + 'back', + 'find', + 'forward', + 'canGoBack', + 'canGoForward', + 'clearData', + 'getProcessId', + 'getZoom', + 'go', + 'print', + 'reload', + 'setZoom', + 'stop', + 'stopFinding', + 'terminate', + 'executeScript', + 'insertCSS', + 'getUserAgent', + 'isUserAgentOverridden', + 'setUserAgentOverride' + ]; + + // Forward proto.foo* method calls to WebViewInternal.foo*. + for (var i = 0; methods[i]; ++i) { + var createHandler = function(m) { + return function(var_args) { + var internal = privates(this).internal; + return $Function.apply(internal[m], internal, arguments); + }; + }; + proto[methods[i]] = createHandler(methods[i]); + } + + WebViewInternal.maybeRegisterExperimentalAPIs(proto); + + 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; +} + +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 ChromeWebView API is available. + * @private + */ +WebViewInternal.prototype.maybeGetChromeWebViewEvents = function() {}; + +/** + * Implemented when the experimental API is available. + * @private + */ +WebViewInternal.prototype.maybeGetExperimentalEvents = function() {}; + +/** + * Implemented when the experimental API is available. + * @private + */ +WebViewInternal.prototype.maybeGetExperimentalPermissions = function() { + return []; +}; + +/** + * Implemented when the experimental API is available. + * @private + */ +WebViewInternal.prototype.setupExperimentalContextMenus = function() { +}; + +exports.WebView = WebView; +exports.WebViewInternal = WebViewInternal; diff --git a/extensions/renderer/resources/web_view_deny.js b/extensions/renderer/resources/web_view_deny.js new file mode 100644 index 0000000..a3e70f4 --- /dev/null +++ b/extensions/renderer/resources/web_view_deny.js @@ -0,0 +1,38 @@ +// 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. + +var DocumentNatives = requireNative('document_natives'); + +// Output error message to console when using the <webview> tag with no +// permission. +var errorMessage = "You do not have permission to use the webview element." + + " Be sure to declare the 'webview' permission in your manifest file."; + +// Registers <webview> custom element. +function registerWebViewElement() { + var proto = Object.create(HTMLElement.prototype); + + proto.createdCallback = function() { + window.console.error(errorMessage); + }; + + 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; +} + +var useCapture = true; +window.addEventListener('readystatechange', function listener(event) { + if (document.readyState == 'loading') + return; + + registerWebViewElement(); + window.removeEventListener(event.type, listener, useCapture); +}, useCapture); diff --git a/extensions/renderer/resources/web_view_events.js b/extensions/renderer/resources/web_view_events.js new file mode 100644 index 0000000..32b500c --- /dev/null +++ b/extensions/renderer/resources/web_view_events.js @@ -0,0 +1,621 @@ +// Copyright 2014 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. + +// Event management for WebViewInternal. + +var DeclarativeWebRequestSchema = + requireNative('schema_registry').GetSchema('declarativeWebRequest'); +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 WebView = require('webViewInternal').WebView; + +var CreateEvent = function(name) { + var eventOpts = {supportsListeners: true, supportsFilters: true}; + return new EventBindings.Event(name, undefined, eventOpts); +}; + +var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged'); +var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed'); +var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage'); + +// WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their +// associated extension event descriptor objects. +// An event listener will be attached to the extension event |evt| specified in +// the descriptor. +// |fields| specifies the public-facing fields in the DOM event that are +// accessible to <webview> developers. +// |customHandler| allows a handler function to be called each time an extension +// event is caught by its event listener. The DOM event should be dispatched +// within this handler function. With no handler function, the DOM event +// will be dispatched by default each time the extension event is caught. +// |cancelable| (default: false) specifies whether the event's default +// behavior can be canceled. If the default action associated with the event +// is prevented, then its dispatch function will return false in its event +// handler. The event must have a custom handler for this to be meaningful. +var WEB_VIEW_EVENTS = { + 'close': { + evt: CreateEvent('webViewInternal.onClose'), + fields: [] + }, + 'consolemessage': { + evt: CreateEvent('webViewInternal.onConsoleMessage'), + fields: ['level', 'message', 'line', 'sourceId'] + }, + 'contentload': { + evt: CreateEvent('webViewInternal.onContentLoad'), + fields: [] + }, + 'dialog': { + cancelable: true, + customHandler: function(handler, event, webViewEvent) { + handler.handleDialogEvent(event, webViewEvent); + }, + evt: CreateEvent('webViewInternal.onDialog'), + fields: ['defaultPromptText', 'messageText', 'messageType', 'url'] + }, + 'exit': { + evt: CreateEvent('webViewInternal.onExit'), + fields: ['processId', 'reason'] + }, + 'findupdate': { + evt: CreateEvent('webViewInternal.onFindReply'), + fields: [ + 'searchText', + 'numberOfMatches', + 'activeMatchOrdinal', + 'selectionRect', + 'canceled', + 'finalUpdate' + ] + }, + 'loadabort': { + cancelable: true, + customHandler: function(handler, event, webViewEvent) { + handler.handleLoadAbortEvent(event, webViewEvent); + }, + evt: CreateEvent('webViewInternal.onLoadAbort'), + fields: ['url', 'isTopLevel', 'reason'] + }, + 'loadcommit': { + customHandler: function(handler, event, webViewEvent) { + handler.handleLoadCommitEvent(event, webViewEvent); + }, + evt: CreateEvent('webViewInternal.onLoadCommit'), + fields: ['url', 'isTopLevel'] + }, + 'loadprogress': { + evt: CreateEvent('webViewInternal.onLoadProgress'), + fields: ['url', 'progress'] + }, + 'loadredirect': { + evt: CreateEvent('webViewInternal.onLoadRedirect'), + fields: ['isTopLevel', 'oldUrl', 'newUrl'] + }, + 'loadstart': { + evt: CreateEvent('webViewInternal.onLoadStart'), + fields: ['url', 'isTopLevel'] + }, + 'loadstop': { + evt: CreateEvent('webViewInternal.onLoadStop'), + fields: [] + }, + 'newwindow': { + cancelable: true, + customHandler: function(handler, event, webViewEvent) { + handler.handleNewWindowEvent(event, webViewEvent); + }, + evt: CreateEvent('webViewInternal.onNewWindow'), + fields: [ + 'initialHeight', + 'initialWidth', + 'targetUrl', + 'windowOpenDisposition', + 'name' + ] + }, + 'permissionrequest': { + cancelable: true, + customHandler: function(handler, event, webViewEvent) { + handler.handlePermissionEvent(event, webViewEvent); + }, + evt: CreateEvent('webViewInternal.onPermissionRequest'), + fields: [ + 'identifier', + 'lastUnlockedBySelf', + 'name', + 'permission', + 'requestMethod', + 'url', + 'userGesture' + ] + }, + 'responsive': { + evt: CreateEvent('webViewInternal.onResponsive'), + fields: ['processId'] + }, + 'sizechanged': { + evt: CreateEvent('webViewInternal.onSizeChanged'), + customHandler: function(handler, event, webViewEvent) { + handler.handleSizeChangedEvent(event, webViewEvent); + }, + fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] + }, + 'unresponsive': { + evt: CreateEvent('webViewInternal.onUnresponsive'), + fields: ['processId'] + }, + 'zoomchange': { + evt: CreateEvent('webViewInternal.onZoomChange'), + fields: ['oldZoomFactor', 'newZoomFactor'] + } +}; + +function DeclarativeWebRequestEvent(opt_eventName, + opt_argSchemas, + opt_eventOptions, + opt_webViewInstanceId) { + var subEventName = opt_eventName + '/' + IdGenerator.GetNextId(); + EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, + opt_webViewInstanceId); + + // TODO(lazyboy): When do we dispose this listener? + WebRequestMessageEvent.addListener(function() { + // Re-dispatch to subEvent's listeners. + $Function.apply(this.dispatch, this, $Array.slice(arguments)); + }.bind(this), {instanceId: opt_webViewInstanceId || 0}); +} + +DeclarativeWebRequestEvent.prototype = { + __proto__: EventBindings.Event.prototype +}; + +// Constructor. +function WebViewEvents(webViewInternal, viewInstanceId) { + this.webViewInternal = webViewInternal; + this.viewInstanceId = viewInstanceId; + this.setup(); +} + +// Sets up events. +WebViewEvents.prototype.setup = function() { + this.setupFrameNameChangedEvent(); + this.setupPluginDestroyedEvent(); + this.setupWebRequestEvents(); + this.webViewInternal.setupExperimentalContextMenus(); + + var events = this.getEvents(); + for (var eventName in events) { + this.setupEvent(eventName, events[eventName]); + } +}; + +WebViewEvents.prototype.setupFrameNameChangedEvent = function() { + FrameNameChangedEvent.addListener(function(e) { + this.webViewInternal.onFrameNameChanged(e.name); + }.bind(this), {instanceId: this.viewInstanceId}); +}; + +WebViewEvents.prototype.setupPluginDestroyedEvent = function() { + PluginDestroyedEvent.addListener(function(e) { + this.webViewInternal.onPluginDestroyed(); + }.bind(this), {instanceId: this.viewInstanceId}); +}; + +WebViewEvents.prototype.setupWebRequestEvents = function() { + var request = {}; + var createWebRequestEvent = function(webRequestEvent) { + return function() { + if (!this[webRequestEvent.name]) { + this[webRequestEvent.name] = + new WebRequestEvent( + 'webViewInternal.' + webRequestEvent.name, + webRequestEvent.parameters, + webRequestEvent.extraParameters, webRequestEvent.options, + this.viewInstanceId); + } + return this[webRequestEvent.name]; + }.bind(this); + }.bind(this); + + var createDeclarativeWebRequestEvent = function(webRequestEvent) { + return function() { + if (!this[webRequestEvent.name]) { + // The onMessage event gets a special event type because we want + // the listener to fire only for messages targeted for this particular + // <webview>. + var EventClass = webRequestEvent.name === 'onMessage' ? + DeclarativeWebRequestEvent : EventBindings.Event; + this[webRequestEvent.name] = + new EventClass( + 'webViewInternal.' + webRequestEvent.name, + webRequestEvent.parameters, + webRequestEvent.options, + this.viewInstanceId); + } + return this[webRequestEvent.name]; + }.bind(this); + }.bind(this); + + for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { + var eventSchema = DeclarativeWebRequestSchema.events[i]; + var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema); + Object.defineProperty( + request, + eventSchema.name, + { + get: webRequestEvent, + enumerable: true + } + ); + } + + // 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.webViewInternal.setRequestPropertyOnWebViewNode(request); +}; + +WebViewEvents.prototype.getEvents = function() { + var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents(); + for (var eventName in experimentalEvents) { + WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; + } + var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents(); + for (var eventName in chromeEvents) { + WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName]; + } + return WEB_VIEW_EVENTS; +}; + +WebViewEvents.prototype.setupEvent = function(name, info) { + info.evt.addListener(function(e) { + var details = {bubbles:true}; + if (info.cancelable) { + details.cancelable = true; + } + var webViewEvent = new Event(name, details); + $Array.forEach(info.fields, function(field) { + if (e[field] !== undefined) { + webViewEvent[field] = e[field]; + } + }.bind(this)); + if (info.customHandler) { + info.customHandler(this, e, webViewEvent); + return; + } + this.webViewInternal.dispatchEvent(webViewEvent); + }.bind(this), {instanceId: this.viewInstanceId}); + + this.webViewInternal.setupEventProperty(name); +}; + + +WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) { + var showWarningMessage = function(dialogType) { + var VOWELS = ['a', 'e', 'i', 'o', 'u']; + var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.'; + var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A'; + var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article); + output = output.replace('%2', dialogType); + window.console.warn(output); + }; + + var requestId = event.requestId; + var actionTaken = false; + + var validateCall = function() { + var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' + + 'An action has already been taken for this "dialog" event.'; + + if (actionTaken) { + throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN); + } + actionTaken = true; + }; + + var getGuestInstanceId = function() { + return this.webViewInternal.getGuestInstanceId(); + }.bind(this); + + var dialog = { + ok: function(user_input) { + validateCall(); + user_input = user_input || ''; + WebView.setPermission(getGuestInstanceId(), requestId, 'allow', + user_input); + }, + cancel: function() { + validateCall(); + WebView.setPermission(getGuestInstanceId(), requestId, 'deny'); + } + }; + webViewEvent.dialog = dialog; + + var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent); + if (actionTaken) { + return; + } + + if (defaultPrevented) { + // Tell the JavaScript garbage collector to track lifetime of |dialog| and + // call back when the dialog object has been collected. + MessagingNatives.BindToGC(dialog, function() { + // Avoid showing a warning message if the decision has already been made. + if (actionTaken) { + return; + } + WebView.setPermission( + getGuestInstanceId(), requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(event.messageType); + }); + }); + } else { + actionTaken = true; + // The default action is equivalent to canceling the dialog. + WebView.setPermission( + getGuestInstanceId(), requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(event.messageType); + }); + } +}; + +WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) { + var showWarningMessage = function(reason) { + var WARNING_MSG_LOAD_ABORTED = '<webview>: ' + + 'The load has aborted with reason "%1".'; + window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason)); + }; + if (this.webViewInternal.dispatchEvent(webViewEvent)) { + showWarningMessage(event.reason); + } +}; + +WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) { + this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount, + event.processId, event.url, + event.isTopLevel); + this.webViewInternal.dispatchEvent(webViewEvent); +}; + +WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) { + var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' + + 'An action has already been taken for this "newwindow" event.'; + + var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' + + 'Unable to attach the new window to the provided webViewInternal.'; + + var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.'; + + var showWarningMessage = function() { + var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.'; + window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED); + }; + + var requestId = event.requestId; + var actionTaken = false; + var getGuestInstanceId = function() { + return this.webViewInternal.getGuestInstanceId(); + }.bind(this); + + var validateCall = function () { + if (actionTaken) { + throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN); + } + actionTaken = true; + }; + + var windowObj = { + attach: function(webview) { + validateCall(); + if (!webview || !webview.tagName || webview.tagName != '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 webViewInternal = privates(webview).internal; + // Update the partition. + if (event.storagePartitionId) { + webViewInternal.onAttach(event.storagePartitionId); + } + + var attached = webViewInternal.attachWindow(event.windowId, true); + + if (!attached) { + window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); + } + + var guestInstanceId = getGuestInstanceId(); + if (!guestInstanceId) { + // If the opener is already gone, then we won't have its + // guestInstanceId. + return; + } + + // If the object being passed into attach is not a valid <webview> + // 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( + guestInstanceId, requestId, attached ? 'allow' : 'deny'); + }, 0); + }, + discard: function() { + validateCall(); + var guestInstanceId = getGuestInstanceId(); + if (!guestInstanceId) { + // If the opener is already gone, then we won't have its + // guestInstanceId. + return; + } + WebView.setPermission(guestInstanceId, requestId, 'deny'); + } + }; + webViewEvent.window = windowObj; + + var defaultPrevented = !this.webViewInternal.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; + } + + var guestInstanceId = getGuestInstanceId(); + if (!guestInstanceId) { + // If the opener is already gone, then we won't have its + // guestInstanceId. + return; + } + + WebView.setPermission( + guestInstanceId, requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(); + }); + }); + } else { + actionTaken = true; + // The default action is to discard the window. + WebView.setPermission( + getGuestInstanceId(), requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(); + }); + } +}; + +WebViewEvents.prototype.getPermissionTypes = function() { + var permissions = + ['media', + 'geolocation', + 'pointerLock', + 'download', + 'loadplugin', + 'filesystem']; + return permissions.concat( + this.webViewInternal.maybeGetExperimentalPermissions()); +}; + +WebViewEvents.prototype.handlePermissionEvent = + function(event, webViewEvent) { + var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' + + 'Permission has already been decided for this "permissionrequest" event.'; + + var showWarningMessage = function(permission) { + var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' + + 'The permission request for "%1" has been denied.'; + window.console.warn( + WARNING_MSG_PERMISSION_DENIED.replace('%1', permission)); + }; + + var requestId = event.requestId; + var getGuestInstanceId = function() { + return this.webViewInternal.getGuestInstanceId(); + }.bind(this); + + if (this.getPermissionTypes().indexOf(event.permission) < 0) { + // The permission type is not allowed. Trigger the default response. + WebView.setPermission( + getGuestInstanceId(), requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(event.permission); + }); + return; + } + + 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(getGuestInstanceId(), requestId, 'allow'); + }, + deny: function() { + validateCall(); + WebView.setPermission(getGuestInstanceId(), requestId, 'deny'); + } + }; + webViewEvent.request = request; + + var defaultPrevented = !this.webViewInternal.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( + getGuestInstanceId(), requestId, 'default', '', function(allowed) { + if (allowed) { + return; + } + showWarningMessage(event.permission); + }); + }); + } else { + decisionMade = true; + WebView.setPermission( + getGuestInstanceId(), requestId, 'default', '', + function(allowed) { + if (allowed) { + return; + } + showWarningMessage(event.permission); + }); + } +}; + +WebViewEvents.prototype.handleSizeChangedEvent = function( + event, webViewEvent) { + this.webViewInternal.onSizeChanged(webViewEvent); +}; + +exports.WebViewEvents = WebViewEvents; +exports.CreateEvent = CreateEvent; diff --git a/extensions/renderer/resources/web_view_experimental.js b/extensions/renderer/resources/web_view_experimental.js new file mode 100644 index 0000000..c1246ae --- /dev/null +++ b/extensions/renderer/resources/web_view_experimental.js @@ -0,0 +1,31 @@ +// Copyright 2014 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. + +// This module implements experimental API for <webview>. +// See web_view.js for details. +// +// <webview> Experimental API is only available on canary and dev channels of +// Chrome. + +var WebViewInternal = require('webView').WebViewInternal; + +WebViewInternal.prototype.maybeGetExperimentalEvents = function() { + return {}; +}; + +/** @private */ +WebViewInternal.prototype.maybeGetExperimentalPermissions = function() { + return []; +}; + +/** @private */ +WebViewInternal.prototype.captureVisibleRegion = function(spec, callback) { + WebView.captureVisibleRegion(this.guestInstanceId, spec, callback); +}; + +WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) { + proto.captureVisibleRegion = function(spec, callback) { + privates(this).internal.captureVisibleRegion(spec, callback); + }; +}; |