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 /** 69 * List of events to blindly forward from the browser plugin to the <adview>. 70 */ 71 var AD_VIEW_EVENTS = { 72 'sizechanged': ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'], 73 }; 74 75 var createEvent = function(name) { 76 var eventOpts = {supportsListeners: true, supportsFilters: true}; 77 return new eventBindings.Event(name, undefined, eventOpts); 78 }; 79 80 var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort'); 81 var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit'); 82 83 var AD_VIEW_EXT_EVENTS = { 84 'loadabort': { 85 evt: AdviewLoadAbortEvent, 86 fields: ['url', 'isTopLevel', 'reason'] 87 }, 88 'loadcommit': { 89 customHandler: function(adview, event) { 90 if (event.isTopLevel) { 91 adview.browserPluginNode_.setAttribute('src', event.url); 92 } 93 }, 94 evt: AdviewLoadCommitEvent, 95 fields: ['url', 'isTopLevel'] 96 } 97 }; 98 99 /** 100 * List of supported ad-networks. 101 * 102 * name: identifier of the ad-network, corresponding to a valid value 103 * of the "ad-network" attribute of an <adview> element. 104 * url: url to navigate to when initially displaying the <adview>. 105 * origin: origin of urls the <adview> is allowed navigate to. 106 */ 107 var AD_VIEW_AD_NETWORKS_WHITELIST = [ 108 { 109 name: 'admob', 110 url: 'https://admob-sdk.doubleclick.net/chromeapps', 111 origin: 'https://double.net' 112 }, 113 ]; 114 115 /** 116 * Return the whitelisted ad-network entry named |name|. 117 */ 118 function getAdNetworkInfo(name) { 119 var result = null; 120 $Array.forEach(AD_VIEW_AD_NETWORKS_WHITELIST, function(item) { 121 if (item.name === name) 122 result = item; 123 }); 124 return result; 125 } 126 127 /** 128 * @constructor 129 */ 130 function AdView(adviewNode) { 131 this.adviewNode_ = adviewNode; 132 this.browserPluginNode_ = this.createBrowserPluginNode_(); 133 var shadowRoot = this.adviewNode_.webkitCreateShadowRoot(); 134 shadowRoot.appendChild(this.browserPluginNode_); 135 136 this.setupCustomAttributes_(); 137 this.setupAdviewNodeObservers_(); 138 this.setupAdviewNodeMethods_(); 139 this.setupAdviewNodeProperties_(); 140 this.setupAdviewNodeEvents_(); 141 this.setupBrowserPluginNodeObservers_(); 142 } 143 144 /** 145 * @private 146 */ 147 AdView.prototype.createBrowserPluginNode_ = function() { 148 var browserPluginNode = document.createElement('object'); 149 browserPluginNode.type = 'application/browser-plugin'; 150 // The <object> node fills in the <adview> container. 151 browserPluginNode.style.width = '100%'; 152 browserPluginNode.style.height = '100%'; 153 $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) { 154 // Only copy attributes that have been assigned values, rather than copying 155 // a series of undefined attributes to BrowserPlugin. 156 if (this.adviewNode_.hasAttribute(attributeName)) { 157 browserPluginNode.setAttribute( 158 attributeName, this.adviewNode_.getAttribute(attributeName)); 159 } 160 }, this); 161 162 return browserPluginNode; 163 } 164 165 /** 166 * @private 167 */ 168 AdView.prototype.setupCustomAttributes_ = function() { 169 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) { 170 if (attributeInfo.onMutation) { 171 attributeInfo.onMutation(this); 172 } 173 }, this); 174 } 175 176 /** 177 * @private 178 */ 179 AdView.prototype.setupAdviewNodeMethods_ = function() { 180 // this.browserPluginNode_[apiMethod] are not necessarily defined immediately 181 // after the shadow object is appended to the shadow root. 182 var self = this; 183 $Array.forEach(AD_VIEW_API_METHODS, function(apiMethod) { 184 self.adviewNode_[apiMethod] = function(var_args) { 185 return $Function.apply(self.browserPluginNode_[apiMethod], 186 self.browserPluginNode_, arguments); 187 }; 188 }, this); 189 } 190 191 /** 192 * @private 193 */ 194 AdView.prototype.setupAdviewNodeObservers_ = function() { 195 // Map attribute modifications on the <adview> tag to property changes in 196 // the underlying <object> node. 197 var handleMutation = $Function.bind(function(mutation) { 198 this.handleAdviewAttributeMutation_(mutation); 199 }, this); 200 var observer = new MutationObserver(function(mutations) { 201 $Array.forEach(mutations, handleMutation); 202 }); 203 observer.observe( 204 this.adviewNode_, 205 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); 206 207 this.setupAdviewNodeCustomObservers_(); 208 } 209 210 /** 211 * @private 212 */ 213 AdView.prototype.setupAdviewNodeCustomObservers_ = function() { 214 var handleMutation = $Function.bind(function(mutation) { 215 this.handleAdviewCustomAttributeMutation_(mutation); 216 }, this); 217 var observer = new MutationObserver(function(mutations) { 218 $Array.forEach(mutations, handleMutation); 219 }); 220 var customAttributeNames = 221 AD_VIEW_CUSTOM_ATTRIBUTES.map(function(item) { return item.name; }); 222 observer.observe( 223 this.adviewNode_, 224 {attributes: true, attributeFilter: customAttributeNames}); 225 } 226 227 /** 228 * @private 229 */ 230 AdView.prototype.setupBrowserPluginNodeObservers_ = function() { 231 var handleMutation = $Function.bind(function(mutation) { 232 this.handleBrowserPluginAttributeMutation_(mutation); 233 }, this); 234 var objectObserver = new MutationObserver(function(mutations) { 235 $Array.forEach(mutations, handleMutation); 236 }); 237 objectObserver.observe( 238 this.browserPluginNode_, 239 {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES}); 240 } 241 242 /** 243 * @private 244 */ 245 AdView.prototype.setupAdviewNodeProperties_ = function() { 246 var browserPluginNode = this.browserPluginNode_; 247 // Expose getters and setters for the attributes. 248 $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) { 249 Object.defineProperty(this.adviewNode_, attributeName, { 250 get: function() { 251 return browserPluginNode[attributeName]; 252 }, 253 set: function(value) { 254 browserPluginNode[attributeName] = value; 255 }, 256 enumerable: true 257 }); 258 }, this); 259 260 // Expose getters and setters for the custom attributes. 261 var adviewNode = this.adviewNode_; 262 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) { 263 if (attributeInfo.isProperty()) { 264 var attributeName = attributeInfo.name; 265 Object.defineProperty(this.adviewNode_, attributeName, { 266 get: function() { 267 return adviewNode.getAttribute(attributeName); 268 }, 269 set: function(value) { 270 adviewNode.setAttribute(attributeName, value); 271 }, 272 enumerable: true 273 }); 274 } 275 }, this); 276 277 this.setupAdviewContentWindowProperty_(); 278 } 279 280 /** 281 * @private 282 */ 283 AdView.prototype.setupAdviewContentWindowProperty_ = function() { 284 var browserPluginNode = this.browserPluginNode_; 285 // We cannot use {writable: true} property descriptor because we want dynamic 286 // getter value. 287 Object.defineProperty(this.adviewNode_, 'contentWindow', { 288 get: function() { 289 // TODO(fsamuel): This is a workaround to enable 290 // contentWindow.postMessage until http://crbug.com/152006 is fixed. 291 if (browserPluginNode.contentWindow) 292 return browserPluginNode.contentWindow.self; 293 console.error('contentWindow is not available at this time. ' + 294 'It will become available when the page has finished loading.'); 295 }, 296 // No setter. 297 enumerable: true 298 }); 299 } 300 301 /** 302 * @private 303 */ 304 AdView.prototype.handleAdviewAttributeMutation_ = function(mutation) { 305 // This observer monitors mutations to attributes of the <adview> and 306 // updates the BrowserPlugin properties accordingly. In turn, updating 307 // a BrowserPlugin property will update the corresponding BrowserPlugin 308 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more 309 // details. 310 this.browserPluginNode_[mutation.attributeName] = 311 this.adviewNode_.getAttribute(mutation.attributeName); 312 }; 313 314 /** 315 * @private 316 */ 317 AdView.prototype.handleAdviewCustomAttributeMutation_ = function(mutation) { 318 $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(item) { 319 if (mutation.attributeName.toUpperCase() == item.name.toUpperCase()) { 320 if (item.onMutation) { 321 $Function.bind(item.onMutation, item)(this, mutation); 322 } 323 } 324 }, this); 325 }; 326 327 /** 328 * @private 329 */ 330 AdView.prototype.handleBrowserPluginAttributeMutation_ = function(mutation) { 331 // This observer monitors mutations to attributes of the BrowserPlugin and 332 // updates the <adview> attributes accordingly. 333 if (!this.browserPluginNode_.hasAttribute(mutation.attributeName)) { 334 // If an attribute is removed from the BrowserPlugin, then remove it 335 // from the <adview> as well. 336 this.adviewNode_.removeAttribute(mutation.attributeName); 337 } else { 338 // Update the <adview> attribute to match the BrowserPlugin attribute. 339 // Note: Calling setAttribute on <adview> will trigger its mutation 340 // observer which will then propagate that attribute to BrowserPlugin. In 341 // cases where we permit assigning a BrowserPlugin attribute the same value 342 // again (such as navigation when crashed), this could end up in an infinite 343 // loop. Thus, we avoid this loop by only updating the <adview> attribute 344 // if the BrowserPlugin attributes differs from it. 345 var oldValue = this.adviewNode_.getAttribute(mutation.attributeName); 346 var newValue = this.browserPluginNode_.getAttribute(mutation.attributeName); 347 if (newValue != oldValue) { 348 this.adviewNode_.setAttribute(mutation.attributeName, newValue); 349 } 350 } 351 }; 352 353 /** 354 * @private 355 */ 356 AdView.prototype.navigateToUrl_ = function(url) { 357 var newValue = url; 358 var oldValue = this.browserPluginNode_.getAttribute('src'); 359 360 if (newValue === oldValue) 361 return; 362 363 if (url != null) { 364 // Note: Setting the 'src' property directly, as calling setAttribute has no 365 // effect due to implementation details of BrowserPlugin. 366 this.browserPluginNode_['src'] = url; 367 if (allowCustomAdNetworks()) { 368 this.adviewNode_.setAttribute('src', url); 369 } 370 } 371 else { 372 // Note: Setting the 'src' property directly, as calling setAttribute has no 373 // effect due to implementation details of BrowserPlugin. 374 // TODO(rpaquay): Due to another implementation detail of BrowserPlugin, 375 // this line will leave the "src" attribute value untouched. 376 this.browserPluginNode_['src'] = null; 377 if (allowCustomAdNetworks()) { 378 this.adviewNode_.removeAttribute('src'); 379 } 380 } 381 } 382 383 /** 384 * @public 385 */ 386 AdView.prototype.handleAdNetworkMutation = function(mutation) { 387 if (this.adviewNode_.hasAttribute('ad-network')) { 388 var value = this.adviewNode_.getAttribute('ad-network'); 389 var item = getAdNetworkInfo(value); 390 if (item) { 391 this.navigateToUrl_(item.url); 392 } 393 else if (allowCustomAdNetworks()) { 394 console.log('The ad-network "' + value + '" is not recognized, ' + 395 'but custom ad-networks are enabled.'); 396 397 if (mutation) { 398 this.navigateToUrl_(''); 399 } 400 } 401 else { 402 // Ignore the new attribute value and set it to empty string. 403 // Avoid infinite loop by checking for empty string as new value. 404 if (value != '') { 405 console.error('The ad-network "' + value + '" is not recognized.'); 406 this.adviewNode_.setAttribute('ad-network', ''); 407 } 408 this.navigateToUrl_(''); 409 } 410 } 411 else { 412 this.navigateToUrl_(''); 413 } 414 } 415 416 /** 417 * @public 418 */ 419 AdView.prototype.handleSrcMutation = function(mutation) { 420 if (allowCustomAdNetworks()) { 421 if (this.adviewNode_.hasAttribute('src')) { 422 var newValue = this.adviewNode_.getAttribute('src'); 423 // Note: Setting the 'src' property directly, as calling setAttribute has 424 // no effect due to implementation details of BrowserPlugin. 425 this.browserPluginNode_['src'] = newValue; 426 } 427 else { 428 // If an attribute is removed from the <adview>, then remove it 429 // from the BrowserPlugin as well. 430 // Note: Setting the 'src' property directly, as calling setAttribute has 431 // no effect due to implementation details of BrowserPlugin. 432 // TODO(rpaquay): Due to another implementation detail of BrowserPlugin, 433 // this line will leave the "src" attribute value untouched. 434 this.browserPluginNode_['src'] = null; 435 } 436 } 437 else { 438 if (this.adviewNode_.hasAttribute('src')) { 439 var value = this.adviewNode_.getAttribute('src'); 440 // Ignore the new attribute value and set it to empty string. 441 // Avoid infinite loop by checking for empty string as new value. 442 if (value != '') { 443 console.error('Setting the "src" attribute of an <adview> ' + 444 'element is not supported. Use the "ad-network" attribute ' + 445 'instead.'); 446 this.adviewNode_.setAttribute('src', ''); 447 } 448 } 449 } 450 } 451 452 /** 453 * @private 454 */ 455 AdView.prototype.setupAdviewNodeEvents_ = function() { 456 var self = this; 457 var onInstanceIdAllocated = function(e) { 458 var detail = e.detail ? JSON.parse(e.detail) : {}; 459 self.instanceId_ = detail.windowId; 460 var params = { 461 'api': 'adview' 462 }; 463 self.browserPluginNode_['-internal-attach'](params); 464 465 for (var eventName in AD_VIEW_EXT_EVENTS) { 466 self.setupExtEvent_(eventName, AD_VIEW_EXT_EVENTS[eventName]); 467 } 468 }; 469 this.browserPluginNode_.addEventListener('-internal-instanceid-allocated', 470 onInstanceIdAllocated); 471 472 for (var eventName in AD_VIEW_EVENTS) { 473 this.setupEvent_(eventName, AD_VIEW_EVENTS[eventName]); 474 } 475 } 476 477 /** 478 * @private 479 */ 480 AdView.prototype.setupExtEvent_ = function(eventName, eventInfo) { 481 var self = this; 482 var adviewNode = this.adviewNode_; 483 eventInfo.evt.addListener(function(event) { 484 var adviewEvent = new Event(eventName, {bubbles: true}); 485 $Array.forEach(eventInfo.fields, function(field) { 486 adviewEvent[field] = event[field]; 487 }); 488 if (eventInfo.customHandler) { 489 eventInfo.customHandler(self, event); 490 } 491 adviewNode.dispatchEvent(adviewEvent); 492 }, {instanceId: self.instanceId_}); 493 }; 494 495 /** 496 * @private 497 */ 498 AdView.prototype.setupEvent_ = function(eventname, attribs) { 499 var adviewNode = this.adviewNode_; 500 var internalname = '-internal-' + eventname; 501 this.browserPluginNode_.addEventListener(internalname, function(e) { 502 var evt = new Event(eventname, { bubbles: true }); 503 var detail = e.detail ? JSON.parse(e.detail) : {}; 504 $Array.forEach(attribs, function(attribName) { 505 evt[attribName] = detail[attribName]; 506 }); 507 adviewNode.dispatchEvent(evt); 508 }); 509 } 510 511 /** 512 * @public 513 */ 514 AdView.prototype.dispatchEvent = function(eventname, detail) { 515 // Create event object. 516 var evt = new Event(eventname, { bubbles: true }); 517 for(var item in detail) { 518 evt[item] = detail[item]; 519 } 520 521 // Dispatch event. 522 this.adviewNode_.dispatchEvent(evt); 523 } 524 525 addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); }); 526