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