Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / extensions / extension_list.js
1 // Copyright (c) 2012 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 <include src="extension_error.js">
6
7 /**
8  * The type of the extension data object. The definition is based on
9  * chrome/browser/ui/webui/extensions/extension_basic_info.cc
10  * and
11  * chrome/browser/ui/webui/extensions/extension_settings_handler.cc
12  *     ExtensionSettingsHandler::CreateExtensionDetailValue()
13  * @typedef {{allow_reload: boolean,
14  *            allowAllUrls: boolean,
15  *            allowFileAccess: boolean,
16  *            blacklistText: string,
17  *            corruptInstall: boolean,
18  *            dependentExtensions: Array,
19  *            description: string,
20  *            detailsUrl: string,
21  *            enableExtensionInfoDialog: boolean,
22  *            enable_show_button: boolean,
23  *            enabled: boolean,
24  *            enabledIncognito: boolean,
25  *            errorCollectionEnabled: (boolean|undefined),
26  *            hasPopupAction: boolean,
27  *            homepageProvided: boolean,
28  *            homepageUrl: string,
29  *            icon: string,
30  *            id: string,
31  *            incognitoCanBeEnabled: boolean,
32  *            installWarnings: (Array|undefined),
33  *            is_hosted_app: boolean,
34  *            is_platform_app: boolean,
35  *            isFromStore: boolean,
36  *            isUnpacked: boolean,
37  *            kioskEnabled: boolean,
38  *            kioskOnly: boolean,
39  *            locationText: string,
40  *            managedInstall: boolean,
41  *            manifestErrors: (Array.<RuntimeError>|undefined),
42  *            name: string,
43  *            offlineEnabled: boolean,
44  *            optionsOpenInTab: boolean,
45  *            optionsPageHref: string,
46  *            optionsUrl: string,
47  *            order: number,
48  *            packagedApp: boolean,
49  *            path: (string|undefined),
50  *            prettifiedPath: (string|undefined),
51  *            recommendedInstall: boolean,
52  *            runtimeErrors: (Array.<RuntimeError>|undefined),
53  *            suspiciousInstall: boolean,
54  *            terminated: boolean,
55  *            version: string,
56  *            views: Array.<{renderViewId: number, renderProcessId: number,
57  *                path: string, incognito: boolean,
58  *                generatedBackgroundPage: boolean}>,
59  *            wantsAllUrls: boolean,
60  *            wantsErrorCollection: boolean,
61  *            wantsFileAccess: boolean,
62  *            warnings: (Array|undefined)}}
63  */
64 var ExtensionData;
65
66 cr.define('options', function() {
67   'use strict';
68
69   /**
70    * Creates a new list of extensions.
71    * @param {Object=} opt_propertyBag Optional properties.
72    * @constructor
73    * @extends {HTMLDivElement}
74    */
75   var ExtensionsList = cr.ui.define('div');
76
77   /**
78    * @type {Object.<string, boolean>} A map from extension id to a boolean
79    *     indicating whether the incognito warning is showing. This persists
80    *     between calls to decorate.
81    */
82   var butterBarVisibility = {};
83
84   /**
85    * @type {Object.<string, number>} A map from extension id to last reloaded
86    *     timestamp. The timestamp is recorded when the user click the 'Reload'
87    *     link. It is used to refresh the icon of an unpacked extension.
88    *     This persists between calls to decorate.
89    */
90   var extensionReloadedTimestamp = {};
91
92   ExtensionsList.prototype = {
93     __proto__: HTMLDivElement.prototype,
94
95     /**
96      * Indicates whether an embedded options page that was navigated to through
97      * the '?options=' URL query has been shown to the user. This is necessary
98      * to prevent showExtensionNodes_ from opening the options more than once.
99      * @type {boolean}
100      * @private
101      */
102     optionsShown_: false,
103
104     /** @override */
105     decorate: function() {
106       this.textContent = '';
107
108       this.showExtensionNodes_();
109     },
110
111     getIdQueryParam_: function() {
112       return parseQueryParams(document.location)['id'];
113     },
114
115     getOptionsQueryParam_: function() {
116       return parseQueryParams(document.location)['options'];
117     },
118
119     /**
120      * Creates all extension items from scratch.
121      * @private
122      */
123     showExtensionNodes_: function() {
124       // Iterate over the extension data and add each item to the list.
125       this.data_.extensions.forEach(this.createNode_, this);
126
127       var idToHighlight = this.getIdQueryParam_();
128       if (idToHighlight && $(idToHighlight))
129         this.scrollToNode_(idToHighlight);
130
131       var idToOpenOptions = this.getOptionsQueryParam_();
132       if (idToOpenOptions && $(idToOpenOptions))
133         this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
134
135       if (this.data_.extensions.length == 0)
136         this.classList.add('empty-extension-list');
137       else
138         this.classList.remove('empty-extension-list');
139     },
140
141     /**
142      * Scrolls the page down to the extension node with the given id.
143      * @param {string} extensionId The id of the extension to scroll to.
144      * @private
145      */
146     scrollToNode_: function(extensionId) {
147       // Scroll offset should be calculated slightly higher than the actual
148       // offset of the element being scrolled to, so that it ends up not all
149       // the way at the top. That way it is clear that there are more elements
150       // above the element being scrolled to.
151       var scrollFudge = 1.2;
152       var scrollTop = $(extensionId).offsetTop - scrollFudge *
153           $(extensionId).clientHeight;
154       setScrollTopForDocument(document, scrollTop);
155     },
156
157     /**
158      * Synthesizes and initializes an HTML element for the extension metadata
159      * given in |extension|.
160      * @param {ExtensionData} extension A dictionary of extension metadata.
161      * @private
162      */
163     createNode_: function(extension) {
164       var template = $('template-collection').querySelector(
165           '.extension-list-item-wrapper');
166       var node = template.cloneNode(true);
167       node.id = extension.id;
168
169       if (!extension.enabled || extension.terminated)
170         node.classList.add('inactive-extension');
171
172       if (extension.managedInstall ||
173           extension.dependentExtensions.length > 0) {
174         node.classList.add('may-not-modify');
175         node.classList.add('may-not-remove');
176       } else if (extension.recommendedInstall) {
177         node.classList.add('may-not-remove');
178       } else if (extension.suspiciousInstall || extension.corruptInstall) {
179         node.classList.add('may-not-modify');
180       }
181
182       var idToHighlight = this.getIdQueryParam_();
183       if (node.id == idToHighlight)
184         node.classList.add('extension-highlight');
185
186       var item = node.querySelector('.extension-list-item');
187       // Prevent the image cache of extension icon by using the reloaded
188       // timestamp as a query string. The timestamp is recorded when the user
189       // clicks the 'Reload' link. http://crbug.com/159302.
190       if (extensionReloadedTimestamp[extension.id]) {
191         item.style.backgroundImage =
192             'url(' + extension.icon + '?' +
193             extensionReloadedTimestamp[extension.id] + ')';
194       } else {
195         item.style.backgroundImage = 'url(' + extension.icon + ')';
196       }
197
198       var title = node.querySelector('.extension-title');
199       title.textContent = extension.name;
200
201       var version = node.querySelector('.extension-version');
202       version.textContent = extension.version;
203
204       var locationText = node.querySelector('.location-text');
205       locationText.textContent = extension.locationText;
206
207       var blacklistText = node.querySelector('.blacklist-text');
208       blacklistText.textContent = extension.blacklistText;
209
210       var description = document.createElement('span');
211       description.textContent = extension.description;
212       node.querySelector('.extension-description').appendChild(description);
213
214       // The 'Show Browser Action' button.
215       if (extension.enable_show_button) {
216         var showButton = node.querySelector('.show-button');
217         showButton.addEventListener('click', function(e) {
218           chrome.send('extensionSettingsShowButton', [extension.id]);
219         });
220         showButton.hidden = false;
221       }
222
223       // The 'allow in incognito' checkbox.
224       node.querySelector('.incognito-control').hidden =
225           !this.data_.incognitoAvailable;
226       var incognito = node.querySelector('.incognito-control input');
227       incognito.disabled = !extension.incognitoCanBeEnabled;
228       incognito.checked = extension.enabledIncognito;
229       if (!incognito.disabled) {
230         incognito.addEventListener('change', function(e) {
231           var checked = e.target.checked;
232           butterBarVisibility[extension.id] = checked;
233           butterBar.hidden = !checked || extension.is_hosted_app;
234           chrome.send('extensionSettingsEnableIncognito',
235                       [extension.id, String(checked)]);
236         });
237       }
238       var butterBar = node.querySelector('.butter-bar');
239       butterBar.hidden = !butterBarVisibility[extension.id];
240
241       // The 'collect errors' checkbox. This should only be visible if the
242       // error console is enabled - we can detect this by the existence of the
243       // |errorCollectionEnabled| property.
244       if (extension.wantsErrorCollection) {
245         node.querySelector('.error-collection-control').hidden = false;
246         var errorCollection =
247             node.querySelector('.error-collection-control input');
248         errorCollection.checked = extension.errorCollectionEnabled;
249         errorCollection.addEventListener('change', function(e) {
250           chrome.send('extensionSettingsEnableErrorCollection',
251                       [extension.id, String(e.target.checked)]);
252         });
253       }
254
255       // The 'allow on all urls' checkbox. This should only be visible if
256       // active script restrictions are enabled. If they are not enabled, no
257       // extensions should want all urls.
258       if (extension.wantsAllUrls) {
259         var allUrls = node.querySelector('.all-urls-control');
260         allUrls.addEventListener('click', function(e) {
261           chrome.send('extensionSettingsAllowOnAllUrls',
262                       [extension.id, String(e.target.checked)]);
263         });
264         allUrls.querySelector('input').checked = extension.allowAllUrls;
265         allUrls.hidden = false;
266       }
267
268       // The 'allow file:// access' checkbox.
269       if (extension.wantsFileAccess) {
270         var fileAccess = node.querySelector('.file-access-control');
271         fileAccess.addEventListener('click', function(e) {
272           chrome.send('extensionSettingsAllowFileAccess',
273                       [extension.id, String(e.target.checked)]);
274         });
275         fileAccess.querySelector('input').checked = extension.allowFileAccess;
276         fileAccess.hidden = false;
277       }
278
279       // The 'Options' button or link, depending on its behaviour.
280       if (extension.enabled && extension.optionsUrl) {
281         var options, optionsClickListener;
282         if (extension.optionsOpenInTab) {
283           options = node.querySelector('.options-link');
284           // Set an href to get the correct mouse-over appearance (link,
285           // footer) - but the actual link opening is done through chrome.send
286           // with a preventDefault().
287           options.setAttribute('href', extension.optionsPageHref);
288           optionsClickListener = function() {
289             chrome.send('extensionSettingsOptions', [extension.id]);
290           };
291         } else {
292           options = node.querySelector('.options-button');
293           optionsClickListener = function() {
294             this.showEmbeddedExtensionOptions_(extension.id, false);
295           }.bind(this);
296         }
297         options.addEventListener('click', function(e) {
298           optionsClickListener();
299           e.preventDefault();
300         });
301         options.hidden = false;
302       }
303
304       // The 'Permissions' link.
305       var permissions = node.querySelector('.permissions-link');
306       permissions.addEventListener('click', function(e) {
307         chrome.send('extensionSettingsPermissions', [extension.id]);
308         e.preventDefault();
309       });
310
311       // The 'View in Web Store/View Web Site' link.
312       if (extension.homepageUrl && !extension.enableExtensionInfoDialog) {
313         var siteLink = node.querySelector('.site-link');
314         siteLink.href = extension.homepageUrl;
315         siteLink.textContent = loadTimeData.getString(
316                 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
317                                              'extensionSettingsVisitWebStore');
318         siteLink.hidden = false;
319       }
320
321       if (extension.allow_reload) {
322         // The 'Reload' link.
323         var reload = node.querySelector('.reload-link');
324         reload.addEventListener('click', function(e) {
325           chrome.send('extensionSettingsReload', [extension.id]);
326           extensionReloadedTimestamp[extension.id] = Date.now();
327         });
328         reload.hidden = false;
329
330         if (extension.is_platform_app) {
331           // The 'Launch' link.
332           var launch = node.querySelector('.launch-link');
333           launch.addEventListener('click', function(e) {
334             chrome.send('extensionSettingsLaunch', [extension.id]);
335           });
336           launch.hidden = false;
337         }
338       }
339
340       if (extension.terminated) {
341         var terminatedReload = node.querySelector('.terminated-reload-link');
342         terminatedReload.hidden = false;
343         terminatedReload.onclick = function() {
344           chrome.send('extensionSettingsReload', [extension.id]);
345         };
346       } else if (extension.corruptInstall && extension.isFromStore) {
347         var repair = node.querySelector('.corrupted-repair-button');
348         repair.hidden = false;
349         repair.onclick = function() {
350           chrome.send('extensionSettingsRepair', [extension.id]);
351         };
352       } else {
353         // The 'Enabled' checkbox.
354         var enable = node.querySelector('.enable-checkbox');
355         enable.hidden = false;
356         var enableCheckboxDisabled = extension.managedInstall ||
357                                      extension.suspiciousInstall ||
358                                      extension.corruptInstall ||
359                                      extension.dependentExtensions.length > 0;
360         enable.querySelector('input').disabled = enableCheckboxDisabled;
361
362         if (!enableCheckboxDisabled) {
363           enable.addEventListener('click', function(e) {
364             // When e.target is the label instead of the checkbox, it doesn't
365             // have the checked property and the state of the checkbox is
366             // left unchanged.
367             var checked = e.target.checked;
368             if (checked == undefined)
369               checked = !e.currentTarget.querySelector('input').checked;
370             chrome.send('extensionSettingsEnable',
371                         [extension.id, checked ? 'true' : 'false']);
372
373             // This may seem counter-intuitive (to not set/clear the checkmark)
374             // but this page will be updated asynchronously if the extension
375             // becomes enabled/disabled. It also might not become enabled or
376             // disabled, because the user might e.g. get prompted when enabling
377             // and choose not to.
378             e.preventDefault();
379           });
380         }
381
382         enable.querySelector('input').checked = extension.enabled;
383       }
384
385       // 'Remove' button.
386       var trashTemplate = $('template-collection').querySelector('.trash');
387       var trash = trashTemplate.cloneNode(true);
388       trash.title = loadTimeData.getString('extensionUninstall');
389       trash.addEventListener('click', function(e) {
390         butterBarVisibility[extension.id] = false;
391         chrome.send('extensionSettingsUninstall', [extension.id]);
392       });
393       node.querySelector('.enable-controls').appendChild(trash);
394
395       // Developer mode ////////////////////////////////////////////////////////
396
397       // First we have the id.
398       var idLabel = node.querySelector('.extension-id');
399       idLabel.textContent = ' ' + extension.id;
400
401       // Then the path, if provided by unpacked extension.
402       if (extension.isUnpacked) {
403         var loadPath = node.querySelector('.load-path');
404         loadPath.hidden = false;
405         var pathLink = loadPath.querySelector('a:nth-of-type(1)');
406         pathLink.textContent = ' ' + extension.prettifiedPath;
407         pathLink.addEventListener('click', function(e) {
408           chrome.send('extensionSettingsShowPath', [String(extension.id)]);
409           e.preventDefault();
410         });
411       }
412
413       // Then the 'managed, cannot uninstall/disable' message.
414       if (extension.managedInstall || extension.recommendedInstall) {
415         node.querySelector('.managed-message').hidden = false;
416       } else {
417         if (extension.suspiciousInstall) {
418           // Then the 'This isn't from the webstore, looks suspicious' message.
419           node.querySelector('.suspicious-install-message').hidden = false;
420         }
421         if (extension.corruptInstall) {
422           // Then the 'This is a corrupt extension' message.
423           node.querySelector('.corrupt-install-message').hidden = false;
424         }
425       }
426
427       if (extension.dependentExtensions.length > 0) {
428         var dependentMessage =
429             node.querySelector('.dependent-extensions-message');
430         dependentMessage.hidden = false;
431         var dependentList = dependentMessage.querySelector('ul');
432         var dependentTemplate = $('template-collection').querySelector(
433             '.dependent-list-item');
434         extension.dependentExtensions.forEach(function(elem) {
435           var depNode = dependentTemplate.cloneNode(true);
436           depNode.querySelector('.dep-extension-title').textContent = elem.name;
437           depNode.querySelector('.dep-extension-id').textContent = elem.id;
438           dependentList.appendChild(depNode);
439         });
440       }
441
442       // Then active views.
443       if (extension.views.length > 0) {
444         var activeViews = node.querySelector('.active-views');
445         activeViews.hidden = false;
446         var link = activeViews.querySelector('a');
447
448         extension.views.forEach(function(view, i) {
449           var displayName = view.generatedBackgroundPage ?
450               loadTimeData.getString('backgroundPage') : view.path;
451           var label = displayName +
452               (view.incognito ?
453                   ' ' + loadTimeData.getString('viewIncognito') : '') +
454               (view.renderProcessId == -1 ?
455                   ' ' + loadTimeData.getString('viewInactive') : '');
456           link.textContent = label;
457           link.addEventListener('click', function(e) {
458             // TODO(estade): remove conversion to string?
459             chrome.send('extensionSettingsInspect', [
460               String(extension.id),
461               String(view.renderProcessId),
462               String(view.renderViewId),
463               view.incognito
464             ]);
465           });
466
467           if (i < extension.views.length - 1) {
468             link = link.cloneNode(true);
469             activeViews.appendChild(link);
470           }
471         });
472       }
473
474       // The extension warnings (describing runtime issues).
475       if (extension.warnings) {
476         var panel = node.querySelector('.extension-warnings');
477         panel.hidden = false;
478         var list = panel.querySelector('ul');
479         extension.warnings.forEach(function(warning) {
480           list.appendChild(document.createElement('li')).innerText = warning;
481         });
482       }
483
484       // If the ErrorConsole is enabled, we should have manifest and/or runtime
485       // errors. Otherwise, we may have install warnings. We should not have
486       // both ErrorConsole errors and install warnings.
487       if (extension.manifestErrors) {
488         var panel = node.querySelector('.manifest-errors');
489         panel.hidden = false;
490         panel.appendChild(new extensions.ExtensionErrorList(
491             extension.manifestErrors));
492       }
493       if (extension.runtimeErrors) {
494         var panel = node.querySelector('.runtime-errors');
495         panel.hidden = false;
496         panel.appendChild(new extensions.ExtensionErrorList(
497             extension.runtimeErrors));
498       }
499       if (extension.installWarnings) {
500         var panel = node.querySelector('.install-warnings');
501         panel.hidden = false;
502         var list = panel.querySelector('ul');
503         extension.installWarnings.forEach(function(warning) {
504           var li = document.createElement('li');
505           li.innerText = warning.message;
506           list.appendChild(li);
507         });
508       }
509
510       this.appendChild(node);
511       if (location.hash.substr(1) == extension.id) {
512         // Scroll beneath the fixed header so that the extension is not
513         // obscured.
514         var topScroll = node.offsetTop - $('page-header').offsetHeight;
515         var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
516         if (!isNaN(pad))
517           topScroll -= pad / 2;
518         setScrollTopForDocument(document, topScroll);
519       }
520     },
521
522     /**
523      * Opens the extension options overlay for the extension with the given id.
524      * @param {string} extensionId The id of extension whose options page should
525      *     be displayed.
526      * @param {boolean} scroll Whether the page should scroll to the extension
527      * @private
528      */
529     showEmbeddedExtensionOptions_: function(extensionId, scroll) {
530       if (this.optionsShown_)
531         return;
532
533       // Get the extension from the given id.
534       var extension = this.data_.extensions.filter(function(extension) {
535         return extension.enabled && extension.id == extensionId;
536       })[0];
537
538       if (!extension)
539         return;
540
541       if (scroll)
542         this.scrollToNode_(extensionId);
543       // Add the options query string. Corner case: the 'options' query string
544       // will clobber the 'id' query string if the options link is clicked when
545       // 'id' is in the URL, or if both query strings are in the URL.
546       uber.replaceState({}, '?options=' + extensionId);
547
548       extensions.ExtensionOptionsOverlay.getInstance().
549           setExtensionAndShowOverlay(extensionId,
550                                      extension.name,
551                                      extension.icon);
552
553       this.optionsShown_ = true;
554       $('overlay').addEventListener('cancelOverlay', function() {
555         this.optionsShown_ = false;
556       }.bind(this));
557     },
558   };
559
560   return {
561     ExtensionsList: ExtensionsList
562   };
563 });