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. 4 5// Shim that simulates a <adview> tag via Mutation Observers. 6// 7// The actual tag is implemented via the browser plugin. The internals of this 8// are hidden via Shadow DOM. 9 10// TODO(rpaquay): This file is currently very similar to "web_view.js". Do we 11// want to refactor to extract common pieces? 12 13var eventBindings = require('event_bindings'); 14var process = requireNative('process'); 15var addTagWatcher = require('tagWatcher').addTagWatcher; 16 17/** 18 * Define "allowCustomAdNetworks" function such that the 19 * "kEnableAdviewSrcAttribute" flag is respected. 20 */ 21function allowCustomAdNetworks() { 22 return process.HasSwitch('enable-adview-src-attribute'); 23} 24 25/** 26 * List of attribute names to "blindly" sync between <adview> tag and internal 27 * browser plugin. 28 */ 29var AD_VIEW_ATTRIBUTES = [ 30 'name', 31]; 32 33/** 34 * List of custom attributes (and their behavior). 35 * 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. 39 */ 40var AD_VIEW_CUSTOM_ATTRIBUTES = [ 41 { 42 name: 'ad-network', 43 onMutation: function(adview, mutation) { 44 adview.handleAdNetworkMutation(mutation); 45 }, 46 isProperty: function() { 47 return true; 48 } 49 }, 50 { 51 name: 'src', 52 onMutation: function(adview, mutation) { 53 adview.handleSrcMutation(mutation); 54 }, 55 isProperty: function() { 56 return allowCustomAdNetworks(); 57 } 58 } 59]; 60 61/** 62 * List of api methods. These are forwarded to the browser plugin. 63 */ 64var AD_VIEW_API_METHODS = [ 65 // Empty for now. 66]; 67 68var createEvent = function(name) { 69 var eventOpts = {supportsListeners: true, supportsFilters: true}; 70 return new eventBindings.Event(name, undefined, eventOpts); 71}; 72 73var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort'); 74var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit'); 75 76var AD_VIEW_EXT_EVENTS = { 77 'loadabort': { 78 evt: AdviewLoadAbortEvent, 79 fields: ['url', 'isTopLevel', 'reason'] 80 }, 81 'loadcommit': { 82 customHandler: function(adview, event) { 83 if (event.isTopLevel) { 84 adview.browserPluginNode_.setAttribute('src', event.url); 85 } 86 }, 87 evt: AdviewLoadCommitEvent, 88 fields: ['url', 'isTopLevel'] 89 } 90}; 91 92/** 93 * List of supported ad-networks. 94 * 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. 99 */ 100var AD_VIEW_AD_NETWORKS_WHITELIST = [ 101 { 102 name: 'admob', 103 url: 'https://admob-sdk.doubleclick.net/chromeapps', 104 origin: 'https://double.net' 105 }, 106]; 107 108/** 109 * Return the whitelisted ad-network entry named |name|. 110 */ 111function getAdNetworkInfo(name) { 112 var result = null; 113 $Array.forEach(AD_VIEW_AD_NETWORKS_WHITELIST, function(item) { 114 if (item.name === name) 115 result = item; 116 }); 117 return result; 118} 119 120/** 121 * @constructor 122 */ 123function AdView(adviewNode) { 124 this.adviewNode_ = adviewNode; 125 this.browserPluginNode_ = this.createBrowserPluginNode_(); 126 var shadowRoot = this.adviewNode_.webkitCreateShadowRoot(); 127 shadowRoot.appendChild(this.browserPluginNode_); 128 129 this.setupCustomAttributes_(); 130 this.setupAdviewNodeObservers_(); 131 this.setupAdviewNodeMethods_(); 132 this.setupAdviewNodeProperties_(); 133 this.setupAdviewNodeEvents_(); 134 this.setupBrowserPluginNodeObservers_(); 135} 136 137/** 138 * @private 139 */ 140AdView.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)); 152 } 153 }, this); 154 155 return browserPluginNode; 156} 157 158/** 159 * @private 160 */ 161AdView.prototype.setupCustomAttributes_ = function() { 162 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) { 163 if (attributeInfo.onMutation) { 164 attributeInfo.onMutation(this); 165 } 166 }, this); 167} 168 169/** 170 * @private 171 */ 172AdView.prototype.setupAdviewNodeMethods_ = function() { 173 // this.browserPluginNode_[apiMethod] are not necessarily defined immediately 174 // after the shadow object is appended to the shadow root. 175 var self = this; 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); 180 }; 181 }, this); 182} 183 184/** 185 * @private 186 */ 187AdView.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); 192 }, this); 193 var observer = new MutationObserver(function(mutations) { 194 $Array.forEach(mutations, handleMutation); 195 }); 196 observer.observe( 197 this.adviewNode_, 198 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); 199 200 this.setupAdviewNodeCustomObservers_(); 201} 202 203/** 204 * @private 205 */ 206AdView.prototype.setupAdviewNodeCustomObservers_ = function() { 207 var handleMutation = $Function.bind(function(mutation) { 208 this.handleAdviewCustomAttributeMutation_(mutation); 209 }, this); 210 var observer = new MutationObserver(function(mutations) { 211 $Array.forEach(mutations, handleMutation); 212 }); 213 var customAttributeNames = 214 AD_VIEW_CUSTOM_ATTRIBUTES.map(function(item) { return item.name; }); 215 observer.observe( 216 this.adviewNode_, 217 {attributes: true, attributeFilter: customAttributeNames}); 218} 219 220/** 221 * @private 222 */ 223AdView.prototype.setupBrowserPluginNodeObservers_ = function() { 224 var handleMutation = $Function.bind(function(mutation) { 225 this.handleBrowserPluginAttributeMutation_(mutation); 226 }, this); 227 var objectObserver = new MutationObserver(function(mutations) { 228 $Array.forEach(mutations, handleMutation); 229 }); 230 objectObserver.observe( 231 this.browserPluginNode_, 232 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); 233} 234 235/** 236 * @private 237 */ 238AdView.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, { 243 get: function() { 244 return browserPluginNode[attributeName]; 245 }, 246 set: function(value) { 247 browserPluginNode[attributeName] = value; 248 }, 249 enumerable: true 250 }); 251 }, this); 252 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, { 259 get: function() { 260 return adviewNode.getAttribute(attributeName); 261 }, 262 set: function(value) { 263 adviewNode.setAttribute(attributeName, value); 264 }, 265 enumerable: true 266 }); 267 } 268 }, this); 269 270 this.setupAdviewContentWindowProperty_(); 271} 272 273/** 274 * @private 275 */ 276AdView.prototype.setupAdviewContentWindowProperty_ = function() { 277 var browserPluginNode = this.browserPluginNode_; 278 // We cannot use {writable: true} property descriptor because we want dynamic 279 // getter value. 280 Object.defineProperty(this.adviewNode_, 'contentWindow', { 281 get: function() { 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.'); 288 }, 289 // No setter. 290 enumerable: true 291 }); 292} 293 294/** 295 * @private 296 */ 297AdView.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 302 // details. 303 this.browserPluginNode_[mutation.attributeName] = 304 this.adviewNode_.getAttribute(mutation.attributeName); 305}; 306 307/** 308 * @private 309 */ 310AdView.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); 315 } 316 } 317 }, this); 318}; 319 320/** 321 * @private 322 */ 323AdView.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); 330 } else { 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); 342 } 343 } 344}; 345 346/** 347 * @private 348 */ 349AdView.prototype.navigateToUrl_ = function(url) { 350 var newValue = url; 351 var oldValue = this.browserPluginNode_.getAttribute('src'); 352 353 if (newValue === oldValue) 354 return; 355 356 if (url != null) { 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); 362 } 363 } 364 else { 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'); 372 } 373 } 374} 375 376/** 377 * @public 378 */ 379AdView.prototype.handleAdNetworkMutation = function(mutation) { 380 if (this.adviewNode_.hasAttribute('ad-network')) { 381 var value = this.adviewNode_.getAttribute('ad-network'); 382 var item = getAdNetworkInfo(value); 383 if (item) { 384 this.navigateToUrl_(item.url); 385 } 386 else if (allowCustomAdNetworks()) { 387 console.log('The ad-network "' + value + '" is not recognized, ' + 388 'but custom ad-networks are enabled.'); 389 390 if (mutation) { 391 this.navigateToUrl_(''); 392 } 393 } 394 else { 395 // Ignore the new attribute value and set it to empty string. 396 // Avoid infinite loop by checking for empty string as new value. 397 if (value != '') { 398 console.error('The ad-network "' + value + '" is not recognized.'); 399 this.adviewNode_.setAttribute('ad-network', ''); 400 } 401 this.navigateToUrl_(''); 402 } 403 } 404 else { 405 this.navigateToUrl_(''); 406 } 407} 408 409/** 410 * @public 411 */ 412AdView.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; 419 } 420 else { 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; 428 } 429 } 430 else { 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. 435 if (value != '') { 436 console.error('Setting the "src" attribute of an <adview> ' + 437 'element is not supported. Use the "ad-network" attribute ' + 438 'instead.'); 439 this.adviewNode_.setAttribute('src', ''); 440 } 441 } 442 } 443} 444 445/** 446 * @private 447 */ 448AdView.prototype.setupAdviewNodeEvents_ = function() { 449 var self = this; 450 var onInstanceIdAllocated = function(e) { 451 var detail = e.detail ? JSON.parse(e.detail) : {}; 452 self.instanceId_ = detail.windowId; 453 var params = { 454 'api': 'adview' 455 }; 456 self.browserPluginNode_['-internal-attach'](params); 457 458 for (var eventName in AD_VIEW_EXT_EVENTS) { 459 self.setupExtEvent_(eventName, AD_VIEW_EXT_EVENTS[eventName]); 460 } 461 }; 462 this.browserPluginNode_.addEventListener('-internal-instanceid-allocated', 463 onInstanceIdAllocated); 464} 465 466/** 467 * @private 468 */ 469AdView.prototype.setupExtEvent_ = function(eventName, eventInfo) { 470 var self = this; 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]; 476 }); 477 if (eventInfo.customHandler) { 478 eventInfo.customHandler(self, event); 479 } 480 adviewNode.dispatchEvent(adviewEvent); 481 }, {instanceId: self.instanceId_}); 482}; 483 484/** 485 * @public 486 */ 487AdView.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]; 492 } 493 494 // Dispatch event. 495 this.adviewNode_.dispatchEvent(evt); 496} 497 498addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); }); 499