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() {
11 * @param {string} name The name of the template to clone.
12 * @return {!Element} The freshly cloned template.
14 function cloneTemplate(name
) {
15 var node
= $('templates').querySelector('.' + name
).cloneNode(true);
16 return assertInstanceof(node
, Element
);
20 * @extends {HTMLElement}
23 function ExtensionWrapper() {
24 var wrapper
= cloneTemplate('extension-list-item-wrapper');
25 wrapper
.__proto__
= ExtensionWrapper
.prototype;
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
);
39 /** @return {!cr.ui.FocusRow} */
40 getFocusRow: function() {
41 return this.focusRow_
;
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
54 setupColumn: function(focusType
, query
, opt_eventType
, opt_handler
) {
55 assert(this.focusRow_
.addItem(focusType
, query
));
58 this.querySelector(query
).addEventListener(opt_eventType
, opt_handler
);
63 var ExtensionCommandsOverlay
= extensions
.ExtensionCommandsOverlay
;
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.
71 function compareExtensions(a
, b
) {
72 function compare(x
, y
) {
73 return x
< y
? -1 : (x
> y
? 1 : 0);
75 function compareLocation(x
, y
) {
76 if (x
.location
== y
.location
)
78 if (x
.location
== chrome
.developerPrivate
.Location
.UNPACKED
)
80 if (y
.location
== chrome
.developerPrivate
.Location
.UNPACKED
)
84 return compareLocation(a
, b
) ||
85 compare(a
.name
.toLowerCase(), b
.name
.toLowerCase()) ||
90 function ExtensionListDelegate() {}
92 ExtensionListDelegate
.prototype = {
94 * Called when the number of extensions in the list has changed.
96 onExtensionCountChanged
: assertNotReached
,
100 * Creates a new list of extensions.
101 * @param {extensions.ExtensionListDelegate} delegate
103 * @extends {HTMLDivElement}
105 function ExtensionList(delegate
) {
106 var div
= document
.createElement('div');
107 div
.__proto__
= ExtensionList
.prototype;
108 div
.initialize(delegate
);
112 ExtensionList
.prototype = {
113 __proto__
: HTMLDivElement
.prototype,
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.
122 optionsShown_
: false,
124 /** @private {!cr.ui.FocusGrid} */
125 focusGrid_
: new cr
.ui
.FocusGrid(),
128 * Indicates whether an uninstall dialog is being shown to prevent multiple
129 * dialogs from being displayed.
132 uninstallIsShowing_
: false,
135 * Indicates whether a permissions prompt is showing.
138 permissionsPromptIsShowing_
: false,
141 * Necessary to only show the butterbar once.
144 butterbarShown_
: false,
147 * Whether or not any initial navigation (like scrolling to an extension,
148 * or opening an options page) has occurred.
151 didInitialNavigation_
: false,
154 * Whether or not incognito mode is available.
157 incognitoAvailable_
: false,
160 * Whether or not the app info dialog is enabled.
163 enableAppInfoDialog_
: false,
166 * Initializes the list.
167 * @param {!extensions.ExtensionListDelegate} delegate
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();
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
));
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();
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_
);
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.
230 resetLoadFinished: function() {
232 * |loadFinished| should be used for testing purposes and will be
233 * fulfilled when this list has finished loading the first time.
236 this.loadFinished
= new Promise(function(resolve
, reject
) {
237 /** @private {function(?)} */
238 this.resolveLoadFinished_
= resolve
;
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
247 * @return {Promise} A promise that is resolved once the extensions data is
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
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_
);
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_();
281 return this.extensionsUpdated_
;
285 * Updates elements that need to be visible in order to update properly.
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_
)
294 this.didInitialNavigation_
= true;
295 assert(!this.hidden
);
296 assert(!this.parentElement
.hidden
);
298 var idToHighlight
= this.getIdQueryParam_();
300 var wrapper
= $(idToHighlight
);
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();
312 var idToOpenOptions
= this.getOptionsQueryParam_();
313 if (idToOpenOptions
&& $(idToOpenOptions
))
314 this.showEmbeddedExtensionOptions_(idToOpenOptions
, true);
317 /** @return {number} The number of extensions being displayed. */
318 getNumExtensions: function() {
319 return this.extensions_
.length
;
323 * @param {string} id The id of the extension.
324 * @return {number} The index of the extension with the given id.
327 getIndexOfExtension_: function(id
) {
328 for (var i
= 0; i
< this.extensions_
.length
; ++i
) {
329 if (this.extensions_
[i
].id
== id
)
335 getIdQueryParam_: function() {
336 return parseQueryParams(document
.location
)['id'];
339 getOptionsQueryParam_: function() {
340 return parseQueryParams(document
.location
)['options'];
344 * Creates or updates all extension items from scratch.
347 showExtensionNodes_: function() {
348 // Any node that is not updated will be removed.
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
);
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
);
368 * Removes the wrapper from the DOM and updates the focused element if
370 * @param {!Element} wrapper
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
);
381 var focusableWrapper
= wrappers
[index
+ 1] || wrappers
[index
- 1];
382 if (focusableWrapper
) {
383 var newFocusRow
= focusableWrapper
.getFocusRow();
384 newFocusRow
.getEquivalentElement(document
.activeElement
).focus();
388 var focusRow
= wrapper
.getFocusRow();
389 this.focusGrid_
.removeRow(focusRow
);
390 this.focusGrid_
.ensureRowActive();
393 wrapper
.parentNode
.removeChild(wrapper
);
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.
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
);
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
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
,
430 this.permissionsPromptIsShowing_
= false;
432 this.permissionsPromptIsShowing_
= true;
437 wrapper
.setupColumn('options', '.options-button', 'click', function(e
) {
438 this.showEmbeddedExtensionOptions_(extension
.id
, false);
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
);
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
);
461 // The 'Reload' link.
462 wrapper
.setupColumn('localReload', '.reload-link', 'click', function(e
) {
463 chrome
.developerPrivate
.reload(extension
.id
, {failQuietly
: true});
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
&&
473 var errors
= newEx
.manifestErrors
.concat(newEx
.runtimeErrors
);
474 extensions
.ExtensionErrorOverlay
.getInstance().setErrorsAndShowOverlay(
475 errors
, extensionId
, newEx
.name
);
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',
484 chrome
.developerPrivate
.showPath(extension
.id
);
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
496 // The 'allow in incognito' checkbox.
497 wrapper
.setupColumn('incognito', '.incognito-control input', 'change',
499 var butterBar
= wrapper
.querySelector('.butter-bar');
500 var checked
= e
.target
.checked
;
501 if (!this.butterbarShown_
) {
502 butterBar
.hidden
= !checked
||
504 chrome
.developerPrivate
.ExtensionType
.HOSTED_APP
;
505 this.butterbarShown_
= !butterBar
.hidden
;
507 butterBar
.hidden
= true;
509 chrome
.developerPrivate
.updateExtensionConfiguration({
510 extensionId
: extension
.id
,
511 incognitoAccess
: e
.target
.checked
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
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',
531 chrome
.developerPrivate
.updateExtensionConfiguration({
532 extensionId
: extension
.id
,
533 runOnAllUrls
: e
.target
.checked
537 // The 'allow file:// access' checkbox.
538 wrapper
.setupColumn('localUrls', '.file-access-control input', 'click',
540 chrome
.developerPrivate
.updateExtensionConfiguration({
541 extensionId
: extension
.id
,
542 fileAccess
: e
.target
.checked
546 // The 'Reload' terminated link.
547 wrapper
.setupColumn('terminatedReload', '.terminated-reload-link',
548 'click', function(e
) {
549 chrome
.developerPrivate
.reload(extension
.id
, {failQuietly
: true});
552 // The 'Repair' corrupted link.
553 wrapper
.setupColumn('repair', '.corrupted-repair-button', 'click',
555 chrome
.developerPrivate
.repairExtension(extension
.id
);
558 // The 'Enabled' checkbox.
559 wrapper
.setupColumn('enabled', '.enable-checkbox input', 'change',
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.
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_
)
584 this.uninstallIsShowing_
= true;
585 chrome
.management
.uninstall(extension
.id
,
586 {showConfirmDialog
: true},
588 // TODO(devlin): What should we do if the uninstall fails?
589 this.uninstallIsShowing_
= false;
591 if (trash
.classList
.contains('mouse-clicked'))
594 if (chrome
.runtime
.lastError
) {
595 // The uninstall failed (e.g. a cancel). Allow the trash to close.
596 trash
.classList
.remove('open');
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.
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
);
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.
619 updateWrapper_: function(extension
, wrapper
) {
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');
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_
,
650 var incognito
= item
.querySelector('input');
651 incognito
.disabled
= !extension
.incognitoAccess
.isEnabled
;
652 incognito
.checked
= extension
.incognitoAccess
.isActive
;
655 // Hide butterBar if incognito is not enabled for the extension.
656 var butterBar
= wrapper
.querySelector('.butter-bar');
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
,
667 item
.querySelector('input').checked
=
668 extension
.errorCollection
.isActive
;
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
,
678 item
.querySelector('input').checked
= extension
.runOnAllUrls
.isActive
;
681 // The 'allow file:// access' checkbox.
682 this.updateVisibility_(wrapper
, '.file-access-control',
683 isActive
&& extension
.fileAccess
.isEnabled
,
685 item
.querySelector('input').checked
= extension
.fileAccess
.isActive
;
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
,
700 item
.href
= extension
.homePage
.url
;
701 item
.textContent
= loadTimeData
.getString(
702 extension
.homePage
.specified
? 'extensionSettingsVisitWebsite' :
703 'extensionSettingsVisitWebStore');
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
,
722 var Level
= chrome
.developerPrivate
.ErrorLevel
;
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).
742 icon
.className
= 'extension-error-icon'; // Remove other classes.
743 icon
.classList
.add(map
[highestSeverity
].name
);
746 // The 'Reload' terminated link.
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
;
771 // Indicator for extensions controlled by policy.
772 var controlNode
= wrapper
.querySelector('.enable-controls');
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';
786 case ControllerType
.CHILD_CUSTODIAN
:
787 controlledByStr
= 'child-custodian';
789 case ControllerType
.SUPERVISED_USER_CUSTODIAN
:
790 controlledByStr
= 'supervised-user-custodian';
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
);
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
,
812 item
.querySelector('a:first-of-type').textContent
=
813 ' ' + extension
.prettifiedPath
;
816 // Then the 'managed, cannot uninstall/disable' message.
817 // We would like to hide managed installed message since this
818 // extension is disabled.
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
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
];
854 if (!dependentExtension
)
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
);
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
) {
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');
892 displayName
= view
.url
;
894 var label
= displayName
+
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
908 link
.addEventListener('click', link
.clickHandler
);
910 if (i
< extension
.views
.length
- 1) {
911 link
= link
.cloneNode(true);
912 item
.appendChild(link
);
915 wrapper
.setupColumn('activeView', '.active-views a:last-of-type');
919 // The extension warnings (describing runtime issues).
920 this.updateVisibility_(wrapper
, '.extension-warnings',
921 extension
.runtimeWarnings
.length
> 0,
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
;
932 this.updateVisibility_(wrapper
, '.install-warnings',
933 extension
.installWarnings
.length
> 0,
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
);
946 if (location
.hash
.substr(1) == extension
.id
) {
947 // Scroll beneath the fixed header so that the extension is not
949 var topScroll
= wrapper
.offsetTop
- $('page-header').offsetHeight
;
950 var pad
= parseInt(window
.getComputedStyle(wrapper
).marginTop
, 10);
952 topScroll
-= pad
/ 2;
953 setScrollTopForDocument(document
, topScroll
);
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
964 setText_: function(node
, query
, textContent
) {
965 node
.querySelector(query
).textContent
= textContent
;
969 * Updates an element's visibility and calls |shownCallback| if it is
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
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
);
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
990 * @param {boolean} scroll Whether the page should scroll to the extension
993 showEmbeddedExtensionOptions_: function(extensionId
, scroll
) {
994 if (this.optionsShown_
)
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
;
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();
1022 overlay
.setExtensionAndShow(extensionId
, extension
.name
,
1023 extension
.iconUrl
, shownCallback
);
1024 this.optionsShown_
= true;
1027 $('overlay').addEventListener('cancelOverlay', function f() {
1028 self
.optionsShown_
= false;
1029 $('overlay').removeEventListener('cancelOverlay', f
);
1031 // Remove the options query string.
1032 uber
.replaceState({}, '');
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();
1041 * Hides the extension options overlay for the extension with id
1042 * |extensionId|. If there is an overlay showing for a different extension,
1044 * @param {string} extensionId ID of the extension to hide.
1047 hideEmbeddedExtensionOptions_: function(extensionId
) {
1048 if (!this.optionsShown_
)
1051 var overlay
= extensions
.ExtensionOptionsOverlay
.getInstance();
1052 if (overlay
.getExtensionId() == extensionId
)
1057 * Updates or creates a wrapper for |extension|.
1058 * @param {!ExtensionInfo} extension The information about the extension to
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
1067 this.extensions_
[currIndex
] = extension
;
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
);
1076 var wrapper
= $(extension
.id
);
1078 this.updateWrapper_(extension
, wrapper
);
1080 var nextExt
= this.extensions_
[this.extensions_
.indexOf(extension
) + 1];
1081 this.createWrapper_(extension
, nextExt
? $(nextExt
.id
) : null);
1087 ExtensionList
: ExtensionList
,
1088 ExtensionListDelegate
: ExtensionListDelegate