Javascript  |  499行  |  15.1 KB

// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Shim that simulates a <adview> tag via Mutation Observers.
//
// The actual tag is implemented via the browser plugin. The internals of this
// are hidden via Shadow DOM.

// TODO(rpaquay): This file is currently very similar to "web_view.js". Do we
//                want to refactor to extract common pieces?

var eventBindings = require('event_bindings');
var process = requireNative('process');
var addTagWatcher = require('tagWatcher').addTagWatcher;

/**
 * Define "allowCustomAdNetworks" function such that the
 * "kEnableAdviewSrcAttribute" flag is respected.
 */
function allowCustomAdNetworks() {
  return process.HasSwitch('enable-adview-src-attribute');
}

/**
 * List of attribute names to "blindly" sync between <adview> tag and internal
 * browser plugin.
 */
var AD_VIEW_ATTRIBUTES = [
  'name',
];

/**
 * List of custom attributes (and their behavior).
 *
 * name: attribute name.
 * onMutation(adview, mutation): callback invoked when attribute is mutated.
 * isProperty: True if the attribute should be exposed as a property.
 */
var AD_VIEW_CUSTOM_ATTRIBUTES = [
  {
    name: 'ad-network',
    onMutation: function(adview, mutation) {
      adview.handleAdNetworkMutation(mutation);
    },
    isProperty: function() {
      return true;
    }
  },
  {
    name: 'src',
    onMutation: function(adview, mutation) {
      adview.handleSrcMutation(mutation);
    },
    isProperty: function() {
      return allowCustomAdNetworks();
    }
  }
];

/**
 * List of api methods. These are forwarded to the browser plugin.
 */
var AD_VIEW_API_METHODS = [
 // Empty for now.
];

var createEvent = function(name) {
  var eventOpts = {supportsListeners: true, supportsFilters: true};
  return new eventBindings.Event(name, undefined, eventOpts);
};

var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort');
var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit');

var AD_VIEW_EXT_EVENTS = {
  'loadabort': {
    evt: AdviewLoadAbortEvent,
    fields: ['url', 'isTopLevel', 'reason']
  },
  'loadcommit': {
    customHandler: function(adview, event) {
      if (event.isTopLevel) {
        adview.browserPluginNode_.setAttribute('src', event.url);
      }
    },
    evt: AdviewLoadCommitEvent,
    fields: ['url', 'isTopLevel']
  }
};

/**
 * List of supported ad-networks.
 *
 * name: identifier of the ad-network, corresponding to a valid value
 *       of the "ad-network" attribute of an <adview> element.
 * url: url to navigate to when initially displaying the <adview>.
 * origin: origin of urls the <adview> is allowed navigate to.
 */
var AD_VIEW_AD_NETWORKS_WHITELIST = [
  {
    name: 'admob',
    url: 'https://admob-sdk.doubleclick.net/chromeapps',
    origin: 'https://double.net'
  },
];

/**
 * Return the whitelisted ad-network entry named |name|.
 */
function getAdNetworkInfo(name) {
  var result = null;
  $Array.forEach(AD_VIEW_AD_NETWORKS_WHITELIST, function(item) {
    if (item.name === name)
      result = item;
  });
  return result;
}

/**
 * @constructor
 */
function AdView(adviewNode) {
  this.adviewNode_ = adviewNode;
  this.browserPluginNode_ = this.createBrowserPluginNode_();
  var shadowRoot = this.adviewNode_.webkitCreateShadowRoot();
  shadowRoot.appendChild(this.browserPluginNode_);

  this.setupCustomAttributes_();
  this.setupAdviewNodeObservers_();
  this.setupAdviewNodeMethods_();
  this.setupAdviewNodeProperties_();
  this.setupAdviewNodeEvents_();
  this.setupBrowserPluginNodeObservers_();
}

/**
 * @private
 */
