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 13 var eventBindings = require('event_bindings'); 14 var process = requireNative('process'); 15 var addTagWatcher = require('tagWatcher').addTagWatcher; 16 17 /** 18 * Define "allowCustomAdNetworks" function such that the 19 * "kEnableAdviewSrcAttribute" flag is respected. 20 */ 21 function 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 */ 29 var 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 */ 40 var 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 */ 64 var AD_VIEW_API_METHODS = [ 65 // Empty for now. 66 ]; 67 68 var createEvent = function(name) { 69 var eventOpts = {supportsListeners: true, supportsFilters: true}; 70 return new eventBindings.Event(name, undefined, eventOpts); 71 }; 72 73 var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort'); 74 var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit'); 75 76 var 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 */ 100 var 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 */ 111 function 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 */ 123 function 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 */ 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)); 152 } 153 }, this); 154 155 return browserPluginNode; 156 } 157 158 /** 159 * @private 160 */ 161 AdView.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 */ 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. 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 */ 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); 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 */ 206 AdView.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 */ 223 AdView.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 */ 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, { 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 */ 276 AdView.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 */ 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 302 // details. 303 this.browserPluginNode_[mutation.attributeName] = 304 this.adviewNode_.getAttribute(mutation.attributeName); 305 }; 306 307 /** 308 * @private 309 */ 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); 315 } 316 } 317 }, this); 318 }; 319 320 /** 321 * @private 322 */ 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); 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 */ 349 AdView.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 */ 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); 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 */ 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; 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 */ 448 AdView.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 */ 469 AdView.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 */ 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]; 492 } 493 494 // Dispatch event. 495 this.adviewNode_.dispatchEvent(evt); 496 } 497 498 addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); }); 499