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