- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / extensions / ad_view.js
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); });