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">
8 * The type of the extension data object. The definition is based on
9 * chrome/browser/ui/webui/extensions/extension_basic_info.cc
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,
21 * enableExtensionInfoDialog: boolean,
22 * enable_show_button: boolean,
24 * enabledIncognito: boolean,
25 * errorCollectionEnabled: (boolean|undefined),
26 * hasPopupAction: boolean,
27 * homepageProvided: boolean,
28 * homepageUrl: string,
31 * incognitoCanBeEnabled: boolean,
32 * installWarnings: (Array|undefined),
33 * is_hosted_app: boolean,
34 * is_platform_app: boolean,
35 * isFromStore: boolean,
36 * isUnpacked: boolean,
37 * kioskEnabled: boolean,
39 * locationText: string,
40 * managedInstall: boolean,
41 * manifestErrors: (Array.<RuntimeError>|undefined),
43 * offlineEnabled: boolean,
44 * optionsOpenInTab: boolean,
45 * optionsPageHref: string,
48 * packagedApp: boolean,
49 * path: (string|undefined),
50 * prettifiedPath: (string|undefined),
51 * recommendedInstall: boolean,
52 * runtimeErrors: (Array.<RuntimeError>|undefined),
53 * suspiciousInstall: boolean,
54 * terminated: boolean,
56 * views: Array.<{renderViewId: number, renderProcessId: number,
57 * path: string, incognito: boolean,
58 * generatedBackgroundPage: boolean}>,
59 * wantsAllUrls: boolean,
60 * wantsErrorCollection: boolean,
61 * wantsFileAccess: boolean,
62 * warnings: (Array|undefined)}}
66 cr.define('options', function() {
70 * Creates a new list of extensions.
71 * @param {Object=} opt_propertyBag Optional properties.
73 * @extends {HTMLDivElement}
75 var ExtensionsList = cr.ui.define('div');
78 * @type {Object.<string, boolean>} A map from extension id to a boolean
79 * indicating whether the incognito warning is showing. This persists
80 * between calls to decorate.
82 var butterBarVisibility = {};
85 * @type {Object.<string, number>} A map from extension id to last reloaded
86 * timestamp. The timestamp is recorded when the user click the 'Reload'
87 * link. It is used to refresh the icon of an unpacked extension.
88 * This persists between calls to decorate.
90 var extensionReloadedTimestamp = {};
92 ExtensionsList.prototype = {
93 __proto__: HTMLDivElement.prototype,
96 * Indicates whether an embedded options page that was navigated to through
97 * the '?options=' URL query has been shown to the user. This is necessary
98 * to prevent showExtensionNodes_ from opening the options more than once.
102 optionsShown_: false,
105 decorate: function() {
106 this.textContent = '';
108 this.showExtensionNodes_();
111 getIdQueryParam_: function() {
112 return parseQueryParams(document.location)['id'];
115 getOptionsQueryParam_: function() {
116 return parseQueryParams(document.location)['options'];
120 * Creates all extension items from scratch.
123 showExtensionNodes_: function() {
124 // Iterate over the extension data and add each item to the list.
125 this.data_.extensions.forEach(this.createNode_, this);
127 var idToHighlight = this.getIdQueryParam_();
128 if (idToHighlight && $(idToHighlight))
129 this.scrollToNode_(idToHighlight);
131 var idToOpenOptions = this.getOptionsQueryParam_();
132 if (idToOpenOptions && $(idToOpenOptions))
133 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
135 if (this.data_.extensions.length == 0)
136 this.classList.add('empty-extension-list');
138 this.classList.remove('empty-extension-list');
142 * Scrolls the page down to the extension node with the given id.
143 * @param {string} extensionId The id of the extension to scroll to.
146 scrollToNode_: function(extensionId) {
147 // Scroll offset should be calculated slightly higher than the actual
148 // offset of the element being scrolled to, so that it ends up not all
149 // the way at the top. That way it is clear that there are more elements
150 // above the element being scrolled to.
151 var scrollFudge = 1.2;
152 var scrollTop = $(extensionId).offsetTop - scrollFudge *
153 $(extensionId).clientHeight;
154 setScrollTopForDocument(document, scrollTop);
158 * Synthesizes and initializes an HTML element for the extension metadata
159 * given in |extension|.
160 * @param {ExtensionData} extension A dictionary of extension metadata.
163 createNode_: function(extension) {
164 var template = $('template-collection').querySelector(
165 '.extension-list-item-wrapper');
166 var node = template.cloneNode(true);
167 node.id = extension.id;
169 if (!extension.enabled || extension.terminated)
170 node.classList.add('inactive-extension');
172 if (extension.managedInstall ||
173 extension.dependentExtensions.length > 0) {
174 node.classList.add('may-not-modify');
175 node.classList.add('may-not-remove');
176 } else if (extension.recommendedInstall) {
177 node.classList.add('may-not-remove');
178 } else if (extension.suspiciousInstall || extension.corruptInstall) {
179 node.classList.add('may-not-modify');
182 var idToHighlight = this.getIdQueryParam_();
183 if (node.id == idToHighlight)
184 node.classList.add('extension-highlight');
186 var item = node.querySelector('.extension-list-item');
187 // Prevent the image cache of extension icon by using the reloaded
188 // timestamp as a query string. The timestamp is recorded when the user
189 // clicks the 'Reload' link. http://crbug.com/159302.
190 if (extensionReloadedTimestamp[extension.id]) {
191 item.style.backgroundImage =
192 'url(' + extension.icon + '?' +
193 extensionReloadedTimestamp[extension.id] + ')';
195 item.style.backgroundImage = 'url(' + extension.icon + ')';
198 var title = node.querySelector('.extension-title');
199 title.textContent = extension.name;
201 var version = node.querySelector('.extension-version');
202 version.textContent = extension.version;
204 var locationText = node.querySelector('.location-text');
205 locationText.textContent = extension.locationText;
207 var blacklistText = node.querySelector('.blacklist-text');
208 blacklistText.textContent = extension.blacklistText;
210 var description = document.createElement('span');
211 description.textContent = extension.description;
212 node.querySelector('.extension-description').appendChild(description);
214 // The 'Show Browser Action' button.
215 if (extension.enable_show_button) {
216 var showButton = node.querySelector('.show-button');
217 showButton.addEventListener('click', function(e) {
218 chrome.send('extensionSettingsShowButton', [extension.id]);
220 showButton.hidden = false;
223 // The 'allow in incognito' checkbox.
224 node.querySelector('.incognito-control').hidden =
225 !this.data_.incognitoAvailable;
226 var incognito = node.querySelector('.incognito-control input');
227 incognito.disabled = !extension.incognitoCanBeEnabled;
228 incognito.checked = extension.enabledIncognito;
229 if (!incognito.disabled) {
230 incognito.addEventListener('change', function(e) {
231 var checked = e.target.checked;
232 butterBarVisibility[extension.id] = checked;
233 butterBar.hidden = !checked || extension.is_hosted_app;
234 chrome.send('extensionSettingsEnableIncognito',
235 [extension.id, String(checked)]);
238 var butterBar = node.querySelector('.butter-bar');
239 butterBar.hidden = !butterBarVisibility[extension.id];
241 // The 'collect errors' checkbox. This should only be visible if the
242 // error console is enabled - we can detect this by the existence of the
243 // |errorCollectionEnabled| property.
244 if (extension.wantsErrorCollection) {
245 node.querySelector('.error-collection-control').hidden = false;
246 var errorCollection =
247 node.querySelector('.error-collection-control input');
248 errorCollection.checked = extension.errorCollectionEnabled;
249 errorCollection.addEventListener('change', function(e) {
250 chrome.send('extensionSettingsEnableErrorCollection',
251 [extension.id, String(e.target.checked)]);
255 // The 'allow on all urls' checkbox. This should only be visible if
256 // active script restrictions are enabled. If they are not enabled, no
257 // extensions should want all urls.
258 if (extension.wantsAllUrls) {
259 var allUrls = node.querySelector('.all-urls-control');
260 allUrls.addEventListener('click', function(e) {
261 chrome.send('extensionSettingsAllowOnAllUrls',
262 [extension.id, String(e.target.checked)]);
264 allUrls.querySelector('input').checked = extension.allowAllUrls;
265 allUrls.hidden = false;
268 // The 'allow file:// access' checkbox.
269 if (extension.wantsFileAccess) {
270 var fileAccess = node.querySelector('.file-access-control');
271 fileAccess.addEventListener('click', function(e) {
272 chrome.send('extensionSettingsAllowFileAccess',
273 [extension.id, String(e.target.checked)]);
275 fileAccess.querySelector('input').checked = extension.allowFileAccess;
276 fileAccess.hidden = false;
279 // The 'Options' button or link, depending on its behaviour.
280 if (extension.enabled && extension.optionsUrl) {
281 var options, optionsClickListener;
282 if (extension.optionsOpenInTab) {
283 options = node.querySelector('.options-link');
284 // Set an href to get the correct mouse-over appearance (link,
285 // footer) - but the actual link opening is done through chrome.send
286 // with a preventDefault().
287 options.setAttribute('href', extension.optionsPageHref);
288 optionsClickListener = function() {
289 chrome.send('extensionSettingsOptions', [extension.id]);
292 options = node.querySelector('.options-button');
293 optionsClickListener = function() {
294 this.showEmbeddedExtensionOptions_(extension.id, false);
297 options.addEventListener('click', function(e) {
298 optionsClickListener();
301 options.hidden = false;
304 // The 'Permissions' link.
305 var permissions = node.querySelector('.permissions-link');
306 permissions.addEventListener('click', function(e) {
307 chrome.send('extensionSettingsPermissions', [extension.id]);
311 // The 'View in Web Store/View Web Site' link.
312 if (extension.homepageUrl && !extension.enableExtensionInfoDialog) {
313 var siteLink = node.querySelector('.site-link');
314 siteLink.href = extension.homepageUrl;
315 siteLink.textContent = loadTimeData.getString(
316 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
317 'extensionSettingsVisitWebStore');
318 siteLink.hidden = false;
321 if (extension.allow_reload) {
322 // The 'Reload' link.
323 var reload = node.querySelector('.reload-link');
324 reload.addEventListener('click', function(e) {
325 chrome.send('extensionSettingsReload', [extension.id]);
326 extensionReloadedTimestamp[extension.id] = Date.now();
328 reload.hidden = false;
330 if (extension.is_platform_app) {
331 // The 'Launch' link.
332 var launch = node.querySelector('.launch-link');
333 launch.addEventListener('click', function(e) {
334 chrome.send('extensionSettingsLaunch', [extension.id]);
336 launch.hidden = false;
340 if (extension.terminated) {
341 var terminatedReload = node.querySelector('.terminated-reload-link');
342 terminatedReload.hidden = false;
343 terminatedReload.onclick = function() {
344 chrome.send('extensionSettingsReload', [extension.id]);
346 } else if (extension.corruptInstall && extension.isFromStore) {
347 var repair = node.querySelector('.corrupted-repair-button');
348 repair.hidden = false;
349 repair.onclick = function() {
350 chrome.send('extensionSettingsRepair', [extension.id]);
353 // The 'Enabled' checkbox.
354 var enable = node.querySelector('.enable-checkbox');
355 enable.hidden = false;
356 var enableCheckboxDisabled = extension.managedInstall ||
357 extension.suspiciousInstall ||
358 extension.corruptInstall ||
359 extension.dependentExtensions.length > 0;
360 enable.querySelector('input').disabled = enableCheckboxDisabled;
362 if (!enableCheckboxDisabled) {
363 enable.addEventListener('click', function(e) {
364 // When e.target is the label instead of the checkbox, it doesn't
365 // have the checked property and the state of the checkbox is
367 var checked = e.target.checked;
368 if (checked == undefined)
369 checked = !e.currentTarget.querySelector('input').checked;
370 chrome.send('extensionSettingsEnable',
371 [extension.id, checked ? 'true' : 'false']);
373 // This may seem counter-intuitive (to not set/clear the checkmark)
374 // but this page will be updated asynchronously if the extension
375 // becomes enabled/disabled. It also might not become enabled or
376 // disabled, because the user might e.g. get prompted when enabling
377 // and choose not to.
382 enable.querySelector('input').checked = extension.enabled;
386 var trashTemplate = $('template-collection').querySelector('.trash');
387 var trash = trashTemplate.cloneNode(true);
388 trash.title = loadTimeData.getString('extensionUninstall');
389 trash.addEventListener('click', function(e) {
390 butterBarVisibility[extension.id] = false;
391 chrome.send('extensionSettingsUninstall', [extension.id]);
393 node.querySelector('.enable-controls').appendChild(trash);
395 // Developer mode ////////////////////////////////////////////////////////
397 // First we have the id.
398 var idLabel = node.querySelector('.extension-id');
399 idLabel.textContent = ' ' + extension.id;
401 // Then the path, if provided by unpacked extension.
402 if (extension.isUnpacked) {
403 var loadPath = node.querySelector('.load-path');
404 loadPath.hidden = false;
405 var pathLink = loadPath.querySelector('a:nth-of-type(1)');
406 pathLink.textContent = ' ' + extension.prettifiedPath;
407 pathLink.addEventListener('click', function(e) {
408 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
413 // Then the 'managed, cannot uninstall/disable' message.
414 if (extension.managedInstall || extension.recommendedInstall) {
415 node.querySelector('.managed-message').hidden = false;
417 if (extension.suspiciousInstall) {
418 // Then the 'This isn't from the webstore, looks suspicious' message.
419 node.querySelector('.suspicious-install-message').hidden = false;
421 if (extension.corruptInstall) {
422 // Then the 'This is a corrupt extension' message.
423 node.querySelector('.corrupt-install-message').hidden = false;
427 if (extension.dependentExtensions.length > 0) {
428 var dependentMessage =
429 node.querySelector('.dependent-extensions-message');
430 dependentMessage.hidden = false;
431 var dependentList = dependentMessage.querySelector('ul');
432 var dependentTemplate = $('template-collection').querySelector(
433 '.dependent-list-item');
434 extension.dependentExtensions.forEach(function(elem) {
435 var depNode = dependentTemplate.cloneNode(true);
436 depNode.querySelector('.dep-extension-title').textContent = elem.name;
437 depNode.querySelector('.dep-extension-id').textContent = elem.id;
438 dependentList.appendChild(depNode);
442 // Then active views.
443 if (extension.views.length > 0) {
444 var activeViews = node.querySelector('.active-views');
445 activeViews.hidden = false;
446 var link = activeViews.querySelector('a');
448 extension.views.forEach(function(view, i) {
449 var displayName = view.generatedBackgroundPage ?
450 loadTimeData.getString('backgroundPage') : view.path;
451 var label = displayName +
453 ' ' + loadTimeData.getString('viewIncognito') : '') +
454 (view.renderProcessId == -1 ?
455 ' ' + loadTimeData.getString('viewInactive') : '');
456 link.textContent = label;
457 link.addEventListener('click', function(e) {
458 // TODO(estade): remove conversion to string?
459 chrome.send('extensionSettingsInspect', [
460 String(extension.id),
461 String(view.renderProcessId),
462 String(view.renderViewId),
467 if (i < extension.views.length - 1) {
468 link = link.cloneNode(true);
469 activeViews.appendChild(link);
474 // The extension warnings (describing runtime issues).
475 if (extension.warnings) {
476 var panel = node.querySelector('.extension-warnings');
477 panel.hidden = false;
478 var list = panel.querySelector('ul');
479 extension.warnings.forEach(function(warning) {
480 list.appendChild(document.createElement('li')).innerText = warning;
484 // If the ErrorConsole is enabled, we should have manifest and/or runtime
485 // errors. Otherwise, we may have install warnings. We should not have
486 // both ErrorConsole errors and install warnings.
487 if (extension.manifestErrors) {
488 var panel = node.querySelector('.manifest-errors');
489 panel.hidden = false;
490 panel.appendChild(new extensions.ExtensionErrorList(
491 extension.manifestErrors));
493 if (extension.runtimeErrors) {
494 var panel = node.querySelector('.runtime-errors');
495 panel.hidden = false;
496 panel.appendChild(new extensions.ExtensionErrorList(
497 extension.runtimeErrors));
499 if (extension.installWarnings) {
500 var panel = node.querySelector('.install-warnings');
501 panel.hidden = false;
502 var list = panel.querySelector('ul');
503 extension.installWarnings.forEach(function(warning) {
504 var li = document.createElement('li');
505 li.innerText = warning.message;
506 list.appendChild(li);
510 this.appendChild(node);
511 if (location.hash.substr(1) == extension.id) {
512 // Scroll beneath the fixed header so that the extension is not
514 var topScroll = node.offsetTop - $('page-header').offsetHeight;
515 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
517 topScroll -= pad / 2;
518 setScrollTopForDocument(document, topScroll);
523 * Opens the extension options overlay for the extension with the given id.
524 * @param {string} extensionId The id of extension whose options page should
526 * @param {boolean} scroll Whether the page should scroll to the extension
529 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
530 if (this.optionsShown_)
533 // Get the extension from the given id.
534 var extension = this.data_.extensions.filter(function(extension) {
535 return extension.enabled && extension.id == extensionId;
542 this.scrollToNode_(extensionId);
543 // Add the options query string. Corner case: the 'options' query string
544 // will clobber the 'id' query string if the options link is clicked when
545 // 'id' is in the URL, or if both query strings are in the URL.
546 uber.replaceState({}, '?options=' + extensionId);
548 extensions.ExtensionOptionsOverlay.getInstance().
549 setExtensionAndShowOverlay(extensionId,
553 this.optionsShown_ = true;
554 $('overlay').addEventListener('cancelOverlay', function() {
555 this.optionsShown_ = false;
561 ExtensionsList: ExtensionsList