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 * isUnpacked: boolean,
35 * kioskEnabled: boolean,
37 * locationText: string,
38 * managedInstall: boolean,
39 * manifestErrors: (Array.<RuntimeError>|undefined),
41 * offlineEnabled: boolean,
44 * packagedApp: boolean,
45 * path: (string|undefined),
46 * prettifiedPath: (string|undefined),
47 * runtimeErrors: (Array.<RuntimeError>|undefined),
48 * suspiciousInstall: boolean,
49 * terminated: boolean,
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)}}
61 cr
.define('options', function() {
65 * Creates a new list of extensions.
66 * @param {Object=} opt_propertyBag Optional properties.
68 * @extends {HTMLDivElement}
70 var ExtensionsList
= cr
.ui
.define('div');
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
= {};
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,
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.
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.
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');
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.
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.
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
] + ')';
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);
279 chrome
.send('extensionSettingsOptions', [extension
.id
]);
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
]);
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
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.
352 enable
.querySelector('input').checked
= extension
.enabled
;
354 var terminatedReload
= node
.querySelector('.terminated-reload-link');
355 terminatedReload
.hidden
= false;
356 terminatedReload
.addEventListener('click', function(e
) {
357 chrome
.send('extensionSettingsReload', [extension
.id
]);
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
)]);
389 // Then the 'managed, cannot uninstall/disable' message.
390 if (extension
.managedInstall
) {
391 node
.querySelector('.managed-message').hidden
= false;
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
+
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
),
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
490 var topScroll
= node
.offsetTop
- $('page-header').offsetHeight
;
491 var pad
= parseInt(window
.getComputedStyle(node
, null).marginTop
, 10);
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
502 * @param {boolean} scroll Whether the page should scroll to the extension
505 showEmbeddedExtensionOptions_: function(extensionId
, scroll
) {
506 if (this.optionsShown_
)
509 // Get the extension from the given id.
510 var extension
= this.data_
.extensions
.filter(function(extension
) {
511 return extension
.id
== extensionId
;
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
,
529 this.optionsShown_
= true;
530 $('overlay').addEventListener('cancelOverlay', function() {
531 this.optionsShown_
= false;
537 ExtensionsList
: ExtensionsList