• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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