Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blob5c720ef5deb36a7a31a52cd1c5474836b32d6edb
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.
5 <include src="extension_error.js">
7 cr.define('extensions', function() {
8   'use strict';
10   /**
11    * @param {string} name The name of the template to clone.
12    * @return {!Element} The freshly cloned template.
13    */
14   function cloneTemplate(name) {
15     var node = $('templates').querySelector('.' + name).cloneNode(true);
16     return assertInstanceof(node, Element);
17   }
19   /**
20    * @extends {HTMLElement}
21    * @constructor
22    */
23   function ExtensionWrapper() {
24     var wrapper = cloneTemplate('extension-list-item-wrapper');
25     wrapper.__proto__ = ExtensionWrapper.prototype;
26     wrapper.initialize();
27     return wrapper;
28   }
30   ExtensionWrapper.prototype = {
31     __proto__: HTMLElement.prototype,
33     initialize: function() {
34       var boundary = $('extension-settings-list');
35       /** @private {!extensions.FocusRow} */
36       this.focusRow_ = new extensions.FocusRow(this, boundary);
37     },
39     /** @return {!cr.ui.FocusRow} */
40     getFocusRow: function() {
41       return this.focusRow_;
42     },
44     /**
45      * Add an item to the focus row and listen for |eventType| events.
46      * @param {string} focusType A tag used to identify equivalent elements when
47      *     changing focus between rows.
48      * @param {string} query A query to select the element to set up.
49      * @param {string=} opt_eventType The type of event to listen to.
50      * @param {function(Event)=} opt_handler The function that should be called
51      *     by the event.
52      * @private
53      */
54     setupColumn: function(focusType, query, opt_eventType, opt_handler) {
55       assert(this.focusRow_.addItem(focusType, query));
56       if (opt_eventType) {
57         assert(opt_handler);
58         this.querySelector(query).addEventListener(opt_eventType, opt_handler);
59       }
60     },
61   };
63   var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay;
65   /**
66    * Compares two extensions for the order they should appear in the list.
67    * @param {ExtensionInfo} a The first extension.
68    * @param {ExtensionInfo} b The second extension.
69    * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal.
70    */
71   function compareExtensions(a, b) {
72     function compare(x, y) {
73       return x < y ? -1 : (x > y ? 1 : 0);
74     }
75     function compareLocation(x, y) {
76       if (x.location == y.location)
77         return 0;
78       if (x.location == chrome.developerPrivate.Location.UNPACKED)
79         return -1;
80       if (y.location == chrome.developerPrivate.Location.UNPACKED)
81         return 1;
82       return 0;
83     }
84     return compareLocation(a, b) ||
85            compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
86            compare(a.id, b.id);
87   }
89   /** @interface */
90   function ExtensionListDelegate() {}
92   ExtensionListDelegate.prototype = {
93     /**
94      * Called when the number of extensions in the list has changed.
95      */
96     onExtensionCountChanged: assertNotReached,
97   };
99   /**
100    * Creates a new list of extensions.
101    * @param {extensions.ExtensionListDelegate} delegate
102    * @constructor
103    * @extends {HTMLDivElement}
104    */
105   function ExtensionList(delegate) {
106     var div = document.createElement('div');
107     div.__proto__ = ExtensionList.prototype;
108     div.initialize(delegate);
109     return div;
110   }
112   ExtensionList.prototype = {
113     __proto__: HTMLDivElement.prototype,
115     /**
116      * Indicates whether an embedded options page that was navigated to through
117      * the '?options=' URL query has been shown to the user. This is necessary
118      * to prevent showExtensionNodes_ from opening the options more than once.
119      * @type {boolean}
120      * @private
121      */
122     optionsShown_: false,
124     /** @private {!cr.ui.FocusGrid} */
125     focusGrid_: new cr.ui.FocusGrid(),
127     /**
128      * Indicates whether an uninstall dialog is being shown to prevent multiple
129      * dialogs from being displayed.
130      * @private {boolean}
131      */
132     uninstallIsShowing_: false,
134     /**
135      * Indicates whether a permissions prompt is showing.
136      * @private {boolean}
137      */
138     permissionsPromptIsShowing_: false,
140     /**
141      * Necessary to only show the butterbar once.
142      * @private {boolean}
143      */
144     butterbarShown_: false,
146     /**
147      * Whether or not any initial navigation (like scrolling to an extension,
148      * or opening an options page) has occurred.
149      * @private {boolean}
150      */
151     didInitialNavigation_: false,
153     /**
154      * Whether or not incognito mode is available.
155      * @private {boolean}
156      */
157     incognitoAvailable_: false,
159     /**
160      * Whether or not the app info dialog is enabled.
161      * @private {boolean}
162      */
163     enableAppInfoDialog_: false,
165     /**
166      * Initializes the list.
167      * @param {!extensions.ExtensionListDelegate} delegate
168      */
169     initialize: function(delegate) {
170       /** @private {!Array<ExtensionInfo>} */
171       this.extensions_ = [];
173       /** @private {!extensions.ExtensionListDelegate} */
174       this.delegate_ = delegate;
176       this.resetLoadFinished();
178       chrome.developerPrivate.onItemStateChanged.addListener(
179           function(eventData) {
180         var EventType = chrome.developerPrivate.EventType;
181         switch (eventData.event_type) {
182           case EventType.VIEW_REGISTERED:
183           case EventType.VIEW_UNREGISTERED:
184           case EventType.INSTALLED:
185           case EventType.LOADED:
186           case EventType.UNLOADED:
187           case EventType.ERROR_ADDED:
188           case EventType.ERRORS_REMOVED:
189           case EventType.PREFS_CHANGED:
190             if (eventData.extensionInfo) {
191               this.updateOrCreateWrapper_(eventData.extensionInfo);
192               this.focusGrid_.ensureRowActive();
193             }
194             break;
195           case EventType.UNINSTALLED:
196             var index = this.getIndexOfExtension_(eventData.item_id);
197             this.extensions_.splice(index, 1);
198             this.removeWrapper_(getRequiredElement(eventData.item_id));
199             break;
200           default:
201             assertNotReached();
202         }
204         if (eventData.event_type == EventType.UNLOADED)
205           this.hideEmbeddedExtensionOptions_(eventData.item_id);
207         if (eventData.event_type == EventType.INSTALLED ||
208             eventData.event_type == EventType.UNINSTALLED) {
209           this.delegate_.onExtensionCountChanged();
210         }
212         if (eventData.event_type == EventType.LOADED ||
213             eventData.event_type == EventType.UNLOADED ||
214             eventData.event_type == EventType.PREFS_CHANGED ||
215             eventData.event_type == EventType.UNINSTALLED) {
216           // We update the commands overlay whenever an extension is added or
217           // removed (other updates wouldn't affect command-ly things). We
218           // need both UNLOADED and UNINSTALLED since the UNLOADED event results
219           // in an extension losing active keybindings, and UNINSTALLED can
220           // result in the "Keyboard shortcuts" link being removed.
221           ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
222         }
223       }.bind(this));
224     },
226     /**
227      * Resets the |loadFinished| promise so that it can be used again; this
228      * is useful if the page updates and tests need to wait for it to finish.
229      */
230     resetLoadFinished: function() {
231       /**
232        * |loadFinished| should be used for testing purposes and will be
233        * fulfilled when this list has finished loading the first time.
234        * @type {Promise}
235        * */
236       this.loadFinished = new Promise(function(resolve, reject) {
237         /** @private {function(?)} */
238         this.resolveLoadFinished_ = resolve;
239       }.bind(this));
240     },
242     /**
243      * Updates the extensions on the page.
244      * @param {boolean} incognitoAvailable Whether or not incognito is allowed.
245      * @param {boolean} enableAppInfoDialog Whether or not the app info dialog
246      *     is enabled.
247      * @return {Promise} A promise that is resolved once the extensions data is
248      *     fully updated.
249      */
250     updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) {
251       // If we start to need more information about the extension configuration,
252       // consider passing in the full object from the ExtensionSettings.
253       this.incognitoAvailable_ = incognitoAvailable;
254       this.enableAppInfoDialog_ = enableAppInfoDialog;
255       /** @private {Promise} */
256       this.extensionsUpdated_ = new Promise(function(resolve, reject) {
257         chrome.developerPrivate.getExtensionsInfo(
258             {includeDisabled: true, includeTerminated: true},
259             function(extensions) {
260           // Sort in order of unpacked vs. packed, followed by name, followed by
261           // id.
262           extensions.sort(compareExtensions);
263           this.extensions_ = extensions;
264           this.showExtensionNodes_();
266           // We keep the commands overlay's extension info in sync, so that we
267           // don't duplicate the same querying logic there.
268           ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
270           resolve();
272           // |resolve| is async so it's necessary to use |then| here in order to
273           // do work after other |then|s have finished. This is important so
274           // elements are visible when these updates happen.
275           this.extensionsUpdated_.then(function() {
276             this.onUpdateFinished_();
277             this.resolveLoadFinished_();
278           }.bind(this));
279         }.bind(this));
280       }.bind(this));
281       return this.extensionsUpdated_;
282     },
284     /**
285      * Updates elements that need to be visible in order to update properly.
286      * @private
287      */
288     onUpdateFinished_: function() {
289       // Cannot focus or highlight a extension if there are none, and we should
290       // only scroll to a particular extension or open the options page once.
291       if (this.extensions_.length == 0 || this.didInitialNavigation_)
292         return;
294       this.didInitialNavigation_ = true;
295       assert(!this.hidden);
296       assert(!this.parentElement.hidden);
298       var idToHighlight = this.getIdQueryParam_();
299       if (idToHighlight) {
300         var wrapper = $(idToHighlight);
301         if (wrapper) {
302           this.scrollToWrapper_(idToHighlight);
304           var focusRow = wrapper.getFocusRow();
305           (focusRow.getFirstFocusable('enabled') ||
306            focusRow.getFirstFocusable('remove-enterprise') ||
307            focusRow.getFirstFocusable('website') ||
308            focusRow.getFirstFocusable('details')).focus();
309         }
310       }
312       var idToOpenOptions = this.getOptionsQueryParam_();
313       if (idToOpenOptions && $(idToOpenOptions))
314         this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
315     },
317     /** @return {number} The number of extensions being displayed. */
318     getNumExtensions: function() {
319       return this.extensions_.length;
320     },
322     /**
323      * @param {string} id The id of the extension.
324      * @return {number} The index of the extension with the given id.
325      * @private
326      */
327     getIndexOfExtension_: function(id) {
328       for (var i = 0; i < this.extensions_.length; ++i) {
329         if (this.extensions_[i].id == id)
330           return i;
331       }
332       return -1;
333     },
335     getIdQueryParam_: function() {
336       return parseQueryParams(document.location)['id'];
337     },
339     getOptionsQueryParam_: function() {
340       return parseQueryParams(document.location)['options'];
341     },
343     /**
344      * Creates or updates all extension items from scratch.
345      * @private
346      */
347     showExtensionNodes_: function() {
348       // Any node that is not updated will be removed.
349       var seenIds = [];
351       // Iterate over the extension data and add each item to the list.
352       this.extensions_.forEach(function(extension) {
353         seenIds.push(extension.id);
354         this.updateOrCreateWrapper_(extension);
355       }, this);
356       this.focusGrid_.ensureRowActive();
358       // Remove extensions that are no longer installed.
359       var wrappers = document.querySelectorAll(
360           '.extension-list-item-wrapper[id]');
361       Array.prototype.forEach.call(wrappers, function(wrapper) {
362         if (seenIds.indexOf(wrapper.id) < 0)
363           this.removeWrapper_(wrapper);
364       }, this);
365     },
367     /**
368      * Removes the wrapper from the DOM and updates the focused element if
369      * needed.
370      * @param {!Element} wrapper
371      * @private
372      */
373     removeWrapper_: function(wrapper) {
374       // If focus is in the wrapper about to be removed, move it first. This
375       // happens when clicking the trash can to remove an extension.
376       if (wrapper.contains(document.activeElement)) {
377         var wrappers = document.querySelectorAll(
378             '.extension-list-item-wrapper[id]');
379         var index = Array.prototype.indexOf.call(wrappers, wrapper);
380         assert(index != -1);
381         var focusableWrapper = wrappers[index + 1] || wrappers[index - 1];
382         if (focusableWrapper) {
383           var newFocusRow = focusableWrapper.getFocusRow();
384           newFocusRow.getEquivalentElement(document.activeElement).focus();
385         }
386       }
388       var focusRow = wrapper.getFocusRow();
389       this.focusGrid_.removeRow(focusRow);
390       this.focusGrid_.ensureRowActive();
391       focusRow.destroy();
393       wrapper.parentNode.removeChild(wrapper);
394     },
396     /**
397      * Scrolls the page down to the extension node with the given id.
398      * @param {string} extensionId The id of the extension to scroll to.
399      * @private
400      */
401     scrollToWrapper_: function(extensionId) {
402       // Scroll offset should be calculated slightly higher than the actual
403       // offset of the element being scrolled to, so that it ends up not all
404       // the way at the top. That way it is clear that there are more elements
405       // above the element being scrolled to.
406       var wrapper = $(extensionId);
407       var scrollFudge = 1.2;
408       var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight;
409       setScrollTopForDocument(document, scrollTop);
410     },
412     /**
413      * Synthesizes and initializes an HTML element for the extension metadata
414      * given in |extension|.
415      * @param {!ExtensionInfo} extension A dictionary of extension metadata.
416      * @param {?Element} nextWrapper The newly created wrapper will be inserted
417      *     before |nextWrapper| if non-null (else it will be appended to the
418      *     wrapper list).
419      * @private
420      */
421     createWrapper_: function(extension, nextWrapper) {
422       var wrapper = new ExtensionWrapper;
423       wrapper.id = extension.id;
425       // The 'Permissions' link.
426       wrapper.setupColumn('details', '.permissions-link', 'click', function(e) {
427         if (!this.permissionsPromptIsShowing_) {
428           chrome.developerPrivate.showPermissionsDialog(extension.id,
429                                                         function() {
430             this.permissionsPromptIsShowing_ = false;
431           }.bind(this));
432           this.permissionsPromptIsShowing_ = true;
433         }
434         e.preventDefault();
435       });
437       wrapper.setupColumn('options', '.options-button', 'click', function(e) {
438         this.showEmbeddedExtensionOptions_(extension.id, false);
439         e.preventDefault();
440       }.bind(this));
442       // The 'Options' button or link, depending on its behaviour.
443       // Set an href to get the correct mouse-over appearance (link,
444       // footer) - but the actual link opening is done through developerPrivate
445       // API with a preventDefault().
446       wrapper.querySelector('.options-link').href =
447           extension.optionsPage ? extension.optionsPage.url : '';
448       wrapper.setupColumn('options', '.options-link', 'click', function(e) {
449         chrome.developerPrivate.showOptions(extension.id);
450         e.preventDefault();
451       });
453       // The 'View in Web Store/View Web Site' link.
454       wrapper.setupColumn('website', '.site-link');
456       // The 'Launch' link.
457       wrapper.setupColumn('launch', '.launch-link', 'click', function(e) {
458         chrome.management.launchApp(extension.id);
459       });
461       // The 'Reload' link.
462       wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) {
463         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
464       });
466       wrapper.setupColumn('errors', '.errors-link', 'click', function(e) {
467         var extensionId = extension.id;
468         assert(this.extensions_.length > 0);
469         var newEx = this.extensions_.filter(function(e) {
470           return e.state == chrome.developerPrivate.ExtensionState.ENABLED &&
471               e.id == extensionId;
472         })[0];
473         var errors = newEx.manifestErrors.concat(newEx.runtimeErrors);
474         extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay(
475             errors, extensionId, newEx.name);
476       }.bind(this));
478       wrapper.setupColumn('suspiciousLearnMore',
479                           '.suspicious-install-message .learn-more-link');
481       // The path, if provided by unpacked extension.
482       wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click',
483                           function(e) {
484         chrome.developerPrivate.showPath(extension.id);
485         e.preventDefault();
486       });
488       // The 'Show Browser Action' button.
489       wrapper.setupColumn('showButton', '.show-button', 'click', function(e) {
490         chrome.developerPrivate.updateExtensionConfiguration({
491           extensionId: extension.id,
492           showActionButton: true
493         });
494       });
496       // The 'allow in incognito' checkbox.
497       wrapper.setupColumn('incognito', '.incognito-control input', 'change',
498                           function(e) {
499         var butterBar = wrapper.querySelector('.butter-bar');
500         var checked = e.target.checked;
501         if (!this.butterbarShown_) {
502           butterBar.hidden = !checked ||
503               extension.type ==
504                   chrome.developerPrivate.ExtensionType.HOSTED_APP;
505           this.butterbarShown_ = !butterBar.hidden;
506         } else {
507           butterBar.hidden = true;
508         }
509         chrome.developerPrivate.updateExtensionConfiguration({
510           extensionId: extension.id,
511           incognitoAccess: e.target.checked
512         });
513       }.bind(this));
515       // The 'collect errors' checkbox. This should only be visible if the
516       // error console is enabled - we can detect this by the existence of the
517       // |errorCollectionEnabled| property.
518       wrapper.setupColumn('collectErrors', '.error-collection-control input',
519           'change', function(e) {
520         chrome.developerPrivate.updateExtensionConfiguration({
521           extensionId: extension.id,
522           errorCollection: e.target.checked
523         });
524       });
526       // The 'allow on all urls' checkbox. This should only be visible if
527       // active script restrictions are enabled. If they are not enabled, no
528       // extensions should want all urls.
529       wrapper.setupColumn('allUrls', '.all-urls-control input', 'click',
530                           function(e) {
531         chrome.developerPrivate.updateExtensionConfiguration({
532           extensionId: extension.id,
533           runOnAllUrls: e.target.checked
534         });
535       });
537       // The 'allow file:// access' checkbox.
538       wrapper.setupColumn('localUrls', '.file-access-control input', 'click',
539                           function(e) {
540         chrome.developerPrivate.updateExtensionConfiguration({
541           extensionId: extension.id,
542           fileAccess: e.target.checked
543         });
544       });
546       // The 'Reload' terminated link.
547       wrapper.setupColumn('terminatedReload', '.terminated-reload-link',
548                           'click', function(e) {
549         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
550       });
552       // The 'Repair' corrupted link.
553       wrapper.setupColumn('repair', '.corrupted-repair-button', 'click',
554                           function(e) {
555         chrome.developerPrivate.repairExtension(extension.id);
556       });
558       // The 'Enabled' checkbox.
559       wrapper.setupColumn('enabled', '.enable-checkbox input', 'change',
560                           function(e) {
561         var checked = e.target.checked;
562         // TODO(devlin): What should we do if this fails?
563         chrome.management.setEnabled(extension.id, checked);
565         // This may seem counter-intuitive (to not set/clear the checkmark)
566         // but this page will be updated asynchronously if the extension
567         // becomes enabled/disabled. It also might not become enabled or
568         // disabled, because the user might e.g. get prompted when enabling
569         // and choose not to.
570         e.preventDefault();
571       });
573       // 'Remove' button.
574       var trash = cloneTemplate('trash');
575       trash.title = loadTimeData.getString('extensionUninstall');
577       wrapper.querySelector('.enable-controls').appendChild(trash);
579       wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) {
580         trash.classList.add('open');
581         trash.classList.toggle('mouse-clicked', e.detail > 0);
582         if (this.uninstallIsShowing_)
583           return;
584         this.uninstallIsShowing_ = true;
585         chrome.management.uninstall(extension.id,
586                                     {showConfirmDialog: true},
587                                     function() {
588           // TODO(devlin): What should we do if the uninstall fails?
589           this.uninstallIsShowing_ = false;
591           if (trash.classList.contains('mouse-clicked'))
592             trash.blur();
594           if (chrome.runtime.lastError) {
595             // The uninstall failed (e.g. a cancel). Allow the trash to close.
596             trash.classList.remove('open');
597           } else {
598             // Leave the trash open if the uninstall succeded. Otherwise it can
599             // half-close right before it's removed when the DOM is modified.
600           }
601         }.bind(this));
602       }.bind(this));
604       // Maintain the order that nodes should be in when creating as well as
605       // when adding only one new wrapper.
606       this.insertBefore(wrapper, nextWrapper);
607       this.updateWrapper_(extension, wrapper);
609       var nextRow = this.focusGrid_.getRowForRoot(nextWrapper);  // May be null.
610       this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow);
611     },
613     /**
614      * Updates an HTML element for the extension metadata given in |extension|.
615      * @param {!ExtensionInfo} extension A dictionary of extension metadata.
616      * @param {!Element} wrapper The extension wrapper element to update.
617      * @private
618      */
619     updateWrapper_: function(extension, wrapper) {
620       var isActive =
621           extension.state == chrome.developerPrivate.ExtensionState.ENABLED;
622       wrapper.classList.toggle('inactive-extension', !isActive);
623       wrapper.classList.remove('controlled', 'may-not-remove');
625       if (extension.controlledInfo) {
626         wrapper.classList.add('controlled');
627       } else if (!extension.userMayModify ||
628                  extension.mustRemainInstalled ||
629                  extension.dependentExtensions.length > 0) {
630         wrapper.classList.add('may-not-remove');
631       }
633       var item = wrapper.querySelector('.extension-list-item');
634       item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
636       this.setText_(wrapper, '.extension-title', extension.name);
637       this.setText_(wrapper, '.extension-version', extension.version);
638       this.setText_(wrapper, '.location-text', extension.locationText || '');
639       this.setText_(wrapper, '.blacklist-text', extension.blacklistText || '');
640       this.setText_(wrapper, '.extension-description', extension.description);
642       // The 'Show Browser Action' button.
643       this.updateVisibility_(wrapper, '.show-button',
644                              isActive && extension.actionButtonHidden);
646       // The 'allow in incognito' checkbox.
647       this.updateVisibility_(wrapper, '.incognito-control',
648                              isActive && this.incognitoAvailable_,
649                              function(item) {
650         var incognito = item.querySelector('input');
651         incognito.disabled = !extension.incognitoAccess.isEnabled;
652         incognito.checked = extension.incognitoAccess.isActive;
653       });
655       // Hide butterBar if incognito is not enabled for the extension.
656       var butterBar = wrapper.querySelector('.butter-bar');
657       butterBar.hidden =
658           butterBar.hidden || !extension.incognitoAccess.isEnabled;
660       // The 'collect errors' checkbox. This should only be visible if the
661       // error console is enabled - we can detect this by the existence of the
662       // |errorCollectionEnabled| property.
663       this.updateVisibility_(
664           wrapper, '.error-collection-control',
665           isActive && extension.errorCollection.isEnabled,
666           function(item) {
667         item.querySelector('input').checked =
668             extension.errorCollection.isActive;
669       });
671       // The 'allow on all urls' checkbox. This should only be visible if
672       // active script restrictions are enabled. If they are not enabled, no
673       // extensions should want all urls.
674       this.updateVisibility_(
675           wrapper, '.all-urls-control',
676           isActive && extension.runOnAllUrls.isEnabled,
677           function(item) {
678         item.querySelector('input').checked = extension.runOnAllUrls.isActive;
679       });
681       // The 'allow file:// access' checkbox.
682       this.updateVisibility_(wrapper, '.file-access-control',
683                              isActive && extension.fileAccess.isEnabled,
684                              function(item) {
685         item.querySelector('input').checked = extension.fileAccess.isActive;
686       });
688       // The 'Options' button or link, depending on its behaviour.
689       var optionsEnabled = isActive && !!extension.optionsPage;
690       this.updateVisibility_(wrapper, '.options-link', optionsEnabled &&
691                              extension.optionsPage.openInTab);
692       this.updateVisibility_(wrapper, '.options-button', optionsEnabled &&
693                              !extension.optionsPage.openInTab);
695       // The 'View in Web Store/View Web Site' link.
696       var siteLinkEnabled = !!extension.homePage.url &&
697                             !this.enableAppInfoDialog_;
698       this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled,
699                              function(item) {
700         item.href = extension.homePage.url;
701         item.textContent = loadTimeData.getString(
702             extension.homePage.specified ? 'extensionSettingsVisitWebsite' :
703                                            'extensionSettingsVisitWebStore');
704       });
706       var isUnpacked =
707           extension.location == chrome.developerPrivate.Location.UNPACKED;
708       // The 'Reload' link.
709       this.updateVisibility_(wrapper, '.reload-link', isUnpacked);
711       // The 'Launch' link.
712       this.updateVisibility_(
713           wrapper, '.launch-link',
714           isUnpacked && extension.type ==
715                             chrome.developerPrivate.ExtensionType.PLATFORM_APP);
717       // The 'Errors' link.
718       var hasErrors = extension.runtimeErrors.length > 0 ||
719           extension.manifestErrors.length > 0;
720       this.updateVisibility_(wrapper, '.errors-link', hasErrors,
721                              function(item) {
722         var Level = chrome.developerPrivate.ErrorLevel;
724         var map = {};
725         map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'};
726         map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'};
727         map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'};
729         // Find the highest severity of all the errors; manifest errors all have
730         // a 'warning' level severity.
731         var highestSeverity = extension.runtimeErrors.reduce(
732             function(prev, error) {
733           return map[error.severity].weight > map[prev].weight ?
734               error.severity : prev;
735         }, extension.manifestErrors.length ? Level.WARN : Level.LOG);
737         // Adjust the class on the icon.
738         var icon = item.querySelector('.extension-error-icon');
739         // TODO(hcarmona): Populate alt text with a proper description since
740         // this icon conveys the severity of the error. (info, warning, fatal).
741         icon.alt = '';
742         icon.className = 'extension-error-icon';  // Remove other classes.
743         icon.classList.add(map[highestSeverity].name);
744       });
746       // The 'Reload' terminated link.
747       var isTerminated =
748           extension.state == chrome.developerPrivate.ExtensionState.TERMINATED;
749       this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated);
751       // The 'Repair' corrupted link.
752       var canRepair = !isTerminated &&
753                       extension.disableReasons.corruptInstall &&
754                       extension.location ==
755                           chrome.developerPrivate.Location.FROM_STORE;
756       this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair);
758       // The 'Enabled' checkbox.
759       var isOK = !isTerminated && !canRepair;
760       this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) {
761         var enableCheckboxDisabled =
762             !extension.userMayModify ||
763             extension.disableReasons.suspiciousInstall ||
764             extension.disableReasons.corruptInstall ||
765             extension.disableReasons.updateRequired ||
766             extension.dependentExtensions.length > 0;
767         item.querySelector('input').disabled = enableCheckboxDisabled;
768         item.querySelector('input').checked = isActive;
769       });
771       // Indicator for extensions controlled by policy.
772       var controlNode = wrapper.querySelector('.enable-controls');
773       var indicator =
774           controlNode.querySelector('.controlled-extension-indicator');
775       var needsIndicator = isOK && extension.controlledInfo;
777       if (needsIndicator && !indicator) {
778         indicator = new cr.ui.ControlledIndicator();
779         indicator.classList.add('controlled-extension-indicator');
780         var ControllerType = chrome.developerPrivate.ControllerType;
781         var controlledByStr = '';
782         switch (extension.controlledInfo.type) {
783           case ControllerType.POLICY:
784             controlledByStr = 'policy';
785             break;
786           case ControllerType.CHILD_CUSTODIAN:
787             controlledByStr = 'child-custodian';
788             break;
789           case ControllerType.SUPERVISED_USER_CUSTODIAN:
790             controlledByStr = 'supervised-user-custodian';
791             break;
792         }
793         indicator.setAttribute('controlled-by', controlledByStr);
794         var text = extension.controlledInfo.text;
795         indicator.setAttribute('text' + controlledByStr, text);
796         indicator.image.setAttribute('aria-label', text);
797         controlNode.appendChild(indicator);
798         wrapper.setupColumn('remove-enterprise', '[controlled-by] div');
799       } else if (!needsIndicator && indicator) {
800         controlNode.removeChild(indicator);
801       }
803       // Developer mode ////////////////////////////////////////////////////////
805       // First we have the id.
806       var idLabel = wrapper.querySelector('.extension-id');
807       idLabel.textContent = ' ' + extension.id;
809       // Then the path, if provided by unpacked extension.
810       this.updateVisibility_(wrapper, '.load-path', isUnpacked,
811                              function(item) {
812         item.querySelector('a:first-of-type').textContent =
813             ' ' + extension.prettifiedPath;
814       });
816       // Then the 'managed, cannot uninstall/disable' message.
817       // We would like to hide managed installed message since this
818       // extension is disabled.
819       var isRequired =
820           !extension.userMayModify || extension.mustRemainInstalled;
821       this.updateVisibility_(wrapper, '.managed-message', isRequired &&
822                              !extension.disableReasons.updateRequired);
824       // Then the 'This isn't from the webstore, looks suspicious' message.
825       var isSuspicious = extension.disableReasons.suspiciousInstall;
826       this.updateVisibility_(wrapper, '.suspicious-install-message',
827                              !isRequired && isSuspicious);
829       // Then the 'This is a corrupt extension' message.
830       this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired &&
831                              extension.disableReasons.corruptInstall);
833       // Then the 'An update required by enterprise policy' message. Note that
834       // a force-installed extension might be disabled due to being outdated
835       // as well.
836       this.updateVisibility_(wrapper, '.update-required-message',
837                              extension.disableReasons.updateRequired);
839       // The 'following extensions depend on this extension' list.
840       var hasDependents = extension.dependentExtensions.length > 0;
841       wrapper.classList.toggle('developer-extras', hasDependents);
842       this.updateVisibility_(wrapper, '.dependent-extensions-message',
843                              hasDependents, function(item) {
844         var dependentList = item.querySelector('ul');
845         dependentList.textContent = '';
846         extension.dependentExtensions.forEach(function(dependentId) {
847           var dependentExtension = null;
848           for (var i = 0; i < this.extensions_.length; ++i) {
849             if (this.extensions_[i].id == dependentId) {
850               dependentExtension = this.extensions_[i];
851               break;
852             }
853           }
854           if (!dependentExtension)
855             return;
857           var depNode = cloneTemplate('dependent-list-item');
858           depNode.querySelector('.dep-extension-title').textContent =
859               dependentExtension.name;
860           depNode.querySelector('.dep-extension-id').textContent =
861               dependentExtension.id;
862           dependentList.appendChild(depNode);
863         }, this);
864       }.bind(this));
866       // The active views.
867       this.updateVisibility_(wrapper, '.active-views',
868                              extension.views.length > 0, function(item) {
869         var link = item.querySelector('a');
871         // Link needs to be an only child before the list is updated.
872         while (link.nextElementSibling)
873           item.removeChild(link.nextElementSibling);
875         // Link needs to be cleaned up if it was used before.
876         link.textContent = '';
877         if (link.clickHandler)
878           link.removeEventListener('click', link.clickHandler);
880         extension.views.forEach(function(view, i) {
881           if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG ||
882               view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) {
883             return;
884           }
885           var displayName;
886           if (view.url.indexOf('chrome-extension://') == 0) {
887             var pathOffset = 'chrome-extension://'.length + 32 + 1;
888             displayName = view.url.substring(pathOffset);
889             if (displayName == '_generated_background_page.html')
890               displayName = loadTimeData.getString('backgroundPage');
891           } else {
892             displayName = view.url;
893           }
894           var label = displayName +
895               (view.incognito ?
896                   ' ' + loadTimeData.getString('viewIncognito') : '') +
897               (view.renderProcessId == -1 ?
898                   ' ' + loadTimeData.getString('viewInactive') : '');
899           link.textContent = label;
900           link.clickHandler = function(e) {
901             chrome.developerPrivate.openDevTools({
902               extensionId: extension.id,
903               renderProcessId: view.renderProcessId,
904               renderViewId: view.renderViewId,
905               incognito: view.incognito
906             });
907           };
908           link.addEventListener('click', link.clickHandler);
910           if (i < extension.views.length - 1) {
911             link = link.cloneNode(true);
912             item.appendChild(link);
913           }
915           wrapper.setupColumn('activeView', '.active-views a:last-of-type');
916         });
917       });
919       // The extension warnings (describing runtime issues).
920       this.updateVisibility_(wrapper, '.extension-warnings',
921                              extension.runtimeWarnings.length > 0,
922                              function(item) {
923         var warningList = item.querySelector('ul');
924         warningList.textContent = '';
925         extension.runtimeWarnings.forEach(function(warning) {
926           var li = document.createElement('li');
927           warningList.appendChild(li).innerText = warning;
928         });
929       });
931       // Install warnings.
932       this.updateVisibility_(wrapper, '.install-warnings',
933                              extension.installWarnings.length > 0,
934                              function(item) {
935         var installWarningList = item.querySelector('ul');
936         installWarningList.textContent = '';
937         if (extension.installWarnings) {
938           extension.installWarnings.forEach(function(warning) {
939             var li = document.createElement('li');
940             li.innerText = warning;
941             installWarningList.appendChild(li);
942           });
943         }
944       });
946       if (location.hash.substr(1) == extension.id) {
947         // Scroll beneath the fixed header so that the extension is not
948         // obscured.
949         var topScroll = wrapper.offsetTop - $('page-header').offsetHeight;
950         var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10);
951         if (!isNaN(pad))
952           topScroll -= pad / 2;
953         setScrollTopForDocument(document, topScroll);
954       }
955     },
957     /**
958      * Updates an element's textContent.
959      * @param {Node} node Ancestor of the element specified by |query|.
960      * @param {string} query A query to select an element in |node|.
961      * @param {string} textContent
962      * @private
963      */
964     setText_: function(node, query, textContent) {
965       node.querySelector(query).textContent = textContent;
966     },
968     /**
969      * Updates an element's visibility and calls |shownCallback| if it is
970      * visible.
971      * @param {Node} node Ancestor of the element specified by |query|.
972      * @param {string} query A query to select an element in |node|.
973      * @param {boolean} visible Whether the element should be visible or not.
974      * @param {function(Element)=} opt_shownCallback Callback if the element is
975      *     visible. The element passed in will be the element specified by
976      *     |query|.
977      * @private
978      */
979     updateVisibility_: function(node, query, visible, opt_shownCallback) {
980       var element = assertInstanceof(node.querySelector(query), Element);
981       element.hidden = !visible;
982       if (visible && opt_shownCallback)
983         opt_shownCallback(element);
984     },
986     /**
987      * Opens the extension options overlay for the extension with the given id.
988      * @param {string} extensionId The id of extension whose options page should
989      *     be displayed.
990      * @param {boolean} scroll Whether the page should scroll to the extension
991      * @private
992      */
993     showEmbeddedExtensionOptions_: function(extensionId, scroll) {
994       if (this.optionsShown_)
995         return;
997       // Get the extension from the given id.
998       var extension = this.extensions_.filter(function(extension) {
999         return extension.state ==
1000                    chrome.developerPrivate.ExtensionState.ENABLED &&
1001                extension.id == extensionId;
1002       })[0];
1004       if (!extension)
1005         return;
1007       if (scroll)
1008         this.scrollToWrapper_(extensionId);
1010       // Add the options query string. Corner case: the 'options' query string
1011       // will clobber the 'id' query string if the options link is clicked when
1012       // 'id' is in the URL, or if both query strings are in the URL.
1013       uber.replaceState({}, '?options=' + extensionId);
1015       var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1016       var shownCallback = function() {
1017         // This overlay doesn't get focused automatically as <extensionoptions>
1018         // is created after the overlay is shown.
1019         if (cr.ui.FocusOutlineManager.forDocument(document).visible)
1020           overlay.setInitialFocus();
1021       };
1022       overlay.setExtensionAndShow(extensionId, extension.name,
1023                                   extension.iconUrl, shownCallback);
1024       this.optionsShown_ = true;
1026       var self = this;
1027       $('overlay').addEventListener('cancelOverlay', function f() {
1028         self.optionsShown_ = false;
1029         $('overlay').removeEventListener('cancelOverlay', f);
1031         // Remove the options query string.
1032         uber.replaceState({}, '');
1033       });
1035       // TODO(dbeam): why do we need to focus <extensionoptions> before and
1036       // after its showing animation? Makes very little sense to me.
1037       overlay.setInitialFocus();
1038     },
1040     /**
1041      * Hides the extension options overlay for the extension with id
1042      * |extensionId|. If there is an overlay showing for a different extension,
1043      * nothing happens.
1044      * @param {string} extensionId ID of the extension to hide.
1045      * @private
1046      */
1047     hideEmbeddedExtensionOptions_: function(extensionId) {
1048       if (!this.optionsShown_)
1049         return;
1051       var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1052       if (overlay.getExtensionId() == extensionId)
1053         overlay.close();
1054     },
1056     /**
1057      * Updates or creates a wrapper for |extension|.
1058      * @param {!ExtensionInfo} extension The information about the extension to
1059      *     update.
1060      * @private
1061      */
1062     updateOrCreateWrapper_: function(extension) {
1063       var currIndex = this.getIndexOfExtension_(extension.id);
1064       if (currIndex != -1) {
1065         // If there is a current version of the extension, update it with the
1066         // new version.
1067         this.extensions_[currIndex] = extension;
1068       } else {
1069         // If the extension isn't found, push it back and sort. Technically, we
1070         // could optimize by inserting it at the right location, but since this
1071         // only happens on extension install, it's not worth it.
1072         this.extensions_.push(extension);
1073         this.extensions_.sort(compareExtensions);
1074       }
1076       var wrapper = $(extension.id);
1077       if (wrapper) {
1078         this.updateWrapper_(extension, wrapper);
1079       } else {
1080         var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1];
1081         this.createWrapper_(extension, nextExt ? $(nextExt.id) : null);
1082       }
1083     }
1084   };
1086   return {
1087     ExtensionList: ExtensionList,
1088     ExtensionListDelegate: ExtensionListDelegate
1089   };