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"></include> 6 7cr.define('options', function() { 8 'use strict'; 9 10 /** 11 * Creates a new list of extensions. 12 * @param {Object=} opt_propertyBag Optional properties. 13 * @constructor 14 * @extends {cr.ui.div} 15 */ 16 var ExtensionsList = cr.ui.define('div'); 17 18 /** 19 * @type {Object.<string, boolean>} A map from extension id to a boolean 20 * indicating whether the incognito warning is showing. This persists 21 * between calls to decorate. 22 */ 23 var butterBarVisibility = {}; 24 25 /** 26 * @type {Object.<string, string>} A map from extension id to last reloaded 27 * timestamp. The timestamp is recorded when the user click the 'Reload' 28 * link. It is used to refresh the icon of an unpacked extension. 29 * This persists between calls to decorate. 30 */ 31 var extensionReloadedTimestamp = {}; 32 33 ExtensionsList.prototype = { 34 __proto__: HTMLDivElement.prototype, 35 36 /** @override */ 37 decorate: function() { 38 this.textContent = ''; 39 40 this.showExtensionNodes_(); 41 }, 42 43 getIdQueryParam_: function() { 44 return parseQueryParams(document.location)['id']; 45 }, 46 47 /** 48 * Creates all extension items from scratch. 49 * @private 50 */ 51 showExtensionNodes_: function() { 52 // Iterate over the extension data and add each item to the list. 53 this.data_.extensions.forEach(this.createNode_, this); 54 55 var idToHighlight = this.getIdQueryParam_(); 56 if (idToHighlight && $(idToHighlight)) { 57 // Scroll offset should be calculated slightly higher than the actual 58 // offset of the element being scrolled to, so that it ends up not all 59 // the way at the top. That way it is clear that there are more elements 60 // above the element being scrolled to. 61 var scrollFudge = 1.2; 62 var scrollTop = $(idToHighlight).offsetTop - scrollFudge * 63 $(idToHighlight).clientHeight; 64 setScrollTopForDocument(document, scrollTop); 65 } 66 67 if (this.data_.extensions.length == 0) 68 this.classList.add('empty-extension-list'); 69 else 70 this.classList.remove('empty-extension-list'); 71 }, 72 73 /** 74 * Synthesizes and initializes an HTML element for the extension metadata 75 * given in |extension|. 76 * @param {Object} extension A dictionary of extension metadata. 77 * @private 78 */ 79 createNode_: function(extension) { 80 var template = $('template-collection').querySelector( 81 '.extension-list-item-wrapper'); 82 var node = template.cloneNode(true); 83 node.id = extension.id; 84 85 if (!extension.enabled || extension.terminated) 86 node.classList.add('inactive-extension'); 87 88 if (extension.managedInstall) { 89 node.classList.add('may-not-modify'); 90 node.classList.add('may-not-remove'); 91 } else if (extension.suspiciousInstall || extension.corruptInstall) { 92 node.classList.add('may-not-modify'); 93 } 94 95 var idToHighlight = this.getIdQueryParam_(); 96 if (node.id == idToHighlight) 97 node.classList.add('extension-highlight'); 98 99 var item = node.querySelector('.extension-list-item'); 100 // Prevent the image cache of extension icon by using the reloaded 101 // timestamp as a query string. The timestamp is recorded when the user 102 // clicks the 'Reload' link. http://crbug.com/159302. 103 if (extensionReloadedTimestamp[extension.id]) { 104 item.style.backgroundImage = 105 'url(' + extension.icon + '?' + 106 extensionReloadedTimestamp[extension.id] + ')'; 107 } else { 108 item.style.backgroundImage = 'url(' + extension.icon + ')'; 109 } 110 111 var title = node.querySelector('.extension-title'); 112 title.textContent = extension.name; 113 114 var version = node.querySelector('.extension-version'); 115 version.textContent = extension.version; 116 117 var locationText = node.querySelector('.location-text'); 118 locationText.textContent = extension.locationText; 119 120 var blacklistText = node.querySelector('.blacklist-text'); 121 blacklistText.textContent = extension.blacklistText; 122 123 var description = node.querySelector('.extension-description span'); 124 description.textContent = extension.description; 125 126 // The 'Show Browser Action' button. 127 if (extension.enable_show_button) { 128 var showButton = node.querySelector('.show-button'); 129 showButton.addEventListener('click', function(e) { 130 chrome.send('extensionSettingsShowButton', [extension.id]); 131 }); 132 showButton.hidden = false; 133 } 134 135 // The 'allow in incognito' checkbox. 136 var incognito = node.querySelector('.incognito-control input'); 137 incognito.disabled = !extension.incognitoCanBeEnabled; 138 incognito.checked = extension.enabledIncognito; 139 if (!incognito.disabled) { 140 incognito.addEventListener('change', function(e) { 141 var checked = e.target.checked; 142 butterBarVisibility[extension.id] = checked; 143 butterBar.hidden = !checked || extension.is_hosted_app; 144 chrome.send('extensionSettingsEnableIncognito', 145 [extension.id, String(checked)]); 146 }); 147 } 148 var butterBar = node.querySelector('.butter-bar'); 149 butterBar.hidden = !butterBarVisibility[extension.id]; 150 151 // The 'collect errors' checkbox. This should only be visible if the 152 // error console is enabled - we can detect this by the existence of the 153 // |errorCollectionEnabled| property. 154 if (extension.wantsErrorCollection) { 155 node.querySelector('.error-collection-control').hidden = false; 156 var errorCollection = 157 node.querySelector('.error-collection-control input'); 158 errorCollection.checked = extension.errorCollectionEnabled; 159 errorCollection.addEventListener('change', function(e) { 160 chrome.send('extensionSettingsEnableErrorCollection', 161 [extension.id, String(e.target.checked)]); 162 }); 163 } 164 165 // The 'allow on all urls' checkbox. This should only be visible if 166 // active script restrictions are enabled. If they are not enabled, no 167 // extensions should want all urls. 168 if (extension.wantsAllUrls) { 169 var allUrls = node.querySelector('.all-urls-control'); 170 allUrls.addEventListener('click', function(e) { 171 chrome.send('extensionSettingsAllowOnAllUrls', 172 [extension.id, String(e.target.checked)]); 173 }); 174 allUrls.querySelector('input').checked = extension.allowAllUrls; 175 allUrls.hidden = false; 176 } 177 178 // The 'allow file:// access' checkbox. 179 if (extension.wantsFileAccess) { 180 var fileAccess = node.querySelector('.file-access-control'); 181 fileAccess.addEventListener('click', function(e) { 182 chrome.send('extensionSettingsAllowFileAccess', 183 [extension.id, String(e.target.checked)]); 184 }); 185 fileAccess.querySelector('input').checked = extension.allowFileAccess; 186 fileAccess.hidden = false; 187 } 188 189 // The 'Options' link. 190 if (extension.enabled && extension.optionsUrl) { 191 var options = node.querySelector('.options-link'); 192 options.addEventListener('click', function(e) { 193 chrome.send('extensionSettingsOptions', [extension.id]); 194 e.preventDefault(); 195 }); 196 options.hidden = false; 197 } 198 199 // The 'Permissions' link. 200 var permissions = node.querySelector('.permissions-link'); 201 permissions.addEventListener('click', function(e) { 202 chrome.send('extensionSettingsPermissions', [extension.id]); 203 e.preventDefault(); 204 }); 205 206 // The 'View in Web Store/View Web Site' link. 207 if (extension.homepageUrl) { 208 var siteLink = node.querySelector('.site-link'); 209 siteLink.href = extension.homepageUrl; 210 siteLink.textContent = loadTimeData.getString( 211 extension.homepageProvided ? 'extensionSettingsVisitWebsite' : 212 'extensionSettingsVisitWebStore'); 213 siteLink.hidden = false; 214 } 215 216 if (extension.allow_reload) { 217 // The 'Reload' link. 218 var reload = node.querySelector('.reload-link'); 219 reload.addEventListener('click', function(e) { 220 chrome.send('extensionSettingsReload', [extension.id]); 221 extensionReloadedTimestamp[extension.id] = Date.now(); 222 }); 223 reload.hidden = false; 224 225 if (extension.is_platform_app) { 226 // The 'Launch' link. 227 var launch = node.querySelector('.launch-link'); 228 launch.addEventListener('click', function(e) { 229 chrome.send('extensionSettingsLaunch', [extension.id]); 230 }); 231 launch.hidden = false; 232 } 233 } 234 235 if (!extension.terminated) { 236 // The 'Enabled' checkbox. 237 var enable = node.querySelector('.enable-checkbox'); 238 enable.hidden = false; 239 var managedOrHosedExtension = extension.managedInstall || 240 extension.suspiciousInstall || 241 extension.corruptInstall; 242 enable.querySelector('input').disabled = managedOrHosedExtension; 243 244 if (!managedOrHosedExtension) { 245 enable.addEventListener('click', function(e) { 246 // When e.target is the label instead of the checkbox, it doesn't 247 // have the checked property and the state of the checkbox is 248 // left unchanged. 249 var checked = e.target.checked; 250 if (checked == undefined) 251 checked = !e.currentTarget.querySelector('input').checked; 252 chrome.send('extensionSettingsEnable', 253 [extension.id, checked ? 'true' : 'false']); 254 255 // This may seem counter-intuitive (to not set/clear the checkmark) 256 // but this page will be updated asynchronously if the extension 257 // becomes enabled/disabled. It also might not become enabled or 258 // disabled, because the user might e.g. get prompted when enabling 259 // and choose not to. 260 e.preventDefault(); 261 }); 262 } 263 264 enable.querySelector('input').checked = extension.enabled; 265 } else { 266 var terminatedReload = node.querySelector('.terminated-reload-link'); 267 terminatedReload.hidden = false; 268 terminatedReload.addEventListener('click', function(e) { 269 chrome.send('extensionSettingsReload', [extension.id]); 270 }); 271 } 272 273 // 'Remove' button. 274 var trashTemplate = $('template-collection').querySelector('.trash'); 275 var trash = trashTemplate.cloneNode(true); 276 trash.title = loadTimeData.getString('extensionUninstall'); 277 trash.addEventListener('click', function(e) { 278 butterBarVisibility[extension.id] = false; 279 chrome.send('extensionSettingsUninstall', [extension.id]); 280 }); 281 node.querySelector('.enable-controls').appendChild(trash); 282 283 // Developer mode //////////////////////////////////////////////////////// 284 285 // First we have the id. 286 var idLabel = node.querySelector('.extension-id'); 287 idLabel.textContent = ' ' + extension.id; 288 289 // Then the path, if provided by unpacked extension. 290 if (extension.isUnpacked) { 291 var loadPath = node.querySelector('.load-path'); 292 loadPath.hidden = false; 293 loadPath.querySelector('span:nth-of-type(2)').textContent = 294 ' ' + extension.path; 295 } 296 297 // Then the 'managed, cannot uninstall/disable' message. 298 if (extension.managedInstall) { 299 node.querySelector('.managed-message').hidden = false; 300 } else { 301 if (extension.suspiciousInstall) { 302 // Then the 'This isn't from the webstore, looks suspicious' message. 303 node.querySelector('.suspicious-install-message').hidden = false; 304 } 305 if (extension.corruptInstall) { 306 // Then the 'This is a corrupt extension' message. 307 node.querySelector('.corrupt-install-message').hidden = false; 308 } 309 } 310 311 // Then active views. 312 if (extension.views.length > 0) { 313 var activeViews = node.querySelector('.active-views'); 314 activeViews.hidden = false; 315 var link = activeViews.querySelector('a'); 316 317 extension.views.forEach(function(view, i) { 318 var displayName = view.generatedBackgroundPage ? 319 loadTimeData.getString('backgroundPage') : view.path; 320 var label = displayName + 321 (view.incognito ? 322 ' ' + loadTimeData.getString('viewIncognito') : '') + 323 (view.renderProcessId == -1 ? 324 ' ' + loadTimeData.getString('viewInactive') : ''); 325 link.textContent = label; 326 link.addEventListener('click', function(e) { 327 // TODO(estade): remove conversion to string? 328 chrome.send('extensionSettingsInspect', [ 329 String(extension.id), 330 String(view.renderProcessId), 331 String(view.renderViewId), 332 view.incognito 333 ]); 334 }); 335 336 if (i < extension.views.length - 1) { 337 link = link.cloneNode(true); 338 activeViews.appendChild(link); 339 } 340 }); 341 } 342 343 // The extension warnings (describing runtime issues). 344 if (extension.warnings) { 345 var panel = node.querySelector('.extension-warnings'); 346 panel.hidden = false; 347 var list = panel.querySelector('ul'); 348 extension.warnings.forEach(function(warning) { 349 list.appendChild(document.createElement('li')).innerText = warning; 350 }); 351 } 352 353 // If the ErrorConsole is enabled, we should have manifest and/or runtime 354 // errors. Otherwise, we may have install warnings. We should not have 355 // both ErrorConsole errors and install warnings. 356 if (extension.manifestErrors) { 357 var panel = node.querySelector('.manifest-errors'); 358 panel.hidden = false; 359 panel.appendChild(new extensions.ExtensionErrorList( 360 extension.manifestErrors)); 361 } 362 if (extension.runtimeErrors) { 363 var panel = node.querySelector('.runtime-errors'); 364 panel.hidden = false; 365 panel.appendChild(new extensions.ExtensionErrorList( 366 extension.runtimeErrors)); 367 } 368 if (extension.installWarnings) { 369 var panel = node.querySelector('.install-warnings'); 370 panel.hidden = false; 371 var list = panel.querySelector('ul'); 372 extension.installWarnings.forEach(function(warning) { 373 var li = document.createElement('li'); 374 li.innerText = warning.message; 375 list.appendChild(li); 376 }); 377 } 378 379 this.appendChild(node); 380 if (location.hash.substr(1) == extension.id) { 381 // Scroll beneath the fixed header so that the extension is not 382 // obscured. 383 var topScroll = node.offsetTop - $('page-header').offsetHeight; 384 var pad = parseInt(getComputedStyle(node, null).marginTop, 10); 385 if (!isNaN(pad)) 386 topScroll -= pad / 2; 387 setScrollTopForDocument(document, topScroll); 388 } 389 }, 390 }; 391 392 return { 393 ExtensionsList: ExtensionsList 394 }; 395}); 396