| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // <include src="extension_error.js"> |
| |
| cr.define('extensions', function() { |
| 'use strict'; |
| |
| var ExtensionType = chrome.developerPrivate.ExtensionType; |
| |
| /** |
| * @param {string} name The name of the template to clone. |
| * @return {!Element} The freshly cloned template. |
| */ |
| function cloneTemplate(name) { |
| var node = $('templates').querySelector('.' + name).cloneNode(true); |
| return assertInstanceof(node, Element); |
| } |
| |
| /** |
| * @extends {HTMLElement} |
| * @constructor |
| */ |
| function ExtensionWrapper() { |
| var wrapper = cloneTemplate('extension-list-item-wrapper'); |
| wrapper.__proto__ = ExtensionWrapper.prototype; |
| wrapper.initialize(); |
| return wrapper; |
| } |
| |
| ExtensionWrapper.prototype = { |
| __proto__: HTMLElement.prototype, |
| |
| initialize: function() { |
| var boundary = $('extension-settings-list'); |
| /** @private {!extensions.FocusRow} */ |
| this.focusRow_ = new extensions.FocusRow(this, boundary); |
| }, |
| |
| /** @return {!cr.ui.FocusRow} */ |
| getFocusRow: function() { |
| return this.focusRow_; |
| }, |
| |
| /** |
| * Add an item to the focus row and listen for |eventType| events. |
| * @param {string} focusType A tag used to identify equivalent elements when |
| * changing focus between rows. |
| * @param {string} query A query to select the element to set up. |
| * @param {string=} opt_eventType The type of event to listen to. |
| * @param {function(Event)=} opt_handler The function that should be called |
| * by the event. |
| * @private |
| */ |
| setupColumn: function(focusType, query, opt_eventType, opt_handler) { |
| assert(this.focusRow_.addItem(focusType, query)); |
| if (opt_eventType) { |
| assert(opt_handler); |
| this.querySelector(query).addEventListener(opt_eventType, opt_handler); |
| } |
| }, |
| }; |
| |
| var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay; |
| |
| /** |
| * Compares two extensions for the order they should appear in the list. |
| * @param {chrome.developerPrivate.ExtensionInfo} a The first extension. |
| * @param {chrome.developerPrivate.ExtensionInfo} b The second extension. |
| * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal. |
| */ |
| function compareExtensions(a, b) { |
| function compare(x, y) { |
| return x < y ? -1 : (x > y ? 1 : 0); |
| } |
| function compareLocation(x, y) { |
| if (x.location == y.location) |
| return 0; |
| if (x.location == chrome.developerPrivate.Location.UNPACKED) |
| return -1; |
| if (y.location == chrome.developerPrivate.Location.UNPACKED) |
| return 1; |
| return 0; |
| } |
| return compareLocation(a, b) || |
| compare(a.name.toLowerCase(), b.name.toLowerCase()) || |
| compare(a.id, b.id); |
| } |
| |
| /** @interface */ |
| function ExtensionListDelegate() {} |
| |
| ExtensionListDelegate.prototype = { |
| /** |
| * Called when the number of extensions in the list has changed. |
| */ |
| onExtensionCountChanged: assertNotReached, |
| }; |
| |
| /** |
| * Creates a new list of extensions. |
| * @param {extensions.ExtensionListDelegate} delegate |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| function ExtensionList(delegate) { |
| var div = document.createElement('div'); |
| div.__proto__ = ExtensionList.prototype; |
| div.initialize(delegate); |
| return div; |
| } |
| |
| ExtensionList.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| /** |
| * Indicates whether an embedded options page that was navigated to through |
| * the '?options=' URL query has been shown to the user. This is necessary |
| * to prevent showExtensionNodes_ from opening the options more than once. |
| * @type {boolean} |
| * @private |
| */ |
| optionsShown_: false, |
| |
| /** @private {!cr.ui.FocusGrid} */ |
| focusGrid_: new cr.ui.FocusGrid(), |
| |
| /** |
| * Indicates whether an uninstall dialog is being shown to prevent multiple |
| * dialogs from being displayed. |
| * @private {boolean} |
| */ |
| uninstallIsShowing_: false, |
| |
| /** |
| * Indicates whether a permissions prompt is showing. |
| * @private {boolean} |
| */ |
| permissionsPromptIsShowing_: false, |
| |
| /** |
| * Whether or not any initial navigation (like scrolling to an extension, |
| * or opening an options page) has occurred. |
| * @private {boolean} |
| */ |
| didInitialNavigation_: false, |
| |
| /** |
| * Whether or not incognito mode is available. |
| * @private {boolean} |
| */ |
| incognitoAvailable_: false, |
| |
| /** |
| * Whether or not the app info dialog is enabled. |
| * @private {boolean} |
| */ |
| enableAppInfoDialog_: false, |
| |
| /** |
| * Initializes the list. |
| * @param {!extensions.ExtensionListDelegate} delegate |
| */ |
| initialize: function(delegate) { |
| /** @private {!Array<chrome.developerPrivate.ExtensionInfo>} */ |
| this.extensions_ = []; |
| |
| /** @private {!extensions.ExtensionListDelegate} */ |
| this.delegate_ = delegate; |
| |
| this.resetLoadFinished(); |
| |
| chrome.developerPrivate.onItemStateChanged.addListener( |
| function(eventData) { |
| var EventType = chrome.developerPrivate.EventType; |
| switch (eventData.event_type) { |
| case EventType.VIEW_REGISTERED: |
| case EventType.VIEW_UNREGISTERED: |
| case EventType.INSTALLED: |
| case EventType.LOADED: |
| case EventType.UNLOADED: |
| case EventType.ERROR_ADDED: |
| case EventType.ERRORS_REMOVED: |
| case EventType.PREFS_CHANGED: |
| if (eventData.extensionInfo) { |
| this.updateOrCreateWrapper_(eventData.extensionInfo); |
| this.focusGrid_.ensureRowActive(); |
| } |
| break; |
| case EventType.UNINSTALLED: |
| var index = this.getIndexOfExtension_(eventData.item_id); |
| this.extensions_.splice(index, 1); |
| this.removeWrapper_(getRequiredElement(eventData.item_id)); |
| break; |
| default: |
| assertNotReached(); |
| } |
| |
| if (eventData.event_type == EventType.UNLOADED) |
| this.hideEmbeddedExtensionOptions_(eventData.item_id); |
| |
| if (eventData.event_type == EventType.INSTALLED || |
| eventData.event_type == EventType.UNINSTALLED) { |
| this.delegate_.onExtensionCountChanged(); |
| } |
| |
| if (eventData.event_type == EventType.LOADED || |
| eventData.event_type == EventType.UNLOADED || |
| eventData.event_type == EventType.PREFS_CHANGED || |
| eventData.event_type == EventType.UNINSTALLED) { |
| // We update the commands overlay whenever an extension is added or |
| // removed (other updates wouldn't affect command-ly things). We |
| // need both UNLOADED and UNINSTALLED since the UNLOADED event results |
| // in an extension losing active keybindings, and UNINSTALLED can |
| // result in the "Keyboard shortcuts" link being removed. |
| ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); |
| } |
| }.bind(this)); |
| }, |
| |
| /** |
| * Resets the |loadFinished| promise so that it can be used again; this |
| * is useful if the page updates and tests need to wait for it to finish. |
| */ |
| resetLoadFinished: function() { |
| /** |
| * |loadFinished| should be used for testing purposes and will be |
| * fulfilled when this list has finished loading the first time. |
| * @type {Promise} |
| * */ |
| this.loadFinished = new Promise(function(resolve, reject) { |
| /** @private {function(?)} */ |
| this.resolveLoadFinished_ = resolve; |
| }.bind(this)); |
| }, |
| |
| /** |
| * Updates the extensions on the page. |
| * @param {boolean} incognitoAvailable Whether or not incognito is allowed. |
| * @param {boolean} enableAppInfoDialog Whether or not the app info dialog |
| * is enabled. |
| * @return {Promise} A promise that is resolved once the extensions data is |
| * fully updated. |
| */ |
| updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) { |
| // If we start to need more information about the extension configuration, |
| // consider passing in the full object from the ExtensionSettings. |
| this.incognitoAvailable_ = incognitoAvailable; |
| this.enableAppInfoDialog_ = enableAppInfoDialog; |
| /** @private {Promise} */ |
| this.extensionsUpdated_ = new Promise(function(resolve, reject) { |
| chrome.developerPrivate.getExtensionsInfo( |
| {includeDisabled: true, includeTerminated: true}, |
| function(extensions) { |
| // Sort in order of unpacked vs. packed, followed by name, followed by |
| // id. |
| extensions.sort(compareExtensions); |
| this.extensions_ = extensions; |
| this.showExtensionNodes_(); |
| |
| // We keep the commands overlay's extension info in sync, so that we |
| // don't duplicate the same querying logic there. |
| ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); |
| |
| resolve(); |
| |
| // |resolve| is async so it's necessary to use |then| here in order to |
| // do work after other |then|s have finished. This is important so |
| // elements are visible when these updates happen. |
| this.extensionsUpdated_.then(function() { |
| this.onUpdateFinished_(); |
| this.resolveLoadFinished_(); |
| }.bind(this)); |
| }.bind(this)); |
| }.bind(this)); |
| return this.extensionsUpdated_; |
| }, |
| |
| /** |
| * Updates elements that need to be visible in order to update properly. |
| * @private |
| */ |
| onUpdateFinished_: function() { |
| // Cannot focus or highlight a extension if there are none, and we should |
| // only scroll to a particular extension or open the options page once. |
| if (this.extensions_.length == 0 || this.didInitialNavigation_) |
| return; |
| |
| this.didInitialNavigation_ = true; |
| assert(!this.hidden); |
| assert(!this.parentElement.hidden); |
| |
| var idToHighlight = this.getIdQueryParam_(); |
| if (idToHighlight) { |
| var wrapper = $(idToHighlight); |
| if (wrapper) { |
| this.scrollToWrapper_(idToHighlight); |
| |
| var focusRow = wrapper.getFocusRow(); |
| (focusRow.getFirstFocusable('enabled') || |
| focusRow.getFirstFocusable('remove-enterprise') || |
| focusRow.getFirstFocusable('website') || |
| focusRow.getFirstFocusable('details')).focus(); |
| } |
| } |
| |
| var idToOpenOptions = this.getOptionsQueryParam_(); |
| if (idToOpenOptions && $(idToOpenOptions)) |
| this.showEmbeddedExtensionOptions_(idToOpenOptions, true); |
| }, |
| |
| /** @return {number} The number of extensions being displayed. */ |
| getNumExtensions: function() { |
| return this.extensions_.length; |
| }, |
| |
| /** |
| * @param {string} id The id of the extension. |
| * @return {number} The index of the extension with the given id. |
| * @private |
| */ |
| getIndexOfExtension_: function(id) { |
| for (var i = 0; i < this.extensions_.length; ++i) { |
| if (this.extensions_[i].id == id) |
| return i; |
| } |
| return -1; |
| }, |
| |
| getIdQueryParam_: function() { |
| return parseQueryParams(document.location)['id']; |
| }, |
| |
| getOptionsQueryParam_: function() { |
| return parseQueryParams(document.location)['options']; |
| }, |
| |
| /** |
| * Creates or updates all extension items from scratch. |
| * @private |
| */ |
| showExtensionNodes_: function() { |
| // Any node that is not updated will be removed. |
| var seenIds = []; |
| |
| // Iterate over the extension data and add each item to the list. |
| this.extensions_.forEach(function(extension) { |
| seenIds.push(extension.id); |
| this.updateOrCreateWrapper_(extension); |
| }, this); |
| this.focusGrid_.ensureRowActive(); |
| |
| // Remove extensions that are no longer installed. |
| var wrappers = document.querySelectorAll( |
| '.extension-list-item-wrapper[id]'); |
| Array.prototype.forEach.call(wrappers, function(wrapper) { |
| if (seenIds.indexOf(wrapper.id) < 0) |
| this.removeWrapper_(wrapper); |
| }, this); |
| }, |
| |
| /** |
| * Removes the wrapper from the DOM and updates the focused element if |
| * needed. |
| * @param {!Element} wrapper |
| * @private |
| */ |
| removeWrapper_: function(wrapper) { |
| // If focus is in the wrapper about to be removed, move it first. This |
| // happens when clicking the trash can to remove an extension. |
| if (wrapper.contains(document.activeElement)) { |
| var wrappers = document.querySelectorAll( |
| '.extension-list-item-wrapper[id]'); |
| var index = Array.prototype.indexOf.call(wrappers, wrapper); |
| assert(index != -1); |
| var focusableWrapper = wrappers[index + 1] || wrappers[index - 1]; |
| if (focusableWrapper) { |
| var newFocusRow = focusableWrapper.getFocusRow(); |
| newFocusRow.getEquivalentElement(document.activeElement).focus(); |
| } |
| } |
| |
| var focusRow = wrapper.getFocusRow(); |
| this.focusGrid_.removeRow(focusRow); |
| this.focusGrid_.ensureRowActive(); |
| focusRow.destroy(); |
| |
| wrapper.parentNode.removeChild(wrapper); |
| }, |
| |
| /** |
| * Scrolls the page down to the extension node with the given id. |
| * @param {string} extensionId The id of the extension to scroll to. |
| * @private |
| */ |
| scrollToWrapper_: function(extensionId) { |
| // Scroll offset should be calculated slightly higher than the actual |
| // offset of the element being scrolled to, so that it ends up not all |
| // the way at the top. That way it is clear that there are more elements |
| // above the element being scrolled to. |
| var wrapper = $(extensionId); |
| var scrollFudge = 1.2; |
| var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight; |
| setScrollTopForDocument(document, scrollTop); |
| }, |
| |
| /** |
| * Synthesizes and initializes an HTML element for the extension metadata |
| * given in |extension|. |
| * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary |
| * of extension metadata. |
| * @param {?Element} nextWrapper The newly created wrapper will be inserted |
| * before |nextWrapper| if non-null (else it will be appended to the |
| * wrapper list). |
| * @private |
| */ |
| createWrapper_: function(extension, nextWrapper) { |
| var wrapper = new ExtensionWrapper; |
| wrapper.id = extension.id; |
| |
| // The 'Permissions' link. |
| wrapper.setupColumn('details', '.permissions-link', 'click', function(e) { |
| if (!this.permissionsPromptIsShowing_) { |
| chrome.developerPrivate.showPermissionsDialog(extension.id, |
| function() { |
| this.permissionsPromptIsShowing_ = false; |
| }.bind(this)); |
| this.permissionsPromptIsShowing_ = true; |
| } |
| e.preventDefault(); |
| }); |
| |
| wrapper.setupColumn('options', '.options-button', 'click', function(e) { |
| this.showEmbeddedExtensionOptions_(extension.id, false); |
| e.preventDefault(); |
| }.bind(this)); |
| |
| // The 'Options' button or link, depending on its behaviour. |
| // Set an href to get the correct mouse-over appearance (link, |
| // footer) - but the actual link opening is done through developerPrivate |
| // API with a preventDefault(). |
| wrapper.querySelector('.options-link').href = |
| extension.optionsPage ? extension.optionsPage.url : ''; |
| wrapper.setupColumn('options', '.options-link', 'click', function(e) { |
| chrome.developerPrivate.showOptions(extension.id); |
| e.preventDefault(); |
| }); |
| |
| // The 'View in Web Store/View Web Site' link. |
| wrapper.setupColumn('website', '.site-link'); |
| |
| // The 'Launch' link. |
| wrapper.setupColumn('launch', '.launch-link', 'click', function(e) { |
| chrome.management.launchApp(extension.id); |
| }); |
| |
| // The 'Reload' link. |
| wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) { |
| chrome.developerPrivate.reload(extension.id, {failQuietly: true}); |
| }); |
| |
| wrapper.setupColumn('errors', '.errors-link', 'click', function(e) { |
| var extensionId = extension.id; |
| assert(this.extensions_.length > 0); |
| var newEx = this.extensions_.filter(function(e) { |
| return e.id == extensionId; |
| })[0]; |
| var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); |
| extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( |
| errors, extensionId, newEx.name); |
| }.bind(this)); |
| |
| wrapper.setupColumn('suspiciousLearnMore', |
| '.suspicious-install-message .learn-more-link'); |
| |
| // The path, if provided by unpacked extension. |
| wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click', |
| function(e) { |
| chrome.developerPrivate.showPath(extension.id); |
| e.preventDefault(); |
| }); |
| |
| // The 'allow in incognito' checkbox. |
| wrapper.setupColumn('incognito', '.incognito-control input', 'change', |
| function(e) { |
| var butterBar = wrapper.querySelector('.butter-bar'); |
| var checked = e.target.checked; |
| butterBar.hidden = !checked || |
| extension.type == ExtensionType.HOSTED_APP; |
| chrome.developerPrivate.updateExtensionConfiguration({ |
| extensionId: extension.id, |
| incognitoAccess: e.target.checked |
| }); |
| }.bind(this)); |
| |
| // The 'collect errors' checkbox. This should only be visible if the |
| // error console is enabled - we can detect this by the existence of the |
| // |errorCollectionEnabled| property. |
| wrapper.setupColumn('collectErrors', '.error-collection-control input', |
| 'change', function(e) { |
| chrome.developerPrivate.updateExtensionConfiguration({ |
| extensionId: extension.id, |
| errorCollection: e.target.checked |
| }); |
| }); |
| |
| // The 'allow on all urls' checkbox. This should only be visible if |
| // active script restrictions are enabled. If they are not enabled, no |
| // extensions should want all urls. |
| wrapper.setupColumn('allUrls', '.all-urls-control input', 'click', |
| function(e) { |
| chrome.developerPrivate.updateExtensionConfiguration({ |
| extensionId: extension.id, |
| runOnAllUrls: e.target.checked |
| }); |
| }); |
| |
| // The 'allow file:// access' checkbox. |
| wrapper.setupColumn('localUrls', '.file-access-control input', 'click', |
| function(e) { |
| chrome.developerPrivate.updateExtensionConfiguration({ |
| extensionId: extension.id, |
| fileAccess: e.target.checked |
| }); |
| }); |
| |
| // The 'Reload' terminated link. |
| wrapper.setupColumn('terminatedReload', '.terminated-reload-link', |
| 'click', function(e) { |
| chrome.developerPrivate.reload(extension.id, {failQuietly: true}); |
| }); |
| |
| // The 'Repair' corrupted link. |
| wrapper.setupColumn('repair', '.corrupted-repair-button', 'click', |
| function(e) { |
| chrome.developerPrivate.repairExtension(extension.id); |
| }); |
| |
| // The 'Enabled' checkbox. |
| wrapper.setupColumn('enabled', '.enable-checkbox input', 'click', |
| function(e) { |
| var checked = e.target.checked; |
| // TODO(devlin): What should we do if this fails? Ideally we want to |
| // show some kind of error or feedback to the user if this fails. |
| chrome.management.setEnabled(extension.id, checked); |
| |
| // This may seem counter-intuitive (to not set/clear the checkmark) |
| // but this page will be updated asynchronously if the extension |
| // becomes enabled/disabled. It also might not become enabled or |
| // disabled, because the user might e.g. get prompted when enabling |
| // and choose not to. |
| e.preventDefault(); |
| }); |
| |
| // 'Remove' button. |
| var trash = cloneTemplate('trash'); |
| trash.title = loadTimeData.getString('extensionUninstall'); |
| |
| wrapper.querySelector('.enable-controls').appendChild(trash); |
| |
| wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) { |
| trash.classList.add('open'); |
| trash.classList.toggle('mouse-clicked', e.detail > 0); |
| if (this.uninstallIsShowing_) |
| return; |
| this.uninstallIsShowing_ = true; |
| chrome.management.uninstall(extension.id, |
| {showConfirmDialog: true}, |
| function() { |
| // TODO(devlin): What should we do if the uninstall fails? |
| this.uninstallIsShowing_ = false; |
| |
| if (trash.classList.contains('mouse-clicked')) |
| trash.blur(); |
| |
| if (chrome.runtime.lastError) { |
| // The uninstall failed (e.g. a cancel). Allow the trash to close. |
| trash.classList.remove('open'); |
| } else { |
| // Leave the trash open if the uninstall succeded. Otherwise it can |
| // half-close right before it's removed when the DOM is modified. |
| } |
| }.bind(this)); |
| }.bind(this)); |
| |
| // Maintain the order that nodes should be in when creating as well as |
| // when adding only one new wrapper. |
| this.insertBefore(wrapper, nextWrapper); |
| this.updateWrapper_(extension, wrapper); |
| |
| var nextRow = this.focusGrid_.getRowForRoot(nextWrapper); // May be null. |
| this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow); |
| }, |
| |
| /** |
| * Updates an HTML element for the extension metadata given in |extension|. |
| * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary of |
| * extension metadata. |
| * @param {!Element} wrapper The extension wrapper element to update. |
| * @private |
| */ |
| updateWrapper_: function(extension, wrapper) { |
| var isActive = |
| extension.state == chrome.developerPrivate.ExtensionState.ENABLED; |
| wrapper.classList.toggle('inactive-extension', !isActive); |
| wrapper.classList.remove('controlled', 'may-not-remove'); |
| |
| if (extension.controlledInfo) { |
| wrapper.classList.add('controlled'); |
| } else if (!extension.userMayModify || |
| extension.mustRemainInstalled || |
| extension.dependentExtensions.length > 0) { |
| wrapper.classList.add('may-not-remove'); |
| } |
| |
| var item = wrapper.querySelector('.extension-list-item'); |
| item.style.backgroundImage = 'url(' + extension.iconUrl + ')'; |
| |
| this.setText_(wrapper, '.extension-title', extension.name); |
| this.setText_(wrapper, '.extension-version', extension.version); |
| this.setText_(wrapper, '.location-text', extension.locationText || ''); |
| this.setText_(wrapper, '.blacklist-text', extension.blacklistText || ''); |
| this.setText_(wrapper, '.extension-description', extension.description); |
| |
| // The 'allow in incognito' checkbox. |
| this.updateVisibility_(wrapper, '.incognito-control', |
| isActive && this.incognitoAvailable_, |
| function(item) { |
| var incognito = item.querySelector('input'); |
| incognito.disabled = !extension.incognitoAccess.isEnabled; |
| incognito.checked = extension.incognitoAccess.isActive; |
| }); |
| var showButterBar = isActive && |
| extension.incognitoAccess.isActive && |
| extension.type != ExtensionType.HOSTED_APP; |
| // The 'allow in incognito' butter bar. |
| this.updateVisibility_(wrapper, '.butter-bar', showButterBar); |
| |
| // The 'collect errors' checkbox. This should only be visible if the |
| // error console is enabled - we can detect this by the existence of the |
| // |errorCollectionEnabled| property. |
| this.updateVisibility_( |
| wrapper, '.error-collection-control', |
| isActive && extension.errorCollection.isEnabled, |
| function(item) { |
| item.querySelector('input').checked = |
| extension.errorCollection.isActive; |
| }); |
| |
| // The 'allow on all urls' checkbox. This should only be visible if |
| // active script restrictions are enabled. If they are not enabled, no |
| // extensions should want all urls. |
| this.updateVisibility_( |
| wrapper, '.all-urls-control', |
| isActive && extension.runOnAllUrls.isEnabled, |
| function(item) { |
| item.querySelector('input').checked = extension.runOnAllUrls.isActive; |
| }); |
| |
| // The 'allow file:// access' checkbox. |
| this.updateVisibility_(wrapper, '.file-access-control', |
| isActive && extension.fileAccess.isEnabled, |
| function(item) { |
| item.querySelector('input').checked = extension.fileAccess.isActive; |
| }); |
| |
| // The 'Options' button or link, depending on its behaviour. |
| var optionsEnabled = isActive && !!extension.optionsPage; |
| this.updateVisibility_(wrapper, '.options-link', optionsEnabled && |
| extension.optionsPage.openInTab); |
| this.updateVisibility_(wrapper, '.options-button', optionsEnabled && |
| !extension.optionsPage.openInTab); |
| |
| // The 'View in Web Store/View Web Site' link. |
| var siteLinkEnabled = !!extension.homePage.url && |
| !this.enableAppInfoDialog_; |
| this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled, |
| function(item) { |
| item.href = extension.homePage.url; |
| item.textContent = loadTimeData.getString( |
| extension.homePage.specified ? 'extensionSettingsVisitWebsite' : |
| 'extensionSettingsVisitWebStore'); |
| }); |
| |
| var isUnpacked = |
| extension.location == chrome.developerPrivate.Location.UNPACKED; |
| // The 'Reload' link. |
| this.updateVisibility_(wrapper, '.reload-link', isActive && isUnpacked); |
| |
| // The 'Launch' link. |
| this.updateVisibility_( |
| wrapper, '.launch-link', |
| isUnpacked && extension.type == ExtensionType.PLATFORM_APP && |
| isActive); |
| |
| // The 'Errors' link. |
| var hasErrors = extension.runtimeErrors.length > 0 || |
| extension.manifestErrors.length > 0; |
| this.updateVisibility_(wrapper, '.errors-link', hasErrors, |
| function(item) { |
| var Level = chrome.developerPrivate.ErrorLevel; |
| |
| var map = {}; |
| map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'}; |
| map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'}; |
| map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'}; |
| |
| // Find the highest severity of all the errors; manifest errors all have |
| // a 'warning' level severity. |
| var highestSeverity = extension.runtimeErrors.reduce( |
| function(prev, error) { |
| return map[error.severity].weight > map[prev].weight ? |
| error.severity : prev; |
| }, extension.manifestErrors.length ? Level.WARN : Level.LOG); |
| |
| // Adjust the class on the icon. |
| var icon = item.querySelector('.extension-error-icon'); |
| // TODO(hcarmona): Populate alt text with a proper description since |
| // this icon conveys the severity of the error. (info, warning, fatal). |
| icon.alt = ''; |
| icon.className = 'extension-error-icon'; // Remove other classes. |
| icon.classList.add(map[highestSeverity].name); |
| }); |
| |
| // The 'Reload' terminated link. |
| var isTerminated = |
| extension.state == chrome.developerPrivate.ExtensionState.TERMINATED; |
| this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated); |
| |
| // The 'Repair' corrupted link. |
| var canRepair = !isTerminated && |
| extension.disableReasons.corruptInstall && |
| extension.location == |
| chrome.developerPrivate.Location.FROM_STORE; |
| this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair); |
| |
| // The 'Enabled' checkbox. |
| var isOK = !isTerminated && !canRepair; |
| this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) { |
| var enableCheckboxDisabled = |
| !extension.userMayModify || |
| extension.disableReasons.suspiciousInstall || |
| extension.disableReasons.corruptInstall || |
| extension.disableReasons.updateRequired || |
| extension.dependentExtensions.length > 0 || |
| extension.state == |
| chrome.developerPrivate.ExtensionState.BLACKLISTED; |
| item.querySelector('input').disabled = enableCheckboxDisabled; |
| item.querySelector('input').checked = isActive; |
| }); |
| |
| // Indicator for extensions controlled by policy. |
| var controlNode = wrapper.querySelector('.enable-controls'); |
| var indicator = |
| controlNode.querySelector('.controlled-extension-indicator'); |
| var needsIndicator = isOK && extension.controlledInfo; |
| |
| if (needsIndicator && !indicator) { |
| indicator = new cr.ui.ControlledIndicator(); |
| indicator.classList.add('controlled-extension-indicator'); |
| var ControllerType = chrome.developerPrivate.ControllerType; |
| var controlledByStr = ''; |
| switch (extension.controlledInfo.type) { |
| case ControllerType.POLICY: |
| controlledByStr = 'policy'; |
| break; |
| case ControllerType.CHILD_CUSTODIAN: |
| controlledByStr = 'child-custodian'; |
| break; |
| case ControllerType.SUPERVISED_USER_CUSTODIAN: |
| controlledByStr = 'supervised-user-custodian'; |
| break; |
| } |
| indicator.setAttribute('controlled-by', controlledByStr); |
| var text = extension.controlledInfo.text; |
| indicator.setAttribute('text' + controlledByStr, text); |
| indicator.image.setAttribute('aria-label', text); |
| controlNode.appendChild(indicator); |
| wrapper.setupColumn('remove-enterprise', '[controlled-by] div'); |
| } else if (!needsIndicator && indicator) { |
| controlNode.removeChild(indicator); |
| } |
| |
| // Developer mode //////////////////////////////////////////////////////// |
| |
| // First we have the id. |
| var idLabel = wrapper.querySelector('.extension-id'); |
| idLabel.textContent = ' ' + extension.id; |
| |
| // Then the path, if provided by unpacked extension. |
| this.updateVisibility_(wrapper, '.load-path', isUnpacked, |
| function(item) { |
| item.querySelector('a:first-of-type').textContent = |
| ' ' + extension.prettifiedPath; |
| }); |
| |
| // Then the 'managed, cannot uninstall/disable' message. |
| // We would like to hide managed installed message since this |
| // extension is disabled. |
| var isRequired = |
| !extension.userMayModify || extension.mustRemainInstalled; |
| this.updateVisibility_(wrapper, '.managed-message', isRequired && |
| !extension.disableReasons.updateRequired); |
| |
| // Then the 'This isn't from the webstore, looks suspicious' message. |
| var isSuspicious = extension.disableReasons.suspiciousInstall; |
| this.updateVisibility_(wrapper, '.suspicious-install-message', |
| !isRequired && isSuspicious); |
| |
| // Then the 'This is a corrupt extension' message. |
| this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired && |
| extension.disableReasons.corruptInstall); |
| |
| // Then the 'An update required by enterprise policy' message. Note that |
| // a force-installed extension might be disabled due to being outdated |
| // as well. |
| this.updateVisibility_(wrapper, '.update-required-message', |
| extension.disableReasons.updateRequired); |
| |
| // The 'following extensions depend on this extension' list. |
| var hasDependents = extension.dependentExtensions.length > 0; |
| wrapper.classList.toggle('developer-extras', hasDependents); |
| this.updateVisibility_(wrapper, '.dependent-extensions-message', |
| hasDependents, function(item) { |
| var dependentList = item.querySelector('ul'); |
| dependentList.textContent = ''; |
| extension.dependentExtensions.forEach(function(dependentExtension) { |
| var depNode = cloneTemplate('dependent-list-item'); |
| depNode.querySelector('.dep-extension-title').textContent = |
| dependentExtension.name; |
| depNode.querySelector('.dep-extension-id').textContent = |
| dependentExtension.id; |
| dependentList.appendChild(depNode); |
| }, this); |
| }.bind(this)); |
| |
| // The active views. |
| this.updateVisibility_(wrapper, '.active-views', |
| extension.views.length > 0, function(item) { |
| var link = item.querySelector('a'); |
| |
| // Link needs to be an only child before the list is updated. |
| while (link.nextElementSibling) |
| item.removeChild(link.nextElementSibling); |
| |
| // Link needs to be cleaned up if it was used before. |
| link.textContent = ''; |
| if (link.clickHandler) |
| link.removeEventListener('click', link.clickHandler); |
| |
| extension.views.forEach(function(view, i) { |
| if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG || |
| view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) { |
| return; |
| } |
| var displayName; |
| if (view.url.startsWith('chrome-extension://')) { |
| var pathOffset = 'chrome-extension://'.length + 32 + 1; |
| displayName = view.url.substring(pathOffset); |
| if (displayName == '_generated_background_page.html') |
| displayName = loadTimeData.getString('backgroundPage'); |
| } else { |
| displayName = view.url; |
| } |
| var label = displayName + |
| (view.incognito ? |
| ' ' + loadTimeData.getString('viewIncognito') : '') + |
| (view.renderProcessId == -1 ? |
| ' ' + loadTimeData.getString('viewInactive') : '') + |
| (view.isIframe ? |
| ' ' + loadTimeData.getString('viewIframe') : ''); |
| link.textContent = label; |
| link.clickHandler = function(e) { |
| chrome.developerPrivate.openDevTools({ |
| extensionId: extension.id, |
| renderProcessId: view.renderProcessId, |
| renderViewId: view.renderViewId, |
| incognito: view.incognito |
| }); |
| }; |
| link.addEventListener('click', link.clickHandler); |
| |
| if (i < extension.views.length - 1) { |
| link = link.cloneNode(true); |
| item.appendChild(link); |
| } |
| |
| wrapper.setupColumn('activeView', '.active-views a:last-of-type'); |
| }); |
| }); |
| |
| // The extension warnings (describing runtime issues). |
| this.updateVisibility_(wrapper, '.extension-warnings', |
| extension.runtimeWarnings.length > 0, |
| function(item) { |
| var warningList = item.querySelector('ul'); |
| warningList.textContent = ''; |
| extension.runtimeWarnings.forEach(function(warning) { |
| var li = document.createElement('li'); |
| warningList.appendChild(li).innerText = warning; |
| }); |
| }); |
| |
| // Install warnings. |
| this.updateVisibility_(wrapper, '.install-warnings', |
| extension.installWarnings.length > 0, |
| function(item) { |
| var installWarningList = item.querySelector('ul'); |
| installWarningList.textContent = ''; |
| if (extension.installWarnings) { |
| extension.installWarnings.forEach(function(warning) { |
| var li = document.createElement('li'); |
| li.innerText = warning; |
| installWarningList.appendChild(li); |
| }); |
| } |
| }); |
| |
| if (location.hash.substr(1) == extension.id) { |
| // Scroll beneath the fixed header so that the extension is not |
| // obscured. |
| var topScroll = wrapper.offsetTop - $('page-header').offsetHeight; |
| var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10); |
| if (!isNaN(pad)) |
| topScroll -= pad / 2; |
| setScrollTopForDocument(document, topScroll); |
| } |
| }, |
| |
| /** |
| * Updates an element's textContent. |
| * @param {Node} node Ancestor of the element specified by |query|. |
| * @param {string} query A query to select an element in |node|. |
| * @param {string} textContent |
| * @private |
| */ |
| setText_: function(node, query, textContent) { |
| node.querySelector(query).textContent = textContent; |
| }, |
| |
| /** |
| * Updates an element's visibility and calls |shownCallback| if it is |
| * visible. |
| * @param {Node} node Ancestor of the element specified by |query|. |
| * @param {string} query A query to select an element in |node|. |
| * @param {boolean} visible Whether the element should be visible or not. |
| * @param {function(Element)=} opt_shownCallback Callback if the element is |
| * visible. The element passed in will be the element specified by |
| * |query|. |
| * @private |
| */ |
| updateVisibility_: function(node, query, visible, opt_shownCallback) { |
| var element = assertInstanceof(node.querySelector(query), Element); |
| element.hidden = !visible; |
| if (visible && opt_shownCallback) |
| opt_shownCallback(element); |
| }, |
| |
| /** |
| * Opens the extension options overlay for the extension with the given id. |
| * @param {string} extensionId The id of extension whose options page should |
| * be displayed. |
| * @param {boolean} scroll Whether the page should scroll to the extension |
| * @private |
| */ |
| showEmbeddedExtensionOptions_: function(extensionId, scroll) { |
| if (this.optionsShown_) |
| return; |
| |
| // Get the extension from the given id. |
| var extension = this.extensions_.filter(function(extension) { |
| return extension.state == |
| chrome.developerPrivate.ExtensionState.ENABLED && |
| extension.id == extensionId; |
| })[0]; |
| |
| if (!extension) |
| return; |
| |
| if (scroll) |
| this.scrollToWrapper_(extensionId); |
| |
| // Add the options query string. Corner case: the 'options' query string |
| // will clobber the 'id' query string if the options link is clicked when |
| // 'id' is in the URL, or if both query strings are in the URL. |
| window.history.replaceState({}, '', '/?options=' + extensionId); |
| |
| var overlay = extensions.ExtensionOptionsOverlay.getInstance(); |
| var shownCallback = function() { |
| // This overlay doesn't get focused automatically as <extensionoptions> |
| // is created after the overlay is shown. |
| if (cr.ui.FocusOutlineManager.forDocument(document).visible) |
| overlay.setInitialFocus(); |
| }; |
| overlay.setExtensionAndShow(extensionId, extension.name, |
| extension.iconUrl, shownCallback); |
| this.optionsShown_ = true; |
| |
| var self = this; |
| $('overlay').addEventListener('cancelOverlay', function f() { |
| self.optionsShown_ = false; |
| $('overlay').removeEventListener('cancelOverlay', f); |
| |
| // Remove the options query string. |
| window.history.replaceState({}, '', '/'); |
| }); |
| |
| // TODO(dbeam): why do we need to focus <extensionoptions> before and |
| // after its showing animation? Makes very little sense to me. |
| overlay.setInitialFocus(); |
| }, |
| |
| /** |
| * Hides the extension options overlay for the extension with id |
| * |extensionId|. If there is an overlay showing for a different extension, |
| * nothing happens. |
| * @param {string} extensionId ID of the extension to hide. |
| * @private |
| */ |
| hideEmbeddedExtensionOptions_: function(extensionId) { |
| if (!this.optionsShown_) |
| return; |
| |
| var overlay = extensions.ExtensionOptionsOverlay.getInstance(); |
| if (overlay.getExtensionId() == extensionId) |
| overlay.close(); |
| }, |
| |
| /** |
| * Updates or creates a wrapper for |extension|. |
| * @param {!chrome.developerPrivate.ExtensionInfo} extension The information |
| * about the extension to update. |
| * @private |
| */ |
| updateOrCreateWrapper_: function(extension) { |
| var currIndex = this.getIndexOfExtension_(extension.id); |
| if (currIndex != -1) { |
| // If there is a current version of the extension, update it with the |
| // new version. |
| this.extensions_[currIndex] = extension; |
| } else { |
| // If the extension isn't found, push it back and sort. Technically, we |
| // could optimize by inserting it at the right location, but since this |
| // only happens on extension install, it's not worth it. |
| this.extensions_.push(extension); |
| this.extensions_.sort(compareExtensions); |
| } |
| |
| var wrapper = $(extension.id); |
| if (wrapper) { |
| this.updateWrapper_(extension, wrapper); |
| } else { |
| var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1]; |
| this.createWrapper_(extension, nextExt ? $(nextExt.id) : null); |
| } |
| } |
| }; |
| |
| return { |
| ExtensionList: ExtensionList, |
| ExtensionListDelegate: ExtensionListDelegate |
| }; |
| }); |