AdView.prototype.createBrowserPluginNode_ = function() {
  var browserPluginNode = document.createElement('object');
  browserPluginNode.type = 'application/browser-plugin';
  // The <object> node fills in the <adview> container.
  browserPluginNode.style.width = '100%';
  browserPluginNode.style.height = '100%';
  $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) {
    // Only copy attributes that have been assigned values, rather than copying
    // a series of undefined attributes to BrowserPlugin.
    if (this.adviewNode_.hasAttribute(attributeName)) {
      browserPluginNode.setAttribute(
        attributeName, this.adviewNode_.getAttribute(attributeName));
    }
  }, this);

  return browserPluginNode;
}

/**
 * @private
 */
AdView.prototype.setupCustomAttributes_ = function() {
  $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) {
    if (attributeInfo.onMutation) {
      attributeInfo.onMutation(this);
    }
  }, this);
}

/**
 * @private
 */
AdView.prototype.setupAdviewNodeMethods_ = function() {
  // this.browserPluginNode_[apiMethod] are not necessarily defined immediately
  // after the shadow object is appended to the shadow root.
  var self = this;
  $Array.forEach(AD_VIEW_API_METHODS, function(apiMethod) {
    self.adviewNode_[apiMethod] = function(var_args) {
      return $Function.apply(self.browserPluginNode_[apiMethod],
        self.browserPluginNode_, arguments);
    };
  }, this);
}

/**
 * @private
 */
AdView.prototype.setupAdviewNodeObservers_ = function() {
  // Map attribute modifications on the <adview> tag to property changes in
  // the underlying <object> node.
  var handleMutation = $Function.bind(function(mutation) {
    this.handleAdviewAttributeMutation_(mutation);
  }, this);
  var observer = new MutationObserver(function(mutations) {
    $Array.forEach(mutations, handleMutation);
  });
  observer.observe(
      this.adviewNode_,
      {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES});

  this.setupAdviewNodeCustomObservers_();
}

/**
 * @private
 */
AdView.prototype.setupAdviewNodeCustomObservers_ = function() {
  var handleMutation = $Function.bind(function(mutation) {
    this.handleAdviewCustomAttributeMutation_(mutation);
  }, this);
  var observer = new MutationObserver(function(mutations) {
    $Array.forEach(mutations, handleMutation);
  });
  var customAttributeNames =
    AD_VIEW_CUSTOM_ATTRIBUTES.map(function(item) { return item.name; });
  observer.observe(
      this.adviewNode_,
      {attributes: true, attributeFilter: customAttributeNames});
}

/**
 * @private
 */
AdView.prototype.setupBrowserPluginNodeObservers_ = function() {
  var handleMutation = $Function.bind(function(mutation) {
    this.handleBrowserPluginAttributeMutation_(mutation);
  }, this);
  var objectObserver = new MutationObserver(function(mutations) {
    $Array.forEach(mutations, handleMutation);
  });
  objectObserver.observe(
      this.browserPluginNode_,
      {attributes: true, attributeFilter: AD_VIEW_ATTRIBUTES});
}

/**
 * @private
 */
AdView.prototype.setupAdviewNodeProperties_ = function() {
  var browserPluginNode = this.browserPluginNode_;
  // Expose getters and setters for the attributes.
  $Array.forEach(AD_VIEW_ATTRIBUTES, function(attributeName) {
    Object.defineProperty(this.adviewNode_, attributeName, {
      get: function() {
        return browserPluginNode[attributeName];
      },
      set: function(value) {
        browserPluginNode[attributeName] = value;
      },
      enumerable: true
    });
  }, this);

  // Expose getters and setters for the custom attributes.
  var adviewNode = this.adviewNode_;
  $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(attributeInfo) {
    if (attributeInfo.isProperty()) {
      var attributeName = attributeInfo.name;
      Object.defineProperty(this.adviewNode_, attributeName, {
        get: function() {
          return adviewNode.getAttribute(attributeName);
        },
        set: function(value) {
          adviewNode.setAttribute(attributeName, value);
        },
        enumerable: true
      });
    }
  }, this);

  this.setupAdviewContentWindowProperty_();
}

