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 /**
     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