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. 4 5<include src="extension_error.js"> 6 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 * isFromStore: boolean, 35 * isUnpacked: boolean, 36 * kioskEnabled: boolean, 37 * kioskOnly: boolean, 38 * locationText: string, 39 * managedInstall: boolean, 40 * manifestErrors: (Array.<RuntimeError>|undefined), 41 * name: string, 42 * offlineEnabled: boolean, 43 * optionsUrl: string, 44 * order: number, 45 * packagedApp: boolean, 46 * path: (string|undefined), 47 * prettifiedPath: (string|undefined), 48 * runtimeErrors: (Array.<RuntimeError>|undefined), 49 * suspiciousInstall: boolean, 50 * terminated: boolean, 51 * version: string, 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)}} 59 */ 60var ExtensionData; 61 62cr.define('options', function() { 63 'use strict'; 64 65 /** 66 * Creates a new list of extensions. 67 * @param {Object=} opt_propertyBag Optional properties. 68 * @constructor 69 * @extends {HTMLDivElement} 70 */ 71 var ExtensionsList = cr.ui.define('div'); 72 73 /** 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. 77 */ 78 var butterBarVisibility = {}; 79 80 /** 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. 85 */ 86 var extensionReloadedTimestamp = {}; 87 88 ExtensionsList.prototype = { 89 __proto__: HTMLDivElement.prototype, 90 91 /** 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. 95 * @type {boolean} 96 * @private 97 */ 98 optionsShown_: false, 99 100 /** @override */ 101 decorate: function() { 102 this.textContent = ''; 103 104 this.showExtensionNodes_(); 105 }, 106 107 getIdQueryParam_: function() { 108 return parseQueryParams(document.location)['id']; 109 }, 110 111 getOptionsQueryParam_: function() { 112 return parseQueryParams(document.location)['options']; 113 }, 114 115 /** 116 * Creates all extension items from scratch. 117 * @private 118 */ 119 showExtensionNodes_: function() { 120 // Iterate over the extension data and add each item to the list. 121 this.data_.extensions.forEach(this.createNode_, this); 122 123 var idToHighlight = this.getIdQueryParam_(); 124 if (idToHighlight && $(idToHighlight)) 125 this.scrollToNode_(idToHighlight); 126 127 var idToOpenOptions = this.getOptionsQueryParam_(); 128 if (idToOpenOptions && $(idToOpenOptions)) 129 this.showEmbeddedExtensionOptions_(idToOpenOptions, true); 130 131 if (this.data_.extensions.length == 0) 132 this.classList.add('empty-extension-list'); 133 else 134 this.classList.remove('empty-extension-list'); 135 }, 136 137 /** 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. 140 * @private 141 */ 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); 151 }, 152 153 /** 154 * Synthesizes and initializes an HTML element for the extension metadata 155 * given in |extension|. 156 * @param {ExtensionData} extension A dictionary of extension metadata. 157 * @private 158 */ 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; 164 165 if (!extension.enabled || extension.terminated) 166 node.classList.add('inactive-extension'); 167 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'); 174 } 175 176 var idToHighlight = this.getIdQueryParam_(); 177 if (node.id == idToHighlight) 178 node.classList.add('extension-highlight'); 179 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] + ')'; 188 } else { 189 item.style.backgroundImage = 'url(' + extension.icon + ')'; 190 } 191 192 var title = node.querySelector('.extension-title'); 193 title.textContent = extension.name; 194 195 var version = node.querySelector('.extension-version'); 196 version.textContent = extension.version; 197 198 var locationText = node.querySelector('.location-text'); 199 locationText.textContent = extension.locationText; 200 201 var blacklistText = node.querySelector('.blacklist-text'); 202 blacklistText.textContent = extension.blacklistText; 203 204 var description = document.createElement('span'); 205 description.textContent = extension.description; 206 node.querySelector('.extension-description').appendChild(description); 207 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]); 213 }); 214 showButton.hidden = false; 215 } 216 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)]); 230 }); 231 } 232 var butterBar = node.querySelector('.butter-bar'); 233 butterBar.hidden = !butterBarVisibility[extension.id]; 234 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)]); 246 }); 247 } 248 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)]); 257 }); 258 allUrls.querySelector('input').checked = extension.allowAllUrls; 259 allUrls.hidden = false; 260 } 261 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)]); 268 }); 269 fileAccess.querySelector('input').checked = extension.allowFileAccess; 270 fileAccess.hidden = false; 271 } 272 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); 279 } else { 280 chrome.send('extensionSettingsOptions', [extension.id]); 281 } 282 e.preventDefault(); 283 }.bind(this)); 284 options.hidden = false; 285 } 286 287 // The 'Permissions' link. 288 var permissions = node.querySelector('.permissions-link'); 289 permissions.addEventListener('click', function(e) { 290 chrome.send('extensionSettingsPermissions', [extension.id]); 291 e.preventDefault(); 292 }); 293 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; 302 } 303 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(); 310 }); 311 reload.hidden = false; 312 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]); 318 }); 319 launch.hidden = false; 320 } 321 } 322 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]); 328 }; 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]); 334 }; 335 } else { 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; 344 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 349 // left unchanged. 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']); 355 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. 361 e.preventDefault(); 362 }); 363 } 364 365 enable.querySelector('input').checked = extension.enabled; 366 } 367 368 // 'Remove' button. 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]); 375 }); 376 node.querySelector('.enable-controls').appendChild(trash); 377 378 // Developer mode //////////////////////////////////////////////////////// 379 380 // First we have the id. 381 var idLabel = node.querySelector('.extension-id'); 382 idLabel.textContent = ' ' + extension.id; 383 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)]); 392 e.preventDefault(); 393 }); 394 } 395 396 // Then the 'managed, cannot uninstall/disable' message. 397 if (extension.managedInstall) { 398 node.querySelector('.managed-message').hidden = false; 399 } else { 400 if (extension.suspiciousInstall) { 401 // Then the 'This isn't from the webstore, looks suspicious' message. 402 node.querySelector('.suspicious-install-message').hidden = false; 403 } 404 if (extension.corruptInstall) { 405 // Then the 'This is a corrupt extension' message. 406 node.querySelector('.corrupt-install-message').hidden = false; 407 } 408 } 409 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); 422 }); 423 } 424 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'); 430 431 extension.views.forEach(function(view, i) { 432 var displayName = view.generatedBackgroundPage ? 433 loadTimeData.getString('backgroundPage') : view.path; 434 var label = displayName + 435 (view.incognito ? 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), 446 view.incognito 447 ]); 448 }); 449 450 if (i < extension.views.length - 1) { 451 link = link.cloneNode(true); 452 activeViews.appendChild(link); 453 } 454 }); 455 } 456 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; 464 }); 465 } 466 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)); 475 } 476 if (extension.runtimeErrors) { 477 var panel = node.querySelector('.runtime-errors'); 478 panel.hidden = false; 479 panel.appendChild(new extensions.ExtensionErrorList( 480 extension.runtimeErrors)); 481 } 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); 490 }); 491 } 492 493 this.appendChild(node); 494 if (location.hash.substr(1) == extension.id) { 495 // Scroll beneath the fixed header so that the extension is not 496 // obscured. 497 var topScroll = node.offsetTop - $('page-header').offsetHeight; 498 var pad = parseInt(window.getComputedStyle(node, null).marginTop, 10); 499 if (!isNaN(pad)) 500 topScroll -= pad / 2; 501 setScrollTopForDocument(document, topScroll); 502 } 503 }, 504 505 /** 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 508 * be displayed. 509 * @param {boolean} scroll Whether the page should scroll to the extension 510 * @private 511 */ 512 showEmbeddedExtensionOptions_: function(extensionId, scroll) { 513 if (this.optionsShown_) 514 return; 515 516 // Get the extension from the given id. 517 var extension = this.data_.extensions.filter(function(extension) { 518 return extension.id == extensionId; 519 })[0]; 520 521 if (!extension) 522 return; 523 524 if (scroll) 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); 530 531 extensions.ExtensionOptionsOverlay.getInstance(). 532 setExtensionAndShowOverlay(extensionId, 533 extension.name, 534 extension.icon); 535 536 this.optionsShown_ = true; 537 $('overlay').addEventListener('cancelOverlay', function() { 538 this.optionsShown_ = false; 539 }.bind(this)); 540 }, 541 }; 542 543 return { 544 ExtensionsList: ExtensionsList 545 }; 546}); 547