/**
 * @private
 */
AdView.prototype.setupAdviewContentWindowProperty_ = function() {
  var browserPluginNode = this.browserPluginNode_;
  // We cannot use {writable: true} property descriptor because we want dynamic
  // getter value.
  Object.defineProperty(this.adviewNode_, 'contentWindow', {
    get: function() {
      // TODO(fsamuel): This is a workaround to enable
      // contentWindow.postMessage until http://crbug.com/152006 is fixed.
      if (browserPluginNode.contentWindow)
        return browserPluginNode.contentWindow.self;
      console.error('contentWindow is not available at this time. ' +
          'It will become available when the page has finished loading.');
    },
    // No setter.
    enumerable: true
  });
}

/**
 * @private
 */
AdView.prototype.handleAdviewAttributeMutation_ = function(mutation) {
  // This observer monitors mutations to attributes of the <adview> and
  // updates the BrowserPlugin properties accordingly. In turn, updating
  // a BrowserPlugin property will update the corresponding BrowserPlugin
  // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  // details.
  this.browserPluginNode_[mutation.attributeName] =
      this.adviewNode_.getAttribute(mutation.attributeName);
};

/**
 * @private
 */
AdView.prototype.handleAdviewCustomAttributeMutation_ = function(mutation) {
  $Array.forEach(AD_VIEW_CUSTOM_ATTRIBUTES, function(item) {
    if (mutation.attributeName.toUpperCase() == item.name.toUpperCase()) {
      if (item.onMutation) {
        $Function.bind(item.onMutation, item)(this, mutation);
      }
    }
  }, this);
};

/**
 * @private
 */
AdView.prototype.handleBrowserPluginAttributeMutation_ = function(mutation) {
  // This observer monitors mutations to attributes of the BrowserPlugin and
  // updates the <adview> attributes accordingly.
  if (!this.browserPluginNode_.hasAttribute(mutation.attributeName)) {
    // If an attribute is removed from the BrowserPlugin, then remove it
    // from the <adview> as well.
    this.adviewNode_.removeAttribute(mutation.attributeName);
  } else {
    // Update the <adview> attribute to match the BrowserPlugin attribute.
    // Note: Calling setAttribute on <adview> will trigger its mutation
    // observer which will then propagate that attribute to BrowserPlugin. In
    // cases where we permit assigning a BrowserPlugin attribute the same value
    // again (such as navigation when crashed), this could end up in an infinite
    // loop. Thus, we avoid this loop by only updating the <adview> attribute
    // if the BrowserPlugin attributes differs from it.
    var oldValue = this.adviewNode_.getAttribute(mutation.attributeName);
    var newValue = this.browserPluginNode_.getAttribute(mutation.attributeName);
    if (newValue != oldValue) {
      this.adviewNode_.setAttribute(mutation.attributeName, newValue);
    }
  }
};

/**
 * @private
 */
AdView.prototype.navigateToUrl_ = function(url) {
  var newValue = url;
  var oldValue = this.browserPluginNode_.getAttribute('src');

  if (newValue === oldValue)
    return;

  if (url != null) {
    // Note: Setting the 'src' property directly, as calling setAttribute has no
    // effect due to implementation details of BrowserPlugin.
    this.browserPluginNode_['src'] = url;
    if (allowCustomAdNetworks()) {
      this.adviewNode_.setAttribute('src', url);
    }
  }
  else {
    // Note: Setting the 'src' property directly, as calling setAttribute has no
    // effect due to implementation details of BrowserPlugin.
    // TODO(rpaquay): Due to another implementation detail of BrowserPlugin,
    // this line will leave the "src" attribute value untouched.
    this.browserPluginNode_['src'] = null;
    if (allowCustomAdNetworks()) {
      this.adviewNode_.removeAttribute('src');
    }
  }
}

/**
 * @public
 */
AdView.prototype.handleAdNetworkMutation = function(mutation) {
  if (this.adviewNode_.hasAttribute('ad-network')) {
    var value = this.adviewNode_.getAttribute('ad-network');
    var item = getAdNetworkInfo(value);
    if (item) {
      this.navigateToUrl_(item.url);
    }
    else if (allowCustomAdNetworks()) {
      console.log('The ad-network "' + value + '" is not recognized, ' +
        'but custom ad-networks are enabled.');

      if (mutation) {
        this.navigateToUrl_('');
      }
    }
    else {
      // Ignore the new attribute value and set it to empty string.
      // Avoid infinite loop by checking for empty string as new value.
      if (value != '') {
        console.error('The ad-network "' + value + '" is not recognized.');
        this.adviewNode_.setAttribute('ad-network', '');
      }
      this.navigateToUrl_('');
    }
  }
  else {
    this.navigateToUrl_('');
  }
}

/**
 * @public
 */
AdView.prototype.handleSrcMutation = function(mutation) {
  if (allowCustomAdNetworks()) {
    if (this.adviewNode_.hasAttribute('src')) {
      var newValue = this.adviewNode_.getAttribute('src');
      // Note: Setting the 'src' property directly, as calling setAttribute has
      // no effect due to implementation details of BrowserPlugin.
      this.browserPluginNode_['src'] = newValue;
    }
    else {
      // If an attribute is removed from the <adview>, then remove it
      // from the BrowserPlugin as well.
      // Note: Setting the 'src' property directly, as calling setAttribute has
      // no effect due to implementation details of BrowserPlugin.
      // TODO(rpaquay): Due to another implementation detail of BrowserPlugin,
      // this line will leave the "src" attribute value untouched.
      this.browserPluginNode_['src'] = null;
    }
  }
  else {
    if (this.adviewNode_.hasAttribute('src')) {
      var value = this.adviewNode_.getAttribute('src');
      // Ignore the new attribute value and set it to empty string.
      // Avoid infinite loop by checking for empty string as new value.
      if (value != '') {
        console.error('Setting the "src" attribute of an <adview> ' +
          'element is not supported.  Use the "ad-network" attribute ' +
          'instead.');
        this.adviewNode_.setAttribute('src', '');
      }
    }
  }
}

/**
 * @private
 */
AdView.prototype.setupAdviewNodeEvents_ = function() {
  var self = this;
  var onInstanceIdAllocated = function(e) {
    var detail = e.detail ? JSON.parse(e.detail) : {};
    self.instanceId_ = detail.windowId;
    var params = {
      'api': 'adview'
    };
    self.browserPluginNode_['-internal-attach'](params);

    for (var eventName in AD_VIEW_EXT_EVENTS) {
      self.setupExtEvent_(eventName, AD_VIEW_EXT_EVENTS[eventName]);
    }
  };
  this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
                                           onInstanceIdAllocated);
}

/**
 * @private
 */
AdView.prototype.setupExtEvent_ = function(eventName, eventInfo) {
  var self = this;
  var adviewNode = this.adviewNode_;
  eventInfo.evt.addListener(function(event) {
    var adviewEvent = new Event(eventName, {bubbles: true});
    $Array.forEach(eventInfo.fields, function(field) {
      adviewEvent[field] = event[field];
    });
    if (eventInfo.customHandler) {
      eventInfo.customHandler(self, event);
    }
    adviewNode.dispatchEvent(adviewEvent);
  }, {instanceId: self.instanceId_});
};

/**
 * @public
 */
AdView.prototype.dispatchEvent = function(eventname, detail) {
  // Create event object.
  var evt = new Event(eventname, { bubbles: true });
  for(var item in detail) {
      evt[item] = detail[item];
  }

  // Dispatch event.
  this.adviewNode_.dispatchEvent(evt);
}

addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); });