Move Webstore URL concepts to //extensions and out
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blob12a0d4b73339a6f57a5b44e83d1e45f5b2f00026
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src="extension_error.js">
7 /**
8 * The type of the extension data object. The definition is based on
9 * chrome/browser/ui/webui/extensions/extension_basic_info.cc
10 * and
11 * chrome/browser/ui/webui/extensions/extension_settings_handler.cc
12 * ExtensionSettingsHandler::CreateExtensionDetailValue()
13 * @typedef {{allow_reload: boolean,
14 * allowAllUrls: boolean,
15 * allowFileAccess: boolean,
16 * blacklistText: string,
17 * corruptInstall: boolean,
18 * dependentExtensions: Array,
19 * description: string,
20 * detailsUrl: string,
21 * enable_show_button: boolean,
22 * enabled: boolean,
23 * enabledIncognito: boolean,
24 * errorCollectionEnabled: (boolean|undefined),
25 * hasPopupAction: boolean,
26 * homepageProvided: boolean,
27 * homepageUrl: string,
28 * icon: string,
29 * id: string,
30 * incognitoCanBeEnabled: boolean,
31 * installWarnings: (Array|undefined),
32 * is_hosted_app: boolean,
33 * is_platform_app: boolean,
34 * isUnpacked: boolean,
35 * kioskEnabled: boolean,
36 * kioskOnly: boolean,
37 * locationText: string,
38 * managedInstall: boolean,
39 * manifestErrors: (Array.<RuntimeError>|undefined),
40 * name: string,
41 * offlineEnabled: boolean,
42 * optionsUrl: string,
43 * order: number,
44 * packagedApp: boolean,
45 * path: (string|undefined),
46 * prettifiedPath: (string|undefined),
47 * runtimeErrors: (Array.<RuntimeError>|undefined),
48 * suspiciousInstall: boolean,
49 * terminated: boolean,
50 * version: string,
51 * views: Array.<{renderViewId: number, renderProcessId: number,
52 * path: string, incognito: boolean,
53 * generatedBackgroundPage: boolean}>,
54 * wantsAllUrls: boolean,
55 * wantsErrorCollection: boolean,
56 * wantsFileAccess: boolean,
57 * warnings: (Array|undefined)}}
59 var ExtensionData;
61 cr.define('options', function() {
62 'use strict';
64 /**
65 * Creates a new list of extensions.
66 * @param {Object=} opt_propertyBag Optional properties.
67 * @constructor
68 * @extends {HTMLDivElement}
70 var ExtensionsList = cr.ui.define('div');
72 /**
73 * @type {Object.<string, boolean>} A map from extension id to a boolean
74 * indicating whether the incognito warning is showing. This persists
75 * between calls to decorate.
77 var butterBarVisibility = {};
79 /**
80 * @type {Object.<string, number>} A map from extension id to last reloaded
81 * timestamp. The timestamp is recorded when the user click the 'Reload'
82 * link. It is used to refresh the icon of an unpacked extension.
83 * This persists between calls to decorate.
85 var extensionReloadedTimestamp = {};
87 ExtensionsList.prototype = {
88 __proto__: HTMLDivElement.prototype,
90 /**
91 * Indicates whether an embedded options page that was navigated to through
92 * the '?options=' URL query has been shown to the user. This is necessary
93 * to prevent showExtensionNodes_ from opening the options more than once.
94 * @type {boolean}
95 * @private
97 optionsShown_: false,
99 /** @override */
100 decorate: function() {
101 this.textContent = '';
103 this.showExtensionNodes_();
106 getIdQueryParam_: function() {
107 return parseQueryParams(document.location)['id'];
110 getOptionsQueryParam_: function() {
111 return parseQueryParams(document.location)['options'];
115 * Creates all extension items from scratch.
116 * @private
118 showExtensionNodes_: function() {
119 // Iterate over the extension data and add each item to the list.
120 this.data_.extensions.forEach(this.createNode_, this);
122 var idToHighlight = this.getIdQueryParam_();
123 if (idToHighlight && $(idToHighlight))
124 this.scrollToNode_(idToHighlight);
126 var idToOpenOptions = this.getOptionsQueryParam_();
127 if (idToOpenOptions && $(idToOpenOptions))
128 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
130 if (this.data_.extensions.length == 0)
131 this.classList.add('empty-extension-list');
132 else
133 this.classList.remove('empty-extension-list');
137 * Scrolls the page down to the extension node with the given id.
138 * @param {string} extensionId The id of the extension to scroll to.
139 * @private
141 scrollToNode_: function(extensionId) {
142 // Scroll offset should be calculated slightly higher than the actual
143 // offset of the element being scrolled to, so that it ends up not all
144 // the way at the top. That way it is clear that there are more elements
145 // above the element being scrolled to.
146 var scrollFudge = 1.2;
147 var scrollTop = $(extensionId).offsetTop - scrollFudge *
148 $(extensionId).clientHeight;
149 setScrollTopForDocument(document, scrollTop);
153 * Synthesizes and initializes an HTML element for the extension metadata
154 * given in |extension|.
155 * @param {ExtensionData} extension A dictionary of extension metadata.
156 * @private
158 createNode_: function(extension) {
159 var template = $('template-collection').querySelector(
160 '.extension-list-item-wrapper');
161 var node = template.cloneNode(true);
162 node.id = extension.id;
164 if (!extension.enabled || extension.terminated)
165 node.classList.add('inactive-extension');
167 if (extension.managedInstall ||
168 extension.dependentExtensions.length > 0) {
169 node.classList.add('may-not-modify');
170 node.classList.add('may-not-remove');
171 } else if (extension.suspiciousInstall || extension.corruptInstall) {
172 node.classList.add('may-not-modify');
175 var idToHighlight = this.getIdQueryParam_();
176 if (node.id == idToHighlight)
177 node.classList.add('extension-highlight');
179 var item = node.querySelector('.extension-list-item');
180 // Prevent the image cache of extension icon by using the reloaded
181 // timestamp as a query string. The timestamp is recorded when the user
182 // clicks the 'Reload' link. http://crbug.com/159302.
183 if (extensionReloadedTimestamp[extension.id]) {
184 item.style.backgroundImage =
185 'url(' + extension.icon + '?' +
186 extensionReloadedTimestamp[extension.id] + ')';
187 } else {
188 item.style.backgroundImage = 'url(' + extension.icon + ')';
191 var title = node.querySelector('.extension-title');
192 title.textContent = extension.name;
194 var version = node.querySelector('.extension-version');
195 version.textContent = extension.version;
197 var locationText = node.querySelector('.location-text');
198 locationText.textContent = extension.locationText;
200 var blacklistText = node.querySelector('.blacklist-text');
201 blacklistText.textContent = extension.blacklistText;
203 var description = document.createElement('span');
204 description.textContent = extension.description;
205 node.querySelector('.extension-description').appendChild(description);
207 // The 'Show Browser Action' button.
208 if (extension.enable_show_button) {
209 var showButton = node.querySelector('.show-button');
210 showButton.addEventListener('click', function(e) {
211 chrome.send('extensionSettingsShowButton', [extension.id]);
213 showButton.hidden = false;
216 // The 'allow in incognito' checkbox.
217 node.querySelector('.incognito-control').hidden =
218 !this.data_.incognitoAvailable;
219 var incognito = node.querySelector('.incognito-control input');
220 incognito.disabled = !extension.incognitoCanBeEnabled;
221 incognito.checked = extension.enabledIncognito;
222 if (!incognito.disabled) {
223 incognito.addEventListener('change', function(e) {
224 var checked = e.target.checked;
225 butterBarVisibility[extension.id] = checked;
226 butterBar.hidden = !checked || extension.is_hosted_app;
227 chrome.send('extensionSettingsEnableIncognito',
228 [extension.id, String(checked)]);
231 var butterBar = node.querySelector('.butter-bar');
232 butterBar.hidden = !butterBarVisibility[extension.id];
234 // The 'collect errors' checkbox. This should only be visible if the
235 // error console is enabled - we can detect this by the existence of the
236 // |errorCollectionEnabled| property.
237 if (extension.wantsErrorCollection) {
238 node.querySelector('.error-collection-control').hidden = false;
239 var errorCollection =
240 node.querySelector('.error-collection-control input');
241 errorCollection.checked = extension.errorCollectionEnabled;
242 errorCollection.addEventListener('change', function(e) {
243 chrome.send('extensionSettingsEnableErrorCollection',
244 [extension.id, String(e.target.checked)]);
248 // The 'allow on all urls' checkbox. This should only be visible if
249 // active script restrictions are enabled. If they are not enabled, no
250 // extensions should want all urls.
251 if (extension.wantsAllUrls) {
252 var allUrls = node.querySelector('.all-urls-control');
253 allUrls.addEventListener('click', function(e) {
254 chrome.send('extensionSettingsAllowOnAllUrls',
255 [extension.id, String(e.target.checked)]);
257 allUrls.querySelector('input').checked = extension.allowAllUrls;
258 allUrls.hidden = false;
261 // The 'allow file:// access' checkbox.
262 if (extension.wantsFileAccess) {
263 var fileAccess = node.querySelector('.file-access-control');
264 fileAccess.addEventListener('click', function(e) {
265 chrome.send('extensionSettingsAllowFileAccess',
266 [extension.id, String(e.target.checked)]);
268 fileAccess.querySelector('input').checked = extension.allowFileAccess;
269 fileAccess.hidden = false;
272 // The 'Options' link.
273 if (extension.enabled && extension.optionsUrl) {
274 var options = node.querySelector('.options-link');
275 options.addEventListener('click', function(e) {
276 if (!extension.optionsOpenInTab) {
277 this.showEmbeddedExtensionOptions_(extension.id, false);
278 } else {
279 chrome.send('extensionSettingsOptions', [extension.id]);
281 e.preventDefault();
282 }.bind(this));
283 options.hidden = false;
286 // The 'Permissions' link.
287 var permissions = node.querySelector('.permissions-link');
288 permissions.addEventListener('click', function(e) {
289 chrome.send('extensionSettingsPermissions', [extension.id]);
290 e.preventDefault();
293 // The 'View in Web Store/View Web Site' link.
294 if (extension.homepageUrl) {
295 var siteLink = node.querySelector('.site-link');
296 siteLink.href = extension.homepageUrl;
297 siteLink.textContent = loadTimeData.getString(
298 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
299 'extensionSettingsVisitWebStore');
300 siteLink.hidden = false;
303 if (extension.allow_reload) {
304 // The 'Reload' link.
305 var reload = node.querySelector('.reload-link');
306 reload.addEventListener('click', function(e) {
307 chrome.send('extensionSettingsReload', [extension.id]);
308 extensionReloadedTimestamp[extension.id] = Date.now();
310 reload.hidden = false;
312 if (extension.is_platform_app) {
313 // The 'Launch' link.
314 var launch = node.querySelector('.launch-link');
315 launch.addEventListener('click', function(e) {
316 chrome.send('extensionSettingsLaunch', [extension.id]);
318 launch.hidden = false;
322 if (!extension.terminated) {
323 // The 'Enabled' checkbox.
324 var enable = node.querySelector('.enable-checkbox');
325 enable.hidden = false;
326 var enableCheckboxDisabled = extension.managedInstall ||
327 extension.suspiciousInstall ||
328 extension.corruptInstall ||
329 extension.dependentExtensions.length > 0;
330 enable.querySelector('input').disabled = enableCheckboxDisabled;
332 if (!enableCheckboxDisabled) {
333 enable.addEventListener('click', function(e) {
334 // When e.target is the label instead of the checkbox, it doesn't
335 // have the checked property and the state of the checkbox is
336 // left unchanged.
337 var checked = e.target.checked;
338 if (checked == undefined)
339 checked = !e.currentTarget.querySelector('input').checked;
340 chrome.send('extensionSettingsEnable',
341 [extension.id, checked ? 'true' : 'false']);
343 // This may seem counter-intuitive (to not set/clear the checkmark)
344 // but this page will be updated asynchronously if the extension
345 // becomes enabled/disabled. It also might not become enabled or
346 // disabled, because the user might e.g. get prompted when enabling
347 // and choose not to.
348 e.preventDefault();
352 enable.querySelector('input').checked = extension.enabled;
353 } else {
354 var terminatedReload = node.querySelector('.terminated-reload-link');
355 terminatedReload.hidden = false;
356 terminatedReload.addEventListener('click', function(e) {
357 chrome.send('extensionSettingsReload', [extension.id]);
361 // 'Remove' button.
362 var trashTemplate = $('template-collection').querySelector('.trash');
363 var trash = trashTemplate.cloneNode(true);
364 trash.title = loadTimeData.getString('extensionUninstall');
365 trash.addEventListener('click', function(e) {
366 butterBarVisibility[extension.id] = false;
367 chrome.send('extensionSettingsUninstall', [extension.id]);
369 node.querySelector('.enable-controls').appendChild(trash);
371 // Developer mode ////////////////////////////////////////////////////////
373 // First we have the id.
374 var idLabel = node.querySelector('.extension-id');
375 idLabel.textContent = ' ' + extension.id;
377 // Then the path, if provided by unpacked extension.
378 if (extension.isUnpacked) {
379 var loadPath = node.querySelector('.load-path');
380 loadPath.hidden = false;
381 var pathLink = loadPath.querySelector('a:nth-of-type(1)');
382 pathLink.textContent = ' ' + extension.prettifiedPath;
383 pathLink.addEventListener('click', function(e) {
384 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
385 e.preventDefault();
389 // Then the 'managed, cannot uninstall/disable' message.
390 if (extension.managedInstall) {
391 node.querySelector('.managed-message').hidden = false;
392 } else {
393 if (extension.suspiciousInstall) {
394 // Then the 'This isn't from the webstore, looks suspicious' message.
395 node.querySelector('.suspicious-install-message').hidden = false;
397 if (extension.corruptInstall) {
398 // Then the 'This is a corrupt extension' message.
399 node.querySelector('.corrupt-install-message').hidden = false;
403 if (extension.dependentExtensions.length > 0) {
404 var dependentMessage =
405 node.querySelector('.dependent-extensions-message');
406 dependentMessage.hidden = false;
407 var dependentList = dependentMessage.querySelector('ul');
408 var dependentTemplate = $('template-collection').querySelector(
409 '.dependent-list-item');
410 extension.dependentExtensions.forEach(function(elem) {
411 var depNode = dependentTemplate.cloneNode(true);
412 depNode.querySelector('.dep-extension-title').textContent = elem.name;
413 depNode.querySelector('.dep-extension-id').textContent = elem.id;
414 dependentList.appendChild(depNode);
418 // Then active views.
419 if (extension.views.length > 0) {
420 var activeViews = node.querySelector('.active-views');
421 activeViews.hidden = false;
422 var link = activeViews.querySelector('a');
424 extension.views.forEach(function(view, i) {
425 var displayName = view.generatedBackgroundPage ?
426 loadTimeData.getString('backgroundPage') : view.path;
427 var label = displayName +
428 (view.incognito ?
429 ' ' + loadTimeData.getString('viewIncognito') : '') +
430 (view.renderProcessId == -1 ?
431 ' ' + loadTimeData.getString('viewInactive') : '');
432 link.textContent = label;
433 link.addEventListener('click', function(e) {
434 // TODO(estade): remove conversion to string?
435 chrome.send('extensionSettingsInspect', [
436 String(extension.id),
437 String(view.renderProcessId),
438 String(view.renderViewId),
439 view.incognito
443 if (i < extension.views.length - 1) {
444 link = link.cloneNode(true);
445 activeViews.appendChild(link);
450 // The extension warnings (describing runtime issues).
451 if (extension.warnings) {
452 var panel = node.querySelector('.extension-warnings');
453 panel.hidden = false;
454 var list = panel.querySelector('ul');
455 extension.warnings.forEach(function(warning) {
456 list.appendChild(document.createElement('li')).innerText = warning;
460 // If the ErrorConsole is enabled, we should have manifest and/or runtime
461 // errors. Otherwise, we may have install warnings. We should not have
462 // both ErrorConsole errors and install warnings.
463 if (extension.manifestErrors) {
464 var panel = node.querySelector('.manifest-errors');
465 panel.hidden = false;
466 panel.appendChild(new extensions.ExtensionErrorList(
467 extension.manifestErrors));
469 if (extension.runtimeErrors) {
470 var panel = node.querySelector('.runtime-errors');
471 panel.hidden = false;
472 panel.appendChild(new extensions.ExtensionErrorList(
473 extension.runtimeErrors));
475 if (extension.installWarnings) {
476 var panel = node.querySelector('.install-warnings');
477 panel.hidden = false;
478 var list = panel.querySelector('ul');
479 extension.installWarnings.forEach(function(warning) {
480 var li = document.createElement('li');
481 li.innerText = warning.message;
482 list.appendChild(li);
486 this.appendChild(node);
487 if (location.hash.substr(1) == extension.id) {
488 // Scroll beneath the fixed header so that the extension is not
489 // obscured.
490 var topScroll = node.offsetTop - $('page-header').offsetHeight;
491 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10);
492 if (!isNaN(pad))
493 topScroll -= pad / 2;
494 setScrollTopForDocument(document, topScroll);
499 * Opens the extension options overlay for the extension with the given id.
500 * @param {string} extensionId The id of extension whose options page should
501 * be displayed.
502 * @param {boolean} scroll Whether the page should scroll to the extension
503 * @private
505 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
506 if (this.optionsShown_)
507 return;
509 // Get the extension from the given id.
510 var extension = this.data_.extensions.filter(function(extension) {
511 return extension.id == extensionId;
512 })[0];
514 if (!extension)
515 return;
517 if (scroll)
518 this.scrollToNode_(extensionId);
519 // Add the options query string. Corner case: the 'options' query string
520 // will clobber the 'id' query string if the options link is clicked when
521 // 'id' is in the URL, or if both query strings are in the URL.
522 uber.replaceState({}, '?options=' + extensionId);
524 extensions.ExtensionOptionsOverlay.getInstance().
525 setExtensionAndShowOverlay(extensionId,
526 extension.name,
527 extension.icon);
529 this.optionsShown_ = true;
530 $('overlay').addEventListener('cancelOverlay', function() {
531 this.optionsShown_ = false;
532 }.bind(this));
536 return {
537 ExtensionsList: ExtensionsList