[Extensions] Update chrome://extensions to use developerPrivate.allowFileAccess
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
bloba9363e1ff0137a2e907509c31a06d38e2aa6e31b
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 /**
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  *            installedByCustodian: boolean,
33  *            installWarnings: (Array|undefined),
34  *            is_hosted_app: boolean,
35  *            is_platform_app: boolean,
36  *            isFromStore: boolean,
37  *            isUnpacked: boolean,
38  *            kioskEnabled: boolean,
39  *            kioskOnly: boolean,
40  *            locationText: string,
41  *            managedInstall: boolean,
42  *            manifestErrors: (Array<RuntimeError>|undefined),
43  *            name: string,
44  *            offlineEnabled: boolean,
45  *            optionsOpenInTab: boolean,
46  *            optionsPageHref: string,
47  *            optionsUrl: string,
48  *            order: number,
49  *            packagedApp: boolean,
50  *            path: (string|undefined),
51  *            policyText: (string|undefined),
52  *            prettifiedPath: (string|undefined),
53  *            recommendedInstall: boolean,
54  *            runtimeErrors: (Array<RuntimeError>|undefined),
55  *            showAllUrls: boolean,
56  *            suspiciousInstall: boolean,
57  *            terminated: boolean,
58  *            updateRequiredByPolicy: boolean,
59  *            version: string,
60  *            views: Array<{renderViewId: number, renderProcessId: number,
61  *                path: string, incognito: boolean,
62  *                generatedBackgroundPage: boolean}>,
63  *            wantsErrorCollection: boolean,
64  *            wantsFileAccess: boolean,
65  *            warnings: (Array|undefined)}}
66  */
67 var ExtensionData;
69 ///////////////////////////////////////////////////////////////////////////////
70 // ExtensionFocusRow:
72 /**
73  * Provides an implementation for a single column grid.
74  * @constructor
75  * @extends {cr.ui.FocusRow}
76  */
77 function ExtensionFocusRow() {}
79 /**
80  * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
81  * @param {Element} focusRow The element that has all the columns.
82  * @param {Node} boundary Focus events are ignored outside of this node.
83  */
84 ExtensionFocusRow.decorate = function(focusRow, boundary) {
85   focusRow.__proto__ = ExtensionFocusRow.prototype;
86   focusRow.decorate(boundary);
89 ExtensionFocusRow.prototype = {
90   __proto__: cr.ui.FocusRow.prototype,
92   /** @override */
93   getEquivalentElement: function(element) {
94     if (this.focusableElements.indexOf(element) > -1)
95       return element;
97     // All elements default to another element with the same type.
98     var columnType = element.getAttribute('column-type');
99     var equivalent = this.querySelector('[column-type=' + columnType + ']');
101     if (!equivalent || !this.canAddElement_(equivalent)) {
102       var actionLinks = ['options', 'website', 'launch', 'localReload'];
103       var optionalControls = ['showButton', 'incognito', 'dev-collectErrors',
104                               'allUrls', 'localUrls'];
105       var removeStyleButtons = ['trash', 'enterprise'];
106       var enableControls = ['terminatedReload', 'repair', 'enabled'];
108       if (actionLinks.indexOf(columnType) > -1)
109         equivalent = this.getFirstFocusableByType_(actionLinks);
110       else if (optionalControls.indexOf(columnType) > -1)
111         equivalent = this.getFirstFocusableByType_(optionalControls);
112       else if (removeStyleButtons.indexOf(columnType) > -1)
113         equivalent = this.getFirstFocusableByType_(removeStyleButtons);
114       else if (enableControls.indexOf(columnType) > -1)
115         equivalent = this.getFirstFocusableByType_(enableControls);
116     }
118     // Return the first focusable element if no equivalent type is found.
119     return equivalent || this.focusableElements[0];
120   },
122   /** Updates the list of focusable elements. */
123   updateFocusableElements: function() {
124     this.focusableElements.length = 0;
126     var focusableCandidates = this.querySelectorAll('[column-type]');
127     for (var i = 0; i < focusableCandidates.length; ++i) {
128       var element = focusableCandidates[i];
129       if (this.canAddElement_(element))
130         this.addFocusableElement(element);
131     }
132   },
134   /**
135    * Get the first focusable element that matches a list of types.
136    * @param {Array<string>} types An array of types to match from.
137    * @return {?Element} Return the first element that matches a type in |types|.
138    * @private
139    */
140   getFirstFocusableByType_: function(types) {
141     for (var i = 0; i < this.focusableElements.length; ++i) {
142       var element = this.focusableElements[i];
143       if (types.indexOf(element.getAttribute('column-type')) > -1)
144         return element;
145     }
146     return null;
147   },
149   /**
150    * Setup a typical column in the ExtensionFocusRow. A column can be any
151    * element and should have an action when clicked/toggled. This function
152    * adds a listener and a handler for an event. Also adds the "column-type"
153    * attribute to make the element focusable in |updateFocusableElements|.
154    * @param {string} query A query to select the element to set up.
155    * @param {string} columnType A tag used to identify the column when
156    *     changing focus.
157    * @param {string} eventType The type of event to listen to.
158    * @param {function(Event)} handler The function that should be called
159    *     by the event.
160    * @private
161    */
162   setupColumn: function(query, columnType, eventType, handler) {
163     var element = this.querySelector(query);
164     element.addEventListener(eventType, handler);
165     element.setAttribute('column-type', columnType);
166   },
168   /**
169    * @param {Element} element
170    * @return {boolean}
171    * @private
172    */
173   canAddElement_: function(element) {
174     if (!element || element.disabled)
175       return false;
177     var developerMode = $('extension-settings').classList.contains('dev-mode');
178     if (this.isDeveloperOption_(element) && !developerMode)
179       return false;
181     for (var el = element; el; el = el.parentElement) {
182       if (el.hidden)
183         return false;
184     }
186     return true;
187   },
189   /**
190    * Returns true if the element should only be shown in developer mode.
191    * @param {Element} element
192    * @return {boolean}
193    * @private
194    */
195   isDeveloperOption_: function(element) {
196     return /^dev-/.test(element.getAttribute('column-type'));
197   },
200 cr.define('extensions', function() {
201   'use strict';
203   /**
204    * Creates a new list of extensions.
205    * @param {Object=} opt_propertyBag Optional properties.
206    * @constructor
207    * @extends {HTMLDivElement}
208    */
209   var ExtensionList = cr.ui.define('div');
211   /**
212    * @type {Object<string, number>} A map from extension id to last reloaded
213    *     timestamp. The timestamp is recorded when the user click the 'Reload'
214    *     link. It is used to refresh the icon of an unpacked extension.
215    *     This persists between calls to decorate.
216    */
217   var extensionReloadedTimestamp = {};
219   ExtensionList.prototype = {
220     __proto__: HTMLDivElement.prototype,
222     /**
223      * Indicates whether an embedded options page that was navigated to through
224      * the '?options=' URL query has been shown to the user. This is necessary
225      * to prevent showExtensionNodes_ from opening the options more than once.
226      * @type {boolean}
227      * @private
228      */
229     optionsShown_: false,
231     /** @private {!cr.ui.FocusGrid} */
232     focusGrid_: new cr.ui.FocusGrid(),
234     /**
235      * Indicates whether an uninstall dialog is being shown to prevent multiple
236      * dialogs from being displayed.
237      * @type {boolean}
238      * @private
239      */
240     uninstallIsShowing_: false,
242     /**
243      * Necessary to only show the butterbar once.
244      * @private {boolean}
245      */
246     butterbarShown_: false,
248     decorate: function() {
249       this.showExtensionNodes_();
250     },
252     getIdQueryParam_: function() {
253       return parseQueryParams(document.location)['id'];
254     },
256     getOptionsQueryParam_: function() {
257       return parseQueryParams(document.location)['options'];
258     },
260     /**
261      * Creates or updates all extension items from scratch.
262      * @private
263      */
264     showExtensionNodes_: function() {
265       // Remove the rows from |focusGrid_| without destroying them.
266       this.focusGrid_.rows.length = 0;
268       // Any node that is not updated will be removed.
269       var seenIds = [];
271       // Iterate over the extension data and add each item to the list.
272       this.data_.extensions.forEach(function(extension, i) {
273         var nextExt = this.data_.extensions[i + 1];
274         var node = $(extension.id);
275         seenIds.push(extension.id);
277         if (node)
278           this.updateNode_(extension, node);
279         else
280           this.createNode_(extension, nextExt ? $(nextExt.id) : null);
281       }, this);
283       // Remove extensions that are no longer installed.
284       var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
285       for (var i = 0; i < nodes.length; ++i) {
286         var node = nodes[i];
287         if (seenIds.indexOf(node.id) < 0) {
288           node.parentElement.removeChild(node);
289           // Unregister the removed node from events.
290           assertInstanceof(node, ExtensionFocusRow).destroy();
291         }
292       }
294       var idToHighlight = this.getIdQueryParam_();
295       if (idToHighlight && $(idToHighlight))
296         this.scrollToNode_(idToHighlight);
298       var idToOpenOptions = this.getOptionsQueryParam_();
299       if (idToOpenOptions && $(idToOpenOptions))
300         this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
302       var noExtensions = this.data_.extensions.length == 0;
303       this.classList.toggle('empty-extension-list', noExtensions);
304     },
306     /** Updates each row's focusable elements without rebuilding the grid. */
307     updateFocusableElements: function() {
308       var rows = document.querySelectorAll('.extension-list-item-wrapper[id]');
309       for (var i = 0; i < rows.length; ++i) {
310         assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements();
311       }
312     },
314     /**
315      * Scrolls the page down to the extension node with the given id.
316      * @param {string} extensionId The id of the extension to scroll to.
317      * @private
318      */
319     scrollToNode_: function(extensionId) {
320       // Scroll offset should be calculated slightly higher than the actual
321       // offset of the element being scrolled to, so that it ends up not all
322       // the way at the top. That way it is clear that there are more elements
323       // above the element being scrolled to.
324       var scrollFudge = 1.2;
325       var scrollTop = $(extensionId).offsetTop - scrollFudge *
326           $(extensionId).clientHeight;
327       setScrollTopForDocument(document, scrollTop);
328     },
330     /**
331      * Synthesizes and initializes an HTML element for the extension metadata
332      * given in |extension|.
333      * @param {!ExtensionData} extension A dictionary of extension metadata.
334      * @param {?Element} nextNode |node| should be inserted before |nextNode|.
335      *     |node| will be appended to the end if |nextNode| is null.
336      * @private
337      */
338     createNode_: function(extension, nextNode) {
339       var template = $('template-collection').querySelector(
340           '.extension-list-item-wrapper');
341       var node = template.cloneNode(true);
342       ExtensionFocusRow.decorate(node, $('extension-settings-list'));
344       var row = assertInstanceof(node, ExtensionFocusRow);
345       row.id = extension.id;
347       // The 'Show Browser Action' button.
348       row.setupColumn('.show-button', 'showButton', 'click', function(e) {
349         chrome.send('extensionSettingsShowButton', [extension.id]);
350       });
352       // The 'allow in incognito' checkbox.
353       row.setupColumn('.incognito-control input', 'incognito', 'change',
354                       function(e) {
355         var butterBar = row.querySelector('.butter-bar');
356         var checked = e.target.checked;
357         if (!this.butterbarShown_) {
358           butterBar.hidden = !checked || extension.is_hosted_app;
359           this.butterbarShown_ = !butterBar.hidden;
360         } else {
361           butterBar.hidden = true;
362         }
363         chrome.developerPrivate.allowIncognito(extension.id, checked);
364       }.bind(this));
366       // The 'collect errors' checkbox. This should only be visible if the
367       // error console is enabled - we can detect this by the existence of the
368       // |errorCollectionEnabled| property.
369       row.setupColumn('.error-collection-control input', 'dev-collectErrors',
370                       'change', function(e) {
371         chrome.send('extensionSettingsEnableErrorCollection',
372                     [extension.id, String(e.target.checked)]);
373       });
375       // The 'allow on all urls' checkbox. This should only be visible if
376       // active script restrictions are enabled. If they are not enabled, no
377       // extensions should want all urls.
378       row.setupColumn('.all-urls-control input', 'allUrls', 'click',
379                       function(e) {
380         chrome.send('extensionSettingsAllowOnAllUrls',
381                     [extension.id, String(e.target.checked)]);
382       });
384       // The 'allow file:// access' checkbox.
385       row.setupColumn('.file-access-control input', 'localUrls', 'click',
386                       function(e) {
387         chrome.developerPrivate.allowFileAccess(extension.id, e.target.checked);
388       });
390       // The 'Options' button or link, depending on its behaviour.
391       // Set an href to get the correct mouse-over appearance (link,
392       // footer) - but the actual link opening is done through chrome.send
393       // with a preventDefault().
394       row.querySelector('.options-link').href = extension.optionsPageHref;
395       row.setupColumn('.options-link', 'options', 'click', function(e) {
396         chrome.send('extensionSettingsOptions', [extension.id]);
397         e.preventDefault();
398       });
400       row.setupColumn('.options-button', 'options', 'click', function(e) {
401         this.showEmbeddedExtensionOptions_(extension.id, false);
402         e.preventDefault();
403       }.bind(this));
405       // The 'View in Web Store/View Web Site' link.
406       row.querySelector('.site-link').setAttribute('column-type', 'website');
408       // The 'Permissions' link.
409       row.setupColumn('.permissions-link', 'details', 'click', function(e) {
410         chrome.send('extensionSettingsPermissions', [extension.id]);
411         e.preventDefault();
412       });
414       // The 'Reload' link.
415       row.setupColumn('.reload-link', 'localReload', 'click', function(e) {
416         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
417         extensionReloadedTimestamp[extension.id] = Date.now();
418       });
420       // The 'Launch' link.
421       row.setupColumn('.launch-link', 'launch', 'click', function(e) {
422         chrome.send('extensionSettingsLaunch', [extension.id]);
423       });
425       // The 'Reload' terminated link.
426       row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
427                       function(e) {
428         chrome.send('extensionSettingsReload', [extension.id]);
429       });
431       // The 'Repair' corrupted link.
432       row.setupColumn('.corrupted-repair-button', 'repair', 'click',
433                       function(e) {
434         chrome.send('extensionSettingsRepair', [extension.id]);
435       });
437       // The 'Enabled' checkbox.
438       row.setupColumn('.enable-checkbox input', 'enabled', 'change',
439                       function(e) {
440         var checked = e.target.checked;
441         // TODO(devlin): What should we do if this fails?
442         chrome.management.setEnabled(extension.id, checked);
444         // This may seem counter-intuitive (to not set/clear the checkmark)
445         // but this page will be updated asynchronously if the extension
446         // becomes enabled/disabled. It also might not become enabled or
447         // disabled, because the user might e.g. get prompted when enabling
448         // and choose not to.
449         e.preventDefault();
450       });
452       // 'Remove' button.
453       var trashTemplate = $('template-collection').querySelector('.trash');
454       var trash = trashTemplate.cloneNode(true);
455       trash.title = loadTimeData.getString('extensionUninstall');
456       trash.hidden = extension.managedInstall;
457       trash.setAttribute('column-type', 'trash');
458       trash.addEventListener('click', function(e) {
459         trash.classList.add('open');
460         trash.classList.toggle('mouse-clicked', e.detail > 0);
461         if (this.uninstallIsShowing_)
462           return;
463         this.uninstallIsShowing_ = true;
464         chrome.management.uninstall(extension.id,
465                                     {showConfirmDialog: true},
466                                     function() {
467           // TODO(devlin): What should we do if the uninstall fails?
468           this.uninstallIsShowing_ = false;
469         }.bind(this));
470       }.bind(this));
471       row.querySelector('.enable-controls').appendChild(trash);
473       // Developer mode ////////////////////////////////////////////////////////
475       // The path, if provided by unpacked extension.
476       row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
477                       function(e) {
478         chrome.send('extensionSettingsShowPath', [String(extension.id)]);
479         e.preventDefault();
480       });
482       // Maintain the order that nodes should be in when creating as well as
483       // when adding only one new row.
484       this.insertBefore(row, nextNode);
485       this.updateNode_(extension, row);
486     },
488     /**
489      * Updates an HTML element for the extension metadata given in |extension|.
490      * @param {!ExtensionData} extension A dictionary of extension metadata.
491      * @param {!ExtensionFocusRow} row The node that is being updated.
492      * @private
493      */
494     updateNode_: function(extension, row) {
495       var isActive = extension.enabled && !extension.terminated;
497       row.classList.toggle('inactive-extension', !isActive);
499       // Hack to keep the closure compiler happy about |remove|.
500       // TODO(hcarmona): Remove this hack when the closure compiler is updated.
501       var node = /** @type {Element} */ (row);
502       node.classList.remove('policy-controlled', 'may-not-modify',
503                             'may-not-remove');
504       var classes = [];
505       if (extension.managedInstall) {
506         classes.push('policy-controlled', 'may-not-modify');
507       } else if (extension.dependentExtensions.length > 0) {
508         classes.push('may-not-remove', 'may-not-modify');
509       } else if (extension.recommendedInstall) {
510         classes.push('may-not-remove');
511       } else if (extension.suspiciousInstall ||
512                  extension.corruptInstall ||
513                  extension.updateRequiredByPolicy) {
514         classes.push('may-not-modify');
515       }
516       row.classList.add.apply(row.classList, classes);
518       row.classList.toggle('extension-highlight',
519                            row.id == this.getIdQueryParam_());
521       var item = row.querySelector('.extension-list-item');
522       // Prevent the image cache of extension icon by using the reloaded
523       // timestamp as a query string. The timestamp is recorded when the user
524       // clicks the 'Reload' link. http://crbug.com/159302.
525       if (extensionReloadedTimestamp[extension.id]) {
526         item.style.backgroundImage =
527             'url(' + extension.icon + '?' +
528             extensionReloadedTimestamp[extension.id] + ')';
529       } else {
530         item.style.backgroundImage = 'url(' + extension.icon + ')';
531       }
533       this.setText_(row, '.extension-title', extension.name);
534       this.setText_(row, '.extension-version', extension.version);
535       this.setText_(row, '.location-text', extension.locationText);
536       this.setText_(row, '.blacklist-text', extension.blacklistText);
537       this.setText_(row, '.extension-description', extension.description);
539       // The 'Show Browser Action' button.
540       this.updateVisibility_(row, '.show-button',
541                              isActive && extension.enable_show_button);
543       // The 'allow in incognito' checkbox.
544       this.updateVisibility_(row, '.incognito-control',
545                              isActive && this.data_.incognitoAvailable,
546                              function(item) {
547         var incognito = item.querySelector('input');
548         incognito.disabled = !extension.incognitoCanBeEnabled;
549         incognito.checked = extension.enabledIncognito;
550       });
552       // Hide butterBar if incognito is not enabled for the extension.
553       var butterBar = row.querySelector('.butter-bar');
554       butterBar.hidden = butterBar.hidden || !extension.enabledIncognito;
556       // The 'collect errors' checkbox. This should only be visible if the
557       // error console is enabled - we can detect this by the existence of the
558       // |errorCollectionEnabled| property.
559       this.updateVisibility_(row, '.error-collection-control',
560                              isActive && extension.wantsErrorCollection,
561                              function(item) {
562         item.querySelector('input').checked = extension.errorCollectionEnabled;
563       });
565       // The 'allow on all urls' checkbox. This should only be visible if
566       // active script restrictions are enabled. If they are not enabled, no
567       // extensions should want all urls.
568       this.updateVisibility_(row, '.all-urls-control',
569                              isActive && extension.showAllUrls, function(item) {
570         item.querySelector('input').checked = extension.allowAllUrls;
571       });
573       // The 'allow file:// access' checkbox.
574       this.updateVisibility_(row, '.file-access-control',
575                              isActive && extension.wantsFileAccess,
576                              function(item) {
577         item.querySelector('input').checked = extension.allowFileAccess;
578       });
580       // The 'Options' button or link, depending on its behaviour.
581       var optionsEnabled = extension.enabled && !!extension.optionsUrl;
582       this.updateVisibility_(row, '.options-link', optionsEnabled &&
583                              extension.optionsOpenInTab);
584       this.updateVisibility_(row, '.options-button', optionsEnabled &&
585                              !extension.optionsOpenInTab);
587       // The 'View in Web Store/View Web Site' link.
588       var siteLinkEnabled = !!extension.homepageUrl &&
589                             !extension.enableExtensionInfoDialog;
590       this.updateVisibility_(row, '.site-link', siteLinkEnabled,
591                              function(item) {
592         item.href = extension.homepageUrl;
593         item.textContent = loadTimeData.getString(
594             extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
595                                          'extensionSettingsVisitWebStore');
596       });
598       // The 'Reload' link.
599       this.updateVisibility_(row, '.reload-link', extension.allow_reload);
601       // The 'Launch' link.
602       this.updateVisibility_(row, '.launch-link', extension.allow_reload &&
603                              extension.is_platform_app);
605       // The 'Reload' terminated link.
606       var isTerminated = extension.terminated;
607       this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
609       // The 'Repair' corrupted link.
610       var canRepair = !isTerminated && extension.corruptInstall &&
611                       extension.isFromStore;
612       this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
614       // The 'Enabled' checkbox.
615       var isOK = !isTerminated && !canRepair;
616       this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
617         var enableCheckboxDisabled = extension.managedInstall ||
618                                      extension.suspiciousInstall ||
619                                      extension.corruptInstall ||
620                                      extension.updateRequiredByPolicy ||
621                                      extension.installedByCustodian ||
622                                      extension.dependentExtensions.length > 0;
623         item.querySelector('input').disabled = enableCheckboxDisabled;
624         item.querySelector('input').checked = extension.enabled;
625       });
627       // Button for extensions controlled by policy.
628       var controlNode = row.querySelector('.enable-controls');
629       var indicator =
630           controlNode.querySelector('.controlled-extension-indicator');
631       var needsIndicator = isOK && extension.managedInstall;
633       if (needsIndicator && !indicator) {
634         indicator = new cr.ui.ControlledIndicator();
635         indicator.classList.add('controlled-extension-indicator');
636         indicator.setAttribute('controlled-by', 'policy');
637         var textPolicy = extension.policyText || '';
638         indicator.setAttribute('textpolicy', textPolicy);
639         indicator.image.setAttribute('aria-label', textPolicy);
640         controlNode.appendChild(indicator);
641         indicator.querySelector('div').setAttribute('column-type',
642                                                     'enterprise');
643       } else if (!needsIndicator && indicator) {
644         controlNode.removeChild(indicator);
645       }
647       // Developer mode ////////////////////////////////////////////////////////
649       // First we have the id.
650       var idLabel = row.querySelector('.extension-id');
651       idLabel.textContent = ' ' + extension.id;
653       // Then the path, if provided by unpacked extension.
654       this.updateVisibility_(row, '.load-path', extension.isUnpacked,
655                              function(item) {
656         item.querySelector('a:first-of-type').textContent =
657             ' ' + extension.prettifiedPath;
658       });
660       // Then the 'managed, cannot uninstall/disable' message.
661       // We would like to hide managed installed message since this
662       // extension is disabled.
663       var isRequired = extension.managedInstall || extension.recommendedInstall;
664       this.updateVisibility_(row, '.managed-message', isRequired &&
665                              !extension.updateRequiredByPolicy);
667       // Then the 'This isn't from the webstore, looks suspicious' message.
668       this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
669                              extension.suspiciousInstall);
671       // Then the 'This is a corrupt extension' message.
672       this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
673                              extension.corruptInstall);
675       // Then the 'An update required by enterprise policy' message. Note that
676       // a force-installed extension might be disabled due to being outdated
677       // as well.
678       this.updateVisibility_(row, '.update-required-message',
679                              extension.updateRequiredByPolicy);
681       // The 'following extensions depend on this extension' list.
682       var hasDependents = extension.dependentExtensions.length > 0;
683       row.classList.toggle('developer-extras', hasDependents);
684       this.updateVisibility_(row, '.dependent-extensions-message',
685                              hasDependents, function(item) {
686         var dependentList = item.querySelector('ul');
687         dependentList.textContent = '';
688         var dependentTemplate = $('template-collection').querySelector(
689             '.dependent-list-item');
690         extension.dependentExtensions.forEach(function(elem) {
691           var depNode = dependentTemplate.cloneNode(true);
692           depNode.querySelector('.dep-extension-title').textContent = elem.name;
693           depNode.querySelector('.dep-extension-id').textContent = elem.id;
694           dependentList.appendChild(depNode);
695         });
696       });
698       // The active views.
699       this.updateVisibility_(row, '.active-views', extension.views.length > 0,
700                              function(item) {
701         var link = item.querySelector('a');
703         // Link needs to be an only child before the list is updated.
704         while (link.nextElementSibling)
705           item.removeChild(link.nextElementSibling);
707         // Link needs to be cleaned up if it was used before.
708         link.textContent = '';
709         if (link.clickHandler)
710           link.removeEventListener('click', link.clickHandler);
712         extension.views.forEach(function(view, i) {
713           var displayName = view.generatedBackgroundPage ?
714               loadTimeData.getString('backgroundPage') : view.path;
715           var label = displayName +
716               (view.incognito ?
717                   ' ' + loadTimeData.getString('viewIncognito') : '') +
718               (view.renderProcessId == -1 ?
719                   ' ' + loadTimeData.getString('viewInactive') : '');
720           link.textContent = label;
721           link.clickHandler = function(e) {
722             // TODO(estade): remove conversion to string?
723             chrome.send('extensionSettingsInspect', [
724               String(extension.id),
725               String(view.renderProcessId),
726               String(view.renderViewId),
727               view.incognito
728             ]);
729           };
730           link.addEventListener('click', link.clickHandler);
732           if (i < extension.views.length - 1) {
733             link = link.cloneNode(true);
734             item.appendChild(link);
735           }
736         });
738         var allLinks = item.querySelectorAll('a');
739         for (var i = 0; i < allLinks.length; ++i) {
740           allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
741         }
742       });
744       // The extension warnings (describing runtime issues).
745       this.updateVisibility_(row, '.extension-warnings', !!extension.warnings,
746                              function(item) {
747         var warningList = item.querySelector('ul');
748         warningList.textContent = '';
749         extension.warnings.forEach(function(warning) {
750           var li = document.createElement('li');
751           warningList.appendChild(li).innerText = warning;
752         });
753       });
755       // If the ErrorConsole is enabled, we should have manifest and/or runtime
756       // errors. Otherwise, we may have install warnings. We should not have
757       // both ErrorConsole errors and install warnings.
758       // Errors.
759       this.updateErrors_(row.querySelector('.manifest-errors'),
760                          'dev-manifestErrors', extension.manifestErrors);
761       this.updateErrors_(row.querySelector('.runtime-errors'),
762                          'dev-runtimeErrors', extension.runtimeErrors);
764       // Install warnings.
765       this.updateVisibility_(row, '.install-warnings',
766                              !!extension.installWarnings, function(item) {
767         var installWarningList = item.querySelector('ul');
768         installWarningList.textContent = '';
769         if (extension.installWarnings) {
770           extension.installWarnings.forEach(function(warning) {
771             var li = document.createElement('li');
772             li.innerText = warning.message;
773             installWarningList.appendChild(li);
774           });
775         }
776       });
778       if (location.hash.substr(1) == extension.id) {
779         // Scroll beneath the fixed header so that the extension is not
780         // obscured.
781         var topScroll = row.offsetTop - $('page-header').offsetHeight;
782         var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
783         if (!isNaN(pad))
784           topScroll -= pad / 2;
785         setScrollTopForDocument(document, topScroll);
786       }
788       row.updateFocusableElements();
789       this.focusGrid_.addRow(row);
790     },
792     /**
793      * Updates an element's textContent.
794      * @param {Element} node Ancestor of the element specified by |query|.
795      * @param {string} query A query to select an element in |node|.
796      * @param {string} textContent
797      * @private
798      */
799     setText_: function(node, query, textContent) {
800       node.querySelector(query).textContent = textContent;
801     },
803     /**
804      * Updates an element's visibility and calls |shownCallback| if it is
805      * visible.
806      * @param {Element} node Ancestor of the element specified by |query|.
807      * @param {string} query A query to select an element in |node|.
808      * @param {boolean} visible Whether the element should be visible or not.
809      * @param {function(Element)=} opt_shownCallback Callback if the element is
810      *     visible. The element passed in will be the element specified by
811      *     |query|.
812      * @private
813      */
814     updateVisibility_: function(node, query, visible, opt_shownCallback) {
815       var item = assert(node.querySelector(query));
816       item.hidden = !visible;
817       if (visible && opt_shownCallback)
818         opt_shownCallback(item);
819     },
821     /**
822      * Updates an element to show a list of errors.
823      * @param {Element} panel An element to hold the errors.
824      * @param {string} columnType A tag used to identify the column when
825      *     changing focus.
826      * @param {Array<RuntimeError>|undefined} errors The errors to be displayed.
827      * @private
828      */
829     updateErrors_: function(panel, columnType, errors) {
830       // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
831       // rebuilding it every time.
832       panel.hidden = !errors || errors.length == 0;
833       panel.textContent = '';
835       if (panel.hidden)
836         return;
838       var errorList =
839           new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
841       panel.appendChild(errorList);
843       var list = errorList.getErrorListElement();
844       if (list)
845         list.setAttribute('column-type', columnType + 'list');
847       var button = errorList.getToggleElement();
848       if (button)
849         button.setAttribute('column-type', columnType + 'button');
850     },
852     /**
853      * Opens the extension options overlay for the extension with the given id.
854      * @param {string} extensionId The id of extension whose options page should
855      *     be displayed.
856      * @param {boolean} scroll Whether the page should scroll to the extension
857      * @private
858      */
859     showEmbeddedExtensionOptions_: function(extensionId, scroll) {
860       if (this.optionsShown_)
861         return;
863       // Get the extension from the given id.
864       var extension = this.data_.extensions.filter(function(extension) {
865         return extension.enabled && extension.id == extensionId;
866       })[0];
868       if (!extension)
869         return;
871       if (scroll)
872         this.scrollToNode_(extensionId);
874       // Add the options query string. Corner case: the 'options' query string
875       // will clobber the 'id' query string if the options link is clicked when
876       // 'id' is in the URL, or if both query strings are in the URL.
877       uber.replaceState({}, '?options=' + extensionId);
879       var overlay = extensions.ExtensionOptionsOverlay.getInstance();
880       var shownCallback = function() {
881         // This overlay doesn't get focused automatically as <extensionoptions>
882         // is created after the overlay is shown.
883         if (cr.ui.FocusOutlineManager.forDocument(document).visible)
884           overlay.setInitialFocus();
885       };
886       overlay.setExtensionAndShowOverlay(extensionId, extension.name,
887                                          extension.icon, shownCallback);
888       this.optionsShown_ = true;
890       var self = this;
891       $('overlay').addEventListener('cancelOverlay', function f() {
892         self.optionsShown_ = false;
893         $('overlay').removeEventListener('cancelOverlay', f);
894       });
896       // TODO(dbeam): why do we need to focus <extensionoptions> before and
897       // after its showing animation? Makes very little sense to me.
898       overlay.setInitialFocus();
899     },
900   };
902   ExtensionList.uninstallCancel = function() {
903     var trash = document.querySelector('.trash.open');
904     if (trash.classList.contains('mouse-clicked'))
905       trash.blur();
906     trash.classList.remove('open');
907   };
909   return {
910     ExtensionList: ExtensionList
911   };