1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // Shim that simulates a <adview> tag via Mutation Observers.
7 // The actual tag is implemented via the browser plugin. The internals of this
8 // are hidden via Shadow DOM.
10 // TODO(rpaquay): This file is currently very similar to "web_view.js". Do we
11 // want to refactor to extract common pieces?
13 var eventBindings = require('event_bindings');
14 var process = requireNative('process');
15 var addTagWatcher = require('tagWatcher').addTagWatcher;
18 * Define "allowCustomAdNetworks" function such that the
19 * "kEnableAdviewSrcAttribute" flag is respected.
21 function allowCustomAdNetworks() {
22 return process.HasSwitch('enable-adview-src-attribute');
26 * List of attribute names to "blindly" sync between <adview> tag and internal
29 var AD_VIEW_ATTRIBUTES = [
34 * List of custom attributes (and their behavior).
36 * name: attribute name.
37 * onMutation(adview, mutation): callback invoked when attribute is mutated.
38 * isProperty: True if the attribute should be exposed as a property.
40 var AD_VIEW_CUSTOM_ATTRIBUTES = [
43 onMutation: function(adview, mutation) {
44 adview.handleAdNetworkMutation(mutation);
46 isProperty: function() {
52 onMutation: function(adview, mutation) {
53 adview.handleSrcMutation(mutation);
55 isProperty: function() {
56 return allowCustomAdNetworks();
62 * List of api methods. These are forwarded to the browser plugin.
64 var AD_VIEW_API_METHODS = [
68 var createEvent = function(name) {
69 var eventOpts = {supportsListeners: true, supportsFilters: true};
70 return new eventBindings.Event(name, undefined, eventOpts);
73 var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort');
74 var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit');
76 var AD_VIEW_EXT_EVENTS = {
78 evt: AdviewLoadAbortEvent,
79 fields: ['url', 'isTopLevel', 'reason']
82 customHandler: function(adview, event) {
83 if (event.isTopLevel) {
84 adview.browserPluginNode_.setAttribute('src', event.url);
87 evt: AdviewLoadCommitEvent,
88 fields: ['url', 'isTopLevel']
93 * List of supported ad-networks.
95 * name: identifier of the ad-network, corresponding to a valid value
96 * of the "ad-network" attribute of an <adview> element.
97 * url: url to navigate to when initially displaying the <adview>.
98 * origin: origin of urls the <adview> is allowed navigate to.
100 var AD_VIEW_AD_NETWORKS_WHITELIST = [
103 url: 'https://admob-sdk.doubleclick.net/chromeapps',
104 origin: 'https://double.net'
109 * Return the whitelisted ad-network entry named |name|.
111 function getAdNetworkInfo(name) {
113 $Array.forEach(AD_VIEW_AD_NETWORKS_WHITELIST, function(item) {
114 if (item.name === name)
123 function AdView(adviewNode) {
124 this.adviewNode_ = adviewNode;
125 this.browserPluginNode_ = this.createBrowserPluginNode_();
126 var shadowRoot = this.adviewNode_.webkitCreateShadowRoot();
127 shadowRoot.appendChild(this.browserPluginNode_);
129 this.setupCustomAttributes_();
130 this.setupAdviewNodeObservers_();
131 this.setupAdviewNodeMethods_();
132 this.setupAdviewNodeProperties_();
133 this.setupAdviewNodeEvents_();
134 this.setupBrowserPluginNodeObservers_();
140 AdView.prototype.createBrowserPluginNode_ = function() {
141 var browserPluginNode = document.createElement('object');
142 browserPluginNode.type = 'application/browser-plugin';
143 // The <object> node fills in the <adview> container.
144 browserPluginNode.style.width = '100%';
145 browserPluginNode.style.height = '100%';
146 $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) {
147 // Only copy attributes that have been assigned values, rather than copying
148 // a series of undefined attributes to BrowserPlugin.
149 if (this.adviewNode_.hasAttribute(attributeName)) {
150 browserPluginNode.setAttribute(
151 attributeName, this.adviewNode_.getAttribute(attributeName));
155 return browserPluginNode;
161 AdView.prototype.setupCustomAttributes_ = function() {
162 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) {
163 if (attributeInfo.onMutation) {
164 attributeInfo.onMutation(this);
172 AdView.prototype.setupAdviewNodeMethods_ = function() {
173 // this.browserPluginNode_[apiMethod] are not necessarily defined immediately
174 // after the shadow object is appended to the shadow root.
176 $Array.forEach(AD_VIEW_API_METHODS, function(apiMethod) {
177 self.adviewNode_[apiMethod] = function(var_args) {
178 return $Function.apply(self.browserPluginNode_[apiMethod],
179 self.browserPluginNode_, arguments);
187 AdView.prototype.setupAdviewNodeObservers_ = function() {
188 // Map attribute modifications on the <adview> tag to property changes in
189 // the underlying <object> node.
190 var handleMutation = $Function.bind(function(mutation) {
191 this.handleAdviewAttributeMutation_(mutation);
193 var observer = new MutationObserver(function(mutations) {
194 $Array.forEach(mutations, handleMutation);
198 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES});
200 this.setupAdviewNodeCustomObservers_();
206 AdView.prototype.setupAdviewNodeCustomObservers_ = function() {
207 var handleMutation = $Function.bind(function(mutation) {
208 this.handleAdviewCustomAttributeMutation_(mutation);
210 var observer = new MutationObserver(function(mutations) {
211 $Array.forEach(mutations, handleMutation);
213 var customAttributeNames =
214 AD_VIEW_CUSTOM_ATTRIBUTES.map(function(item) { return item.name; });
217 {attributes: true, attributeFilter: customAttributeNames});
223 AdView.prototype.setupBrowserPluginNodeObservers_ = function() {
224 var handleMutation = $Function.bind(function(mutation) {
225 this.handleBrowserPluginAttributeMutation_(mutation);
227 var objectObserver = new MutationObserver(function(mutations) {
228 $Array.forEach(mutations, handleMutation);
230 objectObserver.observe(
231 this.browserPluginNode_,
232 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES});
238 AdView.prototype.setupAdviewNodeProperties_ = function() {
239 var browserPluginNode = this.browserPluginNode_;
240 // Expose getters and setters for the attributes.
241 $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) {
242 Object.defineProperty(this.adviewNode_, attributeName, {
244 return browserPluginNode[attributeName];
246 set: function(value) {
247 browserPluginNode[attributeName] = value;
253 // Expose getters and setters for the custom attributes.
254 var adviewNode = this.adviewNode_;
255 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) {
256 if (attributeInfo.isProperty()) {
257 var attributeName = attributeInfo.name;
258 Object.defineProperty(this.adviewNode_, attributeName, {
260 return adviewNode.getAttribute(attributeName);
262 set: function(value) {
263 adviewNode.setAttribute(attributeName, value);
270 this.setupAdviewContentWindowProperty_();
276 AdView.prototype.setupAdviewContentWindowProperty_ = function() {
277 var browserPluginNode = this.browserPluginNode_;
278 // We cannot use {writable: true} property descriptor because we want dynamic
280 Object.defineProperty(this.adviewNode_, 'contentWindow', {
282 // TODO(fsamuel): This is a workaround to enable
283 // contentWindow.postMessage until http://crbug.com/152006 is fixed.
284 if (browserPluginNode.contentWindow)
285 return browserPluginNode.contentWindow.self;
286 console.error('contentWindow is not available at this time. ' +
287 'It will become available when the page has finished loading.');
297 AdView.prototype.handleAdviewAttributeMutation_ = function(mutation) {
298 // This observer monitors mutations to attributes of the <adview> and
299 // updates the BrowserPlugin properties accordingly. In turn, updating
300 // a BrowserPlugin property will update the corresponding BrowserPlugin
301 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
303 this.browserPluginNode_[mutation.attributeName] =
304 this.adviewNode_.getAttribute(mutation.attributeName);
310 AdView.prototype.handleAdviewCustomAttributeMutation_ = function(mutation) {
311 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(item) {
312 if (mutation.attributeName.toUpperCase() == item.name.toUpperCase()) {
313 if (item.onMutation) {
314 $Function.bind(item.onMutation, item)(this, mutation);
323 AdView.prototype.handleBrowserPluginAttributeMutation_ = function(mutation) {
324 // This observer monitors mutations to attributes of the BrowserPlugin and
325 // updates the <adview> attributes accordingly.
326 if (!this.browserPluginNode_.hasAttribute(mutation.attributeName)) {
327 // If an attribute is removed from the BrowserPlugin, then remove it
328 // from the <adview> as well.
329 this.adviewNode_.removeAttribute(mutation.attributeName);
331 // Update the <adview> attribute to match the BrowserPlugin attribute.
332 // Note: Calling setAttribute on <adview> will trigger its mutation
333 // observer which will then propagate that attribute to BrowserPlugin. In
334 // cases where we permit assigning a BrowserPlugin attribute the same value
335 // again (such as navigation when crashed), this could end up in an infinite
336 // loop. Thus, we avoid this loop by only updating the <adview> attribute
337 // if the BrowserPlugin attributes differs from it.
338 var oldValue = this.adviewNode_.getAttribute(mutation.attributeName);
339 var newValue = this.browserPluginNode_.getAttribute(mutation.attributeName);
340 if (newValue != oldValue) {
341 this.adviewNode_.setAttribute(mutation.attributeName, newValue);
349 AdView.prototype.navigateToUrl_ = function(url) {
351 var oldValue = this.browserPluginNode_.getAttribute('src');
353 if (newValue === oldValue)
357 // Note: Setting the 'src' property directly, as calling setAttribute has no
358 // effect due to implementation details of BrowserPlugin.
359 this.browserPluginNode_['src'] = url;
360 if (allowCustomAdNetworks()) {
361 this.adviewNode_.setAttribute('src', url);
365 // Note: Setting the 'src' property directly, as calling setAttribute has no
366 // effect due to implementation details of BrowserPlugin.
367 // TODO(rpaquay): Due to another implementation detail of BrowserPlugin,
368 // this line will leave the "src" attribute value untouched.
369 this.browserPluginNode_['src'] = null;
370 if (allowCustomAdNetworks()) {
371 this.adviewNode_.removeAttribute('src');
379 AdView.prototype.handleAdNetworkMutation = function(mutation) {
380 if (this.adviewNode_.hasAttribute('ad-network')) {
381 var value = this.adviewNode_.getAttribute('ad-network');
382 var item = getAdNetworkInfo(value);
384 this.navigateToUrl_(item.url);
386 else if (allowCustomAdNetworks()) {
387 console.log('The ad-network "' + value + '" is not recognized, ' +
388 'but custom ad-networks are enabled.');
391 this.navigateToUrl_('');
395 // Ignore the new attribute value and set it to empty string.
396 // Avoid infinite loop by checking for empty string as new value.
398 console.error('The ad-network "' + value + '" is not recognized.');
399 this.adviewNode_.setAttribute('ad-network', '');
401 this.navigateToUrl_('');
405 this.navigateToUrl_('');
412 AdView.prototype.handleSrcMutation = function(mutation) {
413 if (allowCustomAdNetworks()) {
414 if (this.adviewNode_.hasAttribute('src')) {
415 var newValue = this.adviewNode_.getAttribute('src');
416 // Note: Setting the 'src' property directly, as calling setAttribute has
417 // no effect due to implementation details of BrowserPlugin.
418 this.browserPluginNode_['src'] = newValue;
421 // If an attribute is removed from the <adview>, then remove it
422 // from the BrowserPlugin as well.
423 // Note: Setting the 'src' property directly, as calling setAttribute has
424 // no effect due to implementation details of BrowserPlugin.
425 // TODO(rpaquay): Due to another implementation detail of BrowserPlugin,
426 // this line will leave the "src" attribute value untouched.
427 this.browserPluginNode_['src'] = null;
431 if (this.adviewNode_.hasAttribute('src')) {
432 var value = this.adviewNode_.getAttribute('src');
433 // Ignore the new attribute value and set it to empty string.
434 // Avoid infinite loop by checking for empty string as new value.
436 console.error('Setting the "src" attribute of an <adview> ' +
437 'element is not supported. Use the "ad-network" attribute ' +
439 this.adviewNode_.setAttribute('src', '');
448 AdView.prototype.setupAdviewNodeEvents_ = function() {
450 var onInstanceIdAllocated = function(e) {
451 var detail = e.detail ? JSON.parse(e.detail) : {};
452 self.instanceId_ = detail.windowId;
456 self.browserPluginNode_['-internal-attach'](params);
458 for (var eventName in AD_VIEW_EXT_EVENTS) {
459 self.setupExtEvent_(eventName, AD_VIEW_EXT_EVENTS[eventName]);
462 this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
463 onInstanceIdAllocated);
469 AdView.prototype.setupExtEvent_ = function(eventName, eventInfo) {
471 var adviewNode = this.adviewNode_;
472 eventInfo.evt.addListener(function(event) {
473 var adviewEvent = new Event(eventName, {bubbles: true});
474 $Array.forEach(eventInfo.fields, function(field) {
475 adviewEvent[field] = event[field];
477 if (eventInfo.customHandler) {
478 eventInfo.customHandler(self, event);
480 adviewNode.dispatchEvent(adviewEvent);
481 }, {instanceId: self.instanceId_});
487 AdView.prototype.dispatchEvent = function(eventname, detail) {
488 // Create event object.
489 var evt = new Event(eventname, { bubbles: true });
490 for(var item in detail) {
491 evt[item] = detail[item];
495 this.adviewNode_.dispatchEvent(evt);
498 addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); });