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
13var eventBindings = require('event_bindings');
14var process = requireNative('process');
15var addTagWatcher = require('tagWatcher').addTagWatcher;
16
17/**
18 * Define "allowCustomAdNetworks" function such that the
19 * "kEnableAdviewSrcAttribute" flag is respected.
20 */
21function 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 */
29var 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 */
40var 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 */
64var AD_VIEW_API_METHODS = [
65 // Empty for now.
66];
67
68var createEvent = function(name) {
69  var eventOpts = {supportsListeners: true, supportsFilters: true};
70  return new eventBindings.Event(name, undefined, eventOpts);
71};
72
73var AdviewLoadAbortEvent = createEvent('adview.onLoadAbort');
74var AdviewLoadCommitEvent = createEvent('adview.onLoadCommit');
75
76var 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 */
100var 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 */
111function 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 */
123function 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 */
140AdView.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 */
161AdView.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 */
172AdView.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 */
187AdView.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 */
206AdView.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 */
223AdView.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 */
238AdView.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 */
276AdView.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 */
297AdView.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 */
310AdView.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 */
323AdView.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 */
349AdView.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 */
379AdView.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 */
412AdView.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 */
448AdView.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 */
469AdView.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 */
487AdView.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
498addTagWatcher('ADVIEW', function(addedNode) { new AdView(addedNode); });
499