Home | History | Annotate | Download | only in extensions
      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