| // Copyright 2013 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 <adview> tag via Mutation Observers. |
| // |
| // The actual tag is implemented via the browser plugin. The internals of this |
| // are hidden via Shadow DOM. |
| |
| // TODO(rpaquay): This file is currently very similar to "web_view.js". Do we |
| // want to refactor to extract common pieces? |
| |
| var eventBindings = require('event_bindings'); |
| var process = requireNative('process'); |
| var addTagWatcher = require('tagWatcher').addTagWatcher; |
| |
| /** |
| * Define "allowCustomAdNetworks" function such that the |
| * "kEnableAdviewSrcAttribute" flag is respected. |
| */ |
| function allowCustomAdNetworks() { |
| return process.HasSwitch('enable-adview-src-attribute'); |
| } |
| |
| /** |
| * List of attribute names to "blindly" sync between <adview> tag and internal |
| * browser plugin. |
| */ |
| var AD_VIEW_ATTRIBUTES = [ |
| 'name', |
| ]; |
| |
| /** |
| * List of custom attributes (and their behavior). |
| * |
| * name: attribute name. |
| * onMutation(adview, mutation): callback invoked when attribute is mutated. |
| * isProperty: True if the attribute should be exposed as a property. |
| */ |
| var AD_VIEW_CUSTOM_ATTRIBUTES = [ |
| { |
| name: 'ad-network', |
| onMutation: function(adview, mutation) { |
| adview.handleAdNetworkMutation(mutation); |
| }, |
| isProperty: function() { |
| return true; |
| } |
| }, |
| { |
| name: 'src', |
| onMutation: function(adview, mutation) { |
| adview.handleSrcMutation(mutation); |
| }, |
| isProperty: function() { |
| return allowCustomAdNetworks(); |
| } |
| } |
| ]; |
| |
| /** |
| * List of api methods. These are forwarded to the browser plugin. |
| */ |
| var AD_VIEW_API_METHODS = [ |
| // Empty for now. |
| ]; |
| |
| var createEvent = function(name) { |
| var eventOpts = {supportsListeners: true, supportsFilters: true}; |
| return new eventBindings.Event(name, undefined, eventOpts); |
| }; |
| |
| var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort'); |
| var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit'); |
| |
| var AD_VIEW_EXT_EVENTS = { |
| 'loadabort': { |
| evt: AdviewLoadAbortEvent, |
| fields: ['url', 'isTopLevel', 'reason'] |
| }, |
| 'loadcommit': { |
| customHandler: function(adview, event) { |
| if (event.isTopLevel) { |
| adview.browserPluginNode_.setAttribute('src', event.url); |
| } |
| }, |
| evt: AdviewLoadCommitEvent, |
| fields: ['url', 'isTopLevel'] |
| } |
| }; |
| |
| /** |
| * List of supported ad-networks. |
| * |
| * name: identifier of the ad-network, corresponding to a valid value |
| * of the "ad-network" attribute of an <adview> element. |
| * url: url to navigate to when initially displaying the <adview>. |
| * origin: origin of urls the <adview> is allowed navigate to. |
| */ |
| var AD_VIEW_AD_NETWORKS_WHITELIST = [ |
| { |
| name: 'admob', |
| url: 'https://admob-sdk.doubleclick.net/chromeapps', |
| origin: 'https://double.net' |
| }, |
| ]; |
| |
| /** |
| * Return the whitelisted ad-network entry named |name|. |
| */ |
| function getAdNetworkInfo(name) { |
| var result = null; |
| $Array.forEach(AD_VIEW_AD_NETWORKS_WHITELIST, function(item) { |
| if (item.name === name) |
| result = item; |
| }); |
| return result; |
| } |
| |
| /** |
| * @constructor |
| */ |
| function AdView(adviewNode) { |
| this.adviewNode_ = adviewNode; |
| this.browserPluginNode_ = this.createBrowserPluginNode_(); |
| var shadowRoot = this.adviewNode_.webkitCreateShadowRoot(); |
| shadowRoot.appendChild(this.browserPluginNode_); |
| |
| this.setupCustomAttributes_(); |
| this.setupAdviewNodeObservers_(); |
| this.setupAdviewNodeMethods_(); |
| this.setupAdviewNodeProperties_(); |
| this.setupAdviewNodeEvents_(); |
| this.setupBrowserPluginNodeObservers_(); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.createBrowserPluginNode_ = function() { |
| var browserPluginNode = document.createElement('object'); |
| browserPluginNode.type = 'application/browser-plugin'; |
| // The <object> node fills in the <adview> container. |
| browserPluginNode.style.width = '100%'; |
| browserPluginNode.style.height = '100%'; |
| $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) { |
| // Only copy attributes that have been assigned values, rather than copying |
| // a series of undefined attributes to BrowserPlugin. |
| if (this.adviewNode_.hasAttribute(attributeName)) { |
| browserPluginNode.setAttribute( |
| attributeName, this.adviewNode_.getAttribute(attributeName)); |
| } |
| }, this); |
| |
| return browserPluginNode; |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupCustomAttributes_ = function() { |
| $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) { |
| if (attributeInfo.onMutation) { |
| attributeInfo.onMutation(this); |
| } |
| }, this); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewNodeMethods_ = function() { |
| // this.browserPluginNode_[apiMethod] are not necessarily defined immediately |
| // after the shadow object is appended to the shadow root. |
| var self = this; |
| $Array.forEach(AD_VIEW_API_METHODS, function(apiMethod) { |
| self.adviewNode_[apiMethod] = function(var_args) { |
| return $Function.apply(self.browserPluginNode_[apiMethod], |
| self.browserPluginNode_, arguments); |
| }; |
| }, this); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewNodeObservers_ = function() { |
| // Map attribute modifications on the <adview> tag to property changes in |
| // the underlying <object> node. |
| var handleMutation = $Function.bind(function(mutation) { |
| this.handleAdviewAttributeMutation_(mutation); |
| }, this); |
| var observer = new MutationObserver(function(mutations) { |
| $Array.forEach(mutations, handleMutation); |
| }); |
| observer.observe( |
| this.adviewNode_, |
| {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); |
| |
| this.setupAdviewNodeCustomObservers_(); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewNodeCustomObservers_ = function() { |
| var handleMutation = $Function.bind(function(mutation) { |
| this.handleAdviewCustomAttributeMutation_(mutation); |
| }, this); |
| var observer = new MutationObserver(function(mutations) { |
| $Array.forEach(mutations, handleMutation); |
| }); |
| var customAttributeNames = |
| AD_VIEW_CUSTOM_ATTRIBUTES.map(function(item) { return item.name; }); |
| observer.observe( |
| this.adviewNode_, |
| {attributes: true, attributeFilter: customAttributeNames}); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupBrowserPluginNodeObservers_ = function() { |
| var handleMutation = $Function.bind(function(mutation) { |
| this.handleBrowserPluginAttributeMutation_(mutation); |
| }, this); |
| var objectObserver = new MutationObserver(function(mutations) { |
| $Array.forEach(mutations, handleMutation); |
| }); |
| objectObserver.observe( |
| this.browserPluginNode_, |
| {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewNodeProperties_ = function() { |
| var browserPluginNode = this.browserPluginNode_; |
| // Expose getters and setters for the attributes. |
| $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) { |
| Object.defineProperty(this.adviewNode_, attributeName, { |
| get: function() { |
| return browserPluginNode[attributeName]; |
| }, |
| set: function(value) { |
| browserPluginNode[attributeName] = value; |
| }, |
| enumerable: true |
| }); |
| }, this); |
| |
| // Expose getters and setters for the custom attributes. |
| var adviewNode = this.adviewNode_; |
| $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) { |
| if (attributeInfo.isProperty()) { |
| var attributeName = attributeInfo.name; |
| Object.defineProperty(this.adviewNode_, attributeName, { |
| get: function() { |
| return adviewNode.getAttribute(attributeName); |
| }, |
| set: function(value) { |
| adviewNode.setAttribute(attributeName, value); |
| }, |
| enumerable: true |
| }); |
| } |
| }, this); |
| |
| this.setupAdviewContentWindowProperty_(); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewContentWindowProperty_ = function() { |
| var browserPluginNode = this.browserPluginNode_; |
| // We cannot use {writable: true} property descriptor because we want dynamic |
| // getter value. |
| Object.defineProperty(this.adviewNode_, 'contentWindow', { |
| get: function() { |
| // TODO(fsamuel): This is a workaround to enable |
| // contentWindow.postMessage until http://crbug.com/152006 is fixed. |
| if (browserPluginNode.contentWindow) |
| return browserPluginNode.contentWindow.self; |
| console.error('contentWindow is not available at this time. ' + |
| 'It will become available when the page has finished loading.'); |
| }, |
| // No setter. |
| enumerable: true |
| }); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.handleAdviewAttributeMutation_ = function(mutation) { |
| // This observer monitors mutations to attributes of the <adview> 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. |
| this.browserPluginNode_[mutation.attributeName] = |
| this.adviewNode_.getAttribute(mutation.attributeName); |
| }; |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.handleAdviewCustomAttributeMutation_ = function(mutation) { |
| $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(item) { |
| if (mutation.attributeName.toUpperCase() == item.name.toUpperCase()) { |
| if (item.onMutation) { |
| $Function.bind(item.onMutation, item)(this, mutation); |
| } |
| } |
| }, this); |
| }; |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.handleBrowserPluginAttributeMutation_ = function(mutation) { |
| // This observer monitors mutations to attributes of the BrowserPlugin and |
| // updates the <adview> attributes accordingly. |
| if (!this.browserPluginNode_.hasAttribute(mutation.attributeName)) { |
| // If an attribute is removed from the BrowserPlugin, then remove it |
| // from the <adview> as well. |
| this.adviewNode_.removeAttribute(mutation.attributeName); |
| } else { |
| // Update the <adview> attribute to match the BrowserPlugin attribute. |
| // Note: Calling setAttribute on <adview> 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 <adview> attribute |
| // if the BrowserPlugin attributes differs from it. |
| var oldValue = this.adviewNode_.getAttribute(mutation.attributeName); |
| var newValue = this.browserPluginNode_.getAttribute(mutation.attributeName); |
| if (newValue != oldValue) { |
| this.adviewNode_.setAttribute(mutation.attributeName, newValue); |
| } |
| } |
| }; |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.navigateToUrl_ = function(url) { |
| var newValue = url; |
| var oldValue = this.browserPluginNode_.getAttribute('src'); |
| |
| if (newValue === oldValue) |
| return; |
| |
| if (url != null) { |
| // Note: Setting the 'src' property directly, as calling setAttribute has no |
| // effect due to implementation details of BrowserPlugin. |
| this.browserPluginNode_['src'] = url; |
| if (allowCustomAdNetworks()) { |
| this.adviewNode_.setAttribute('src', url); |
| } |
| } |
| else { |
| // Note: Setting the 'src' property directly, as calling setAttribute has no |
| // effect due to implementation details of BrowserPlugin. |
| // TODO(rpaquay): Due to another implementation detail of BrowserPlugin, |
| // this line will leave the "src" attribute value untouched. |
| this.browserPluginNode_['src'] = null; |
| if (allowCustomAdNetworks()) { |
| this.adviewNode_.removeAttribute('src'); |
| } |
| } |
| } |
| |
| /** |
| * @public |
| */ |
| AdView.prototype.handleAdNetworkMutation = function(mutation) { |
| if (this.adviewNode_.hasAttribute('ad-network')) { |
| var value = this.adviewNode_.getAttribute('ad-network'); |
| var item = getAdNetworkInfo(value); |
| if (item) { |
| this.navigateToUrl_(item.url); |
| } |
| else if (allowCustomAdNetworks()) { |
| console.log('The ad-network "' + value + '" is not recognized, ' + |
| 'but custom ad-networks are enabled.'); |
| |
| if (mutation) { |
| this.navigateToUrl_(''); |
| } |
| } |
| else { |
| // Ignore the new attribute value and set it to empty string. |
| // Avoid infinite loop by checking for empty string as new value. |
| if (value != '') { |
| console.error('The ad-network "' + value + '" is not recognized.'); |
| this.adviewNode_.setAttribute('ad-network', ''); |
| } |
| this.navigateToUrl_(''); |
| } |
| } |
| else { |
| this.navigateToUrl_(''); |
| } |
| } |
| |
| /** |
| * @public |
| */ |
| AdView.prototype.handleSrcMutation = function(mutation) { |
| if (allowCustomAdNetworks()) { |
| if (this.adviewNode_.hasAttribute('src')) { |
| var newValue = this.adviewNode_.getAttribute('src'); |
| // Note: Setting the 'src' property directly, as calling setAttribute has |
| // no effect due to implementation details of BrowserPlugin. |
| this.browserPluginNode_['src'] = newValue; |
| } |
| else { |
| // If an attribute is removed from the <adview>, then remove it |
| // from the BrowserPlugin as well. |
| // Note: Setting the 'src' property directly, as calling setAttribute has |
| // no effect due to implementation details of BrowserPlugin. |
| // TODO(rpaquay): Due to another implementation detail of BrowserPlugin, |
| // this line will leave the "src" attribute value untouched. |
| this.browserPluginNode_['src'] = null; |
| } |
| } |
| else { |
| if (this.adviewNode_.hasAttribute('src')) { |
| var value = this.adviewNode_.getAttribute('src'); |
| // Ignore the new attribute value and set it to empty string. |
| // Avoid infinite loop by checking for empty string as new value. |
| if (value != '') { |
| console.error('Setting the "src" attribute of an <adview> ' + |
| 'element is not supported. Use the "ad-network" attribute ' + |
| 'instead.'); |
| this.adviewNode_.setAttribute('src', ''); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupAdviewNodeEvents_ = function() { |
| var self = this; |
| var onInstanceIdAllocated = function(e) { |
| var detail = e.detail ? JSON.parse(e.detail) : {}; |
| self.instanceId_ = detail.windowId; |
| var params = { |
| 'api': 'adview' |
| }; |
| self.browserPluginNode_['-internal-attach'](params); |
| |
| for (var eventName in AD_VIEW_EXT_EVENTS) { |
| self.setupExtEvent_(eventName, AD_VIEW_EXT_EVENTS[eventName]); |
| } |
| }; |
| this.browserPluginNode_.addEventListener('-internal-instanceid-allocated', |
| onInstanceIdAllocated); |
| } |
| |
| /** |
| * @private |
| */ |
| AdView.prototype.setupExtEvent_ = function(eventName, eventInfo) { |
| var self = this; |
| var adviewNode = this.adviewNode_; |
| eventInfo.evt.addListener(function(event) { |
| var adviewEvent = new Event(eventName, {bubbles: true}); |
| $Array.forEach(eventInfo.fields, function(field) { |
| adviewEvent[field] = event[field]; |
| }); |
| if (eventInfo.customHandler) { |
| eventInfo.customHandler(self, event); |
| } |
| adviewNode.dispatchEvent(adviewEvent); |
| }, {instanceId: self.instanceId_}); |
| }; |
| |
| /** |
| * @public |
| */ |
| AdView.prototype.dispatchEvent = function(eventname, detail) { |
| // Create event object. |
| var evt = new Event(eventname, { bubbles: true }); |
| for(var item in detail) { |
| evt[item] = detail[item]; |
| } |
| |
| // Dispatch event. |
| this.adviewNode_.dispatchEvent(evt); |
| } |
| |
| addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); }); |