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 * enable_show_button: boolean,
23 * enabledIncognito: boolean,
24 * errorCollectionEnabled: (boolean|undefined),
25 * hasPopupAction: boolean,
26 * homepageProvided: boolean,
27 * homepageUrl: string,
30 * incognitoCanBeEnabled: boolean,
31 * installWarnings: (Array|undefined),
32 * is_hosted_app: boolean,
33 * is_platform_app: boolean,
34 * isFromStore: boolean,
35 * isUnpacked: boolean,
36 * kioskEnabled: boolean,
38 * locationText: string,
39 * managedInstall: boolean,
40 * manifestErrors: (Array.<RuntimeError>|undefined),
42 * offlineEnabled: boolean,
45 * packagedApp: boolean,
46 * path: (string|undefined),
47 * prettifiedPath: (string|undefined),
48 * runtimeErrors: (Array.<RuntimeError>|undefined),
49 * suspiciousInstall: boolean,
50 * terminated: boolean,
52 * views: Array.<{renderViewId: number, renderProcessId: number,
53 * path: string, incognito: boolean,
54 * generatedBackgroundPage: boolean}>,
55 * wantsAllUrls: boolean,
56 * wantsErrorCollection: boolean,
57 * wantsFileAccess: boolean,
58 * warnings: (Array|undefined)}}
62 cr.define('options', function() {
66 * Creates a new list of extensions.
67 * @param {Object=} opt_propertyBag Optional properties.
69 * @extends {HTMLDivElement}
71 var ExtensionsList = cr.ui.define('div');
74 * @type {Object.<string, boolean>} A map from extension id to a boolean
75 * indicating whether the incognito warning is showing. This persists
76 * between calls to decorate.
78 var butterBarVisibility = {};
81 * @type {Object.<string, number>} A map from extension id to last reloaded
82 * timestamp. The timestamp is recorded when the user click the 'Reload'
83 * link. It is used to refresh the icon of an unpacked extension.
84 * This persists between calls to decorate.
86 var extensionReloadedTimestamp = {};
88 ExtensionsList.prototype = {
89 __proto__: HTMLDivElement.prototype,
92 * Indicates whether an embedded options page that was navigated to through
93 * the '?options=' URL query has been shown to the user. This is necessary
94 * to prevent showExtensionNodes_ from opening the options more than once.
101 decorate: function() {
102 this.textContent = '';
104 this.showExtensionNodes_();
107 getIdQueryParam_: function() {
108 return parseQueryParams(document.location)['id'];
111 getOptionsQueryParam_: function() {
112 return parseQueryParams(document.location)['options'];
116 * Creates all extension items from scratch.
119 showExtensionNodes_: function() {
120 // Iterate over the extension data and add each item to the list.
121 this.data_.extensions.forEach(this.createNode_, this);
123 var idToHighlight = this.getIdQueryParam_();
124 if (idToHighlight && $(idToHighlight))
125 this.scrollToNode_(idToHighlight);
127 var idToOpenOptions = this.getOptionsQueryParam_();
128 if (idToOpenOptions && $(idToOpenOptions))
129 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
131 if (this.data_.extensions.length == 0)
132 this.classList.add('empty-extension-list');
134 this.classList.remove('empty-extension-list');
138 * Scrolls the page down to the extension node with the given id.
139 * @param {string} extensionId The id of the extension to scroll to.
142 scrollToNode_: function(extensionId) {
143 // Scroll offset should be calculated slightly higher than the actual
144 // offset of the element being scrolled to, so that it ends up not all
145 // the way at the top. That way it is clear that there are more elements
146 // above the element being scrolled to.
147 var scrollFudge = 1.2;
148 var scrollTop = $(extensionId).offsetTop - scrollFudge *
149 $(extensionId).clientHeight;
150 setScrollTopForDocument(document, scrollTop);
154 * Synthesizes and initializes an HTML element for the extension metadata
155 * given in |extension|.
156 * @param {ExtensionData} extension A dictionary of extension metadata.
159 createNode_: function(extension) {
160 var template = $('template-collection').querySelector(
161 '.extension-list-item-wrapper');
162 var node = template.cloneNode(true);
163 node.id = extension.id;
165 if (!extension.enabled || extension.terminated)
166 node.classList.add('inactive-extension');
168 if (extension.managedInstall ||
169 extension.dependentExtensions.length > 0) {
170 node.classList.add('may-not-modify');
171 node.classList.add('may-not-remove');
172 } else if (extension.suspiciousInstall || extension.corruptInstall) {
173 node.classList.add('may-not-modify');
176 var idToHighlight = this.getIdQueryParam_();
177 if (node.id == idToHighlight)
178 node.classList.add('extension-highlight');
180 var item = node.querySelector('.extension-list-item');
181 // Prevent the image cache of extension icon by using the reloaded
182 // timestamp as a query string. The timestamp is recorded when the user
183 // clicks the 'Reload' link. http://crbug.com/159302.
184 if (extensionReloadedTimestamp[extension.id]) {
185 item.style.backgroundImage =
186 'url(' + extension.icon + '?' +
187 extensionReloadedTimestamp[extension.id] + ')';
189 item.style.backgroundImage = 'url(' + extension.icon + ')';
192 var title = node.querySelector('.extension-title');
193 title.textContent = extension.name;
195 var version = node.querySelector('.extension-version');
196 version.textContent = extension.version;
198 var locationText = node.querySelector('.location-text');
199 locationText.textContent = extension.locationText;
201 var blacklistText = node.querySelector('.blacklist-text');
202 blacklistText.textContent = extension.blacklistText;
204 var description = document.createElement('span');
205 description.textContent = extension.description;
206 node.querySelector('.extension-description').appendChild(description);
208 // The 'Show Browser Action' button.
209 if (extension.enable_show_button) {
210 var showButton = node.querySelector('.show-button');
211 showButton.addEventListener('click', function(e) {
212 chrome.send('extensionSettingsShowButton', [extension.id]);
214 showButton.hidden = false;
217 // The 'allow in incognito' checkbox.
218 node.querySelector('.incognito-control').hidden =
219 !this.data_.incognitoAvailable;
220 var incognito = node.querySelector('.incognito-control input');
221 incognito.disabled = !extension.incognitoCanBeEnabled;
222 incognito.checked = extension.enabledIncognito;
223 if (!incognito.disabled) {
224 incognito.addEventListener('change', function(e) {
225 var checked = e.target.checked;
226 butterBarVisibility[extension.id] = checked;
227 butterBar.hidden = !checked || extension.is_hosted_app;
228 chrome.send('extensionSettingsEnableIncognito',
229 [extension.id, String(checked)]);
232 var butterBar = node.querySelector('.butter-bar');
233 butterBar.hidden = !butterBarVisibility[extension.id];
235 // The 'collect errors' checkbox. This should only be visible if the
236 // error console is enabled - we can detect this by the existence of the
237 // |errorCollectionEnabled| property.
238 if (extension.wantsErrorCollection) {
239 node.querySelector('.error-collection-control').hidden = false;
240 var errorCollection =
241 node.querySelector('.error-collection-control input');
242 errorCollection.checked = extension.errorCollectionEnabled;
243 errorCollection.addEventListener('change', function(e) {
244 chrome.send('extensionSettingsEnableErrorCollection',
245 [extension.id, String(e.target.checked)]);
249 // The 'allow on all urls' checkbox. This should only be visible if
250 // active script restrictions are enabled. If they are not enabled, no
251 // extensions should want all urls.
252 if (extension.wantsAllUrls) {
253 var allUrls = node.querySelector('.all-urls-control');
254 allUrls.addEventListener('click', function(e) {
255 chrome.send('extensionSettingsAllowOnAllUrls',
256 [extension.id, String(e.target.checked)]);
258 allUrls.querySelector('input').checked = extension.allowAllUrls;
259 allUrls.hidden = false;
262 // The 'allow file:// access' checkbox.
263 if (extension.wantsFileAccess) {
264 var fileAccess = node.querySelector('.file-access-control');
265 fileAccess.addEventListener('click', function(e) {
266 chrome.send('extensionSettingsAllowFileAccess',
267 [extension.id, String(e.target.checked)]);
269 fileAccess.querySelector('input').checked = extension.allowFileAccess;
270 fileAccess.hidden = false;
273 // The 'Options' link.
274 if (extension.enabled && extension.optionsUrl) {
275 var options = node.querySelector('.options-link');
276 options.addEventListener('click', function(e) {
277 if (!extension.optionsOpenInTab) {
278 this.showEmbeddedExtensionOptions_(extension.id, false);
280 chrome.send('extensionSettingsOptions', [extension.id]);
284 options.hidden = false;
287 // The 'Permissions' link.
288 var permissions = node.querySelector('.permissions-link');
289 permissions.addEventListener('click', function(e) {
290 chrome.send('extensionSettingsPermissions', [extension.id]);
294 // The 'View in Web Store/View Web Site' link.
295 if (extension.homepageUrl) {
296 var siteLink = node.querySelector('.site-link');
297 siteLink.href = extension.homepageUrl;
298 siteLink.textContent = loadTimeData.getString(
299 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
300 'extensionSettingsVisitWebStore');
301 siteLink.hidden = false;
304 if (extension.allow_reload) {
305 // The 'Reload' link.
306 var reload = node.querySelector('.reload-link');
307 reload.addEventListener('click', function(e) {
308 chrome.send('extensionSettingsReload', [extension.id]);
309 extensionReloadedTimestamp[extension.id] = Date.now();
311 reload.hidden = false;
313 if (extension.is_platform_app) {
314 // The 'Launch' link.
315 var launch = node.querySelector('.launch-link');
316 launch.addEventListener('click', function(e) {
317 chrome.send('extensionSettingsLaunch', [extension.id]);
319 launch.hidden = false;
323 if (extension.terminated) {
324 var terminatedReload = node.querySelector('.terminated-reload-link');
325 terminatedReload.hidden = false;
326 terminatedReload.onclick = function() {
327 chrome.send('extensionSettingsReload', [extension.id]);
329 } else if (extension.corruptInstall && extension.isFromStore) {
330 var repair = node.querySelector('.corrupted-repair-button');
331 repair.hidden = false;
332 repair.onclick = function() {
333 chrome.send('extensionSettingsRepair', [extension.id]);
336 // The 'Enabled' checkbox.
337 var enable = node.querySelector('.enable-checkbox');
338 enable.hidden = false;
339 var enableCheckboxDisabled = extension.managedInstall ||
340 extension.suspiciousInstall ||
341 extension.corruptInstall ||
342 extension.dependentExtensions.length > 0;
343 enable.querySelector('input').disabled = enableCheckboxDisabled;
345 if (!enableCheckboxDisabled) {
346 enable.addEventListener('click', function(e) {
347 // When e.target is the label instead of the checkbox, it doesn't
348 // have the checked property and the state of the checkbox is
350 var checked = e.target.checked;
351 if (checked == undefined)
352 checked = !e.currentTarget.querySelector('input').checked;
353 chrome.send('extensionSettingsEnable',
354 [extension.id, checked ? 'true' : 'false']);
356 // This may seem counter-intuitive (to not set/clear the checkmark)
357 // but this page will be updated asynchronously if the extension
358 // becomes enabled/disabled. It also might not become enabled or
359 // disabled, because the user might e.g. get prompted when enabling
360 // and choose not to.
365 enable.querySelector('input').checked = extension.enabled;
369 var trashTemplate = $('template-collection').querySelector('.trash');
370 var trash = trashTemplate.cloneNode(true);
371 trash.title = loadTimeData.getString('extensionUninstall');
372 trash.addEventListener('click', function(e) {
373 butterBarVisibility[extension.id] = false;
374 chrome.send('extensionSettingsUninstall', [extension.id]);
376 node.querySelector('.enable-controls').appendChild(trash);
378 // Developer mode ////////////////////////////////////////////////////////
380 // First we have the id.
381 var idLabel = node.querySelector('.extension-id');
382 idLabel.textContent = ' ' + extension.id;
384 // Then the path, if provided by unpacked extension.
385 if (extension.isUnpacked) {
386 var loadPath = node.querySelector('.load-path');
387 loadPath.hidden = false;
388 var pathLink = loadPath.querySelector('a:nth-of-type(1)');
389 pathLink.textContent = ' ' + extension.prettifiedPath;
390 pathLink.addEventListener('click', function(e) {
391 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
396 // Then the 'managed, cannot uninstall/disable' message.
397 if (extension.managedInstall) {
398 node.querySelector('.managed-message').hidden = false;
400 if (extension.suspiciousInstall) {
401 // Then the 'This isn't from the webstore, looks suspicious' message.
402 node.querySelector('.suspicious-install-message').hidden = false;
404 if (extension.corruptInstall) {
405 // Then the 'This is a corrupt extension' message.
406 node.querySelector('.corrupt-install-message').hidden = false;
410 if (extension.dependentExtensions.length > 0) {
411 var dependentMessage =
412 node.querySelector('.dependent-extensions-message');
413 dependentMessage.hidden = false;
414 var dependentList = dependentMessage.querySelector('ul');
415 var dependentTemplate = $('template-collection').querySelector(
416 '.dependent-list-item');
417 extension.dependentExtensions.forEach(function(elem) {
418 var depNode = dependentTemplate.cloneNode(true);
419 depNode.querySelector('.dep-extension-title').textContent = elem.name;
420 depNode.querySelector('.dep-extension-id').textContent = elem.id;
421 dependentList.appendChild(depNode);
425 // Then active views.
426 if (extension.views.length > 0) {
427 var activeViews = node.querySelector('.active-views');
428 activeViews.hidden = false;
429 var link = activeViews.querySelector('a');
431 extension.views.forEach(function(view, i) {
432 var displayName = view.generatedBackgroundPage ?
433 loadTimeData.getString('backgroundPage') : view.path;
434 var label = displayName +
436 ' ' + loadTimeData.getString('viewIncognito') : '') +
437 (view.renderProcessId == -1 ?
438 ' ' + loadTimeData.getString('viewInactive') : '');
439 link.textContent = label;
440 link.addEventListener('click', function(e) {
441 // TODO(estade): remove conversion to string?
442 chrome.send('extensionSettingsInspect', [
443 String(extension.id),
444 String(view.renderProcessId),
445 String(view.renderViewId),
450 if (i < extension.views.length - 1) {
451 link = link.cloneNode(true);
452 activeViews.appendChild(link);
457 // The extension warnings (describing runtime issues).
458 if (extension.warnings) {
459 var panel = node.querySelector('.extension-warnings');
460 panel.hidden = false;
461 var list = panel.querySelector('ul');
462 extension.warnings.forEach(function(warning) {
463 list.appendChild(document.createElement('li')).innerText = warning;
467 // If the ErrorConsole is enabled, we should have manifest and/or runtime
468 // errors. Otherwise, we may have install warnings. We should not have
469 // both ErrorConsole errors and install warnings.
470 if (extension.manifestErrors) {
471 var panel = node.querySelector('.manifest-errors');
472 panel.hidden = false;
473 panel.appendChild(new extensions.ExtensionErrorList(
474 extension.manifestErrors));
476 if (extension.runtimeErrors) {
477 var panel = node.querySelector('.runtime-errors');
478 panel.hidden = false;
479 panel.appendChild(new extensions.ExtensionErrorList(
480 extension.runtimeErrors));
482 if (extension.installWarnings) {
483 var panel = node.querySelector('.install-warnings');
484 panel.hidden = false;
485 var list = panel.querySelector('ul');
486 extension.installWarnings.forEach(function(warning) {
487 var li = document.createElement('li');
488 li.innerText = warning.message;
489 list.appendChild(li);
493 this.appendChild(node);
494 if (location.hash.substr(1) == extension.id) {
495 // Scroll beneath the fixed header so that the extension is not
497 var topScroll = node.offsetTop - $('page-header').offsetHeight;
498 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
500 topScroll -= pad / 2;
501 setScrollTopForDocument(document, topScroll);
506 * Opens the extension options overlay for the extension with the given id.
507 * @param {string} extensionId The id of extension whose options page should
509 * @param {boolean} scroll Whether the page should scroll to the extension
512 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
513 if (this.optionsShown_)
516 // Get the extension from the given id.
517 var extension = this.data_.extensions.filter(function(extension) {
518 return extension.id == extensionId;
525 this.scrollToNode_(extensionId);
526 // Add the options query string. Corner case: the 'options' query string
527 // will clobber the 'id' query string if the options link is clicked when
528 // 'id' is in the URL, or if both query strings are in the URL.
529 uber.replaceState({}, '?options=' + extensionId);
531 extensions.ExtensionOptionsOverlay.getInstance().
532 setExtensionAndShowOverlay(extensionId,
536 this.optionsShown_ = true;
537 $('overlay').addEventListener('cancelOverlay', function() {
538 this.optionsShown_ = false;
544 ExtensionsList: ExtensionsList