• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 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
5cr.define('extensions', function() {
6  'use strict';
7
8  /**
9   * Returns whether or not a given |url| is associated with an extension.
10   * @param {string} url The url to examine.
11   * @param {string} extensionUrl The url of the extension.
12   * @return {boolean} Whether or not the url is associated with the extension.
13   */
14  function isExtensionUrl(url, extensionUrl) {
15    return url.substring(0, extensionUrl.length) == extensionUrl;
16  }
17
18  /**
19   * Get the url relative to the main extension url. If the url is
20   * unassociated with the extension, this will be the full url.
21   * @param {string} url The url to make relative.
22   * @param {string} extensionUrl The host for which the url is relative.
23   * @return {string} The url relative to the host.
24   */
25  function getRelativeUrl(url, extensionUrl) {
26    return isExtensionUrl(url, extensionUrl) ?
27        url.substring(extensionUrl.length) : url;
28  }
29
30  /**
31   * Clone a template within the extension error template collection.
32   * @param {string} templateName The class name of the template to clone.
33   * @return {HTMLElement} The clone of the template.
34   */
35  function cloneTemplate(templateName) {
36    return $('template-collection-extension-error').
37        querySelector('.' + templateName).cloneNode(true);
38  }
39
40  /**
41   * Creates a new ExtensionError HTMLElement; this is used to show a
42   * notification to the user when an error is caused by an extension.
43   * @param {Object} error The error the element should represent.
44   * @param {string} templateName The name of the template to clone for the
45   *     error ('extension-error-[detailed|simple]-wrapper').
46   * @constructor
47   * @extends {HTMLDivElement}
48   */
49  function ExtensionError(error, templateName) {
50    var div = cloneTemplate(templateName);
51    div.__proto__ = ExtensionError.prototype;
52    div.error_ = error;
53    div.decorate();
54    return div;
55  }
56
57  ExtensionError.prototype = {
58    __proto__: HTMLDivElement.prototype,
59
60    /** @override */
61    decorate: function() {
62      var metadata = cloneTemplate('extension-error-metadata');
63
64      // Add an additional class for the severity level.
65      if (this.error_.level == 0)
66        metadata.classList.add('extension-error-severity-info');
67      else if (this.error_.level == 1)
68        metadata.classList.add('extension-error-severity-warning');
69      else
70        metadata.classList.add('extension-error-severity-fatal');
71
72      var iconNode = document.createElement('img');
73      iconNode.className = 'extension-error-icon';
74      metadata.insertBefore(iconNode, metadata.firstChild);
75
76      // Add a property for the extension's base url in order to determine if
77      // a url belongs to the extension.
78      this.extensionUrl_ =
79          'chrome-extension://' + this.error_.extensionId + '/';
80
81      metadata.querySelector('.extension-error-message').textContent =
82          this.error_.message;
83
84      metadata.appendChild(this.createViewSourceAndInspect_(
85          getRelativeUrl(this.error_.source, this.extensionUrl_),
86          this.error_.source));
87
88      // The error template may specify a <summary> to put template metadata in.
89      // If not, just append it to the top-level element.
90      var metadataContainer = this.querySelector('summary') || this;
91      metadataContainer.appendChild(metadata);
92
93      var detailsNode = this.querySelector('.extension-error-details');
94      if (detailsNode && this.error_.contextUrl)
95        detailsNode.appendChild(this.createContextNode_());
96      if (detailsNode && this.error_.stackTrace) {
97        var stackNode = this.createStackNode_();
98        if (stackNode)
99          detailsNode.appendChild(this.createStackNode_());
100      }
101    },
102
103    /**
104     * Return a div with text |description|. If it's possible to view the source
105     * for |url|, linkify the div to do so. Attach an inspect button if it's
106     * possible to open the inspector for |url|.
107     * @param {string} description a human-friendly description the location
108     *     (e.g., filename, line).
109     * @param {string} url The url of the resource to view.
110     * @param {?number} line An optional line number of the resource.
111     * @param {?number} column An optional column number of the resource.
112     * @return {HTMLElement} The created node, either a link or plaintext.
113     * @private
114     */
115    createViewSourceAndInspect_: function(description, url, line, column) {
116      var errorLinks = document.createElement('div');
117      errorLinks.className = 'extension-error-links';
118
119      if (this.error_.canInspect)
120        errorLinks.appendChild(this.createInspectLink_(url, line, column));
121
122      if (this.canViewSource_(url))
123        var viewSource = this.createViewSourceLink_(url, line);
124      else
125        var viewSource = document.createElement('div');
126      viewSource.className = 'extension-error-view-source';
127      viewSource.textContent = description;
128      errorLinks.appendChild(viewSource);
129      return errorLinks;
130    },
131
132    /**
133     * Determine whether we can view the source of a given url.
134     * @param {string} url The url of the resource to view.
135     * @return {boolean} Whether or not we can view the source for the url.
136     * @private
137     */
138    canViewSource_: function(url) {
139      return isExtensionUrl(url, this.extensionUrl_) || url == 'manifest.json';
140    },
141
142    /**
143     * Determine whether or not we should display the url to the user. We don't
144     * want to include any of our own code in stack traces.
145     * @param {string} url The url in question.
146     * @return {boolean} True if the url should be displayed, and false
147     *     otherwise (i.e., if it is an internal script).
148     */
149    shouldDisplayForUrl_: function(url) {
150      var extensionsNamespace = 'extensions::';
151      // All our internal scripts are in the 'extensions::' namespace.
152      return url.substr(0, extensionsNamespace.length) != extensionsNamespace;
153    },
154
155    /**
156     * Create a clickable node to view the source for the given url.
157     * @param {string} url The url to the resource to view.
158     * @param {?number} line An optional line number of the resource (for
159     *     source files).
160     * @return {HTMLElement} The clickable node to view the source.
161     * @private
162     */
163    createViewSourceLink_: function(url, line) {
164      var viewSource = document.createElement('a');
165      viewSource.href = 'javascript:void(0)';
166      var relativeUrl = getRelativeUrl(url, this.extensionUrl_);
167      var requestFileSourceArgs = { 'extensionId': this.error_.extensionId,
168                                    'message': this.error_.message,
169                                    'pathSuffix': relativeUrl };
170      if (relativeUrl == 'manifest.json') {
171        requestFileSourceArgs.manifestKey = this.error_.manifestKey;
172        requestFileSourceArgs.manifestSpecific = this.error_.manifestSpecific;
173      } else {
174        // Prefer |line| if available, or default to the line of the last stack
175        // frame.
176        requestFileSourceArgs.lineNumber =
177            line ? line : this.getLastPosition_('lineNumber');
178      }
179
180      viewSource.addEventListener('click', function(e) {
181        chrome.send('extensionErrorRequestFileSource', [requestFileSourceArgs]);
182      });
183      viewSource.title = loadTimeData.getString('extensionErrorViewSource');
184      return viewSource;
185    },
186
187    /**
188     * Check the most recent stack frame to get the last position in the code.
189     * @param {string} type The position type, i.e. '[line|column]Number'.
190     * @return {?number} The last position of the given |type|, or undefined if
191     *     there is no stack trace to check.
192     * @private
193     */
194    getLastPosition_: function(type) {
195      var stackTrace = this.error_.stackTrace;
196      return stackTrace && stackTrace[0] ? stackTrace[0][type] : undefined;
197    },
198
199    /**
200     * Create an "Inspect" link, in the form of an icon.
201     * @param {?string} url The url of the resource to inspect; if absent, the
202     *     render view (and no particular resource) is inspected.
203     * @param {?number} line An optional line number of the resource.
204     * @param {?number} column An optional column number of the resource.
205     * @return {HTMLImageElement} The created "Inspect" link for the resource.
206     * @private
207     */
208    createInspectLink_: function(url, line, column) {
209      var linkWrapper = document.createElement('a');
210      linkWrapper.href = 'javascript:void(0)';
211      var inspectIcon = document.createElement('img');
212      inspectIcon.className = 'extension-error-inspect';
213      inspectIcon.title = loadTimeData.getString('extensionErrorInspect');
214
215      inspectIcon.addEventListener('click', function(e) {
216          chrome.send('extensionErrorOpenDevTools',
217                      [{'renderProcessId': this.error_.renderProcessId,
218                        'renderViewId': this.error_.renderViewId,
219                        'url': url,
220                        'lineNumber': line ? line :
221                            this.getLastPosition_('lineNumber'),
222                        'columnNumber': column ? column :
223                            this.getLastPosition_('columnNumber')}]);
224      }.bind(this));
225      linkWrapper.appendChild(inspectIcon);
226      return linkWrapper;
227    },
228
229    /**
230     * Get the context node for this error. This will attempt to link to the
231     * context in which the error occurred, and can be either an extension page
232     * or an external page.
233     * @return {HTMLDivElement} The context node for the error, including the
234     *     label and a link to the context.
235     * @private
236     */
237    createContextNode_: function() {
238      var node = cloneTemplate('extension-error-context-wrapper');
239      var linkNode = node.querySelector('a');
240      if (isExtensionUrl(this.error_.contextUrl, this.extensionUrl_)) {
241        linkNode.textContent = getRelativeUrl(this.error_.contextUrl,
242                                              this.extensionUrl_);
243      } else {
244        linkNode.textContent = this.error_.contextUrl;
245      }
246
247      // Prepend a link to inspect the context page, if possible.
248      if (this.error_.canInspect)
249        node.insertBefore(this.createInspectLink_(), linkNode);
250
251      linkNode.href = this.error_.contextUrl;
252      linkNode.target = '_blank';
253      return node;
254    },
255
256    /**
257     * Get a node for the stack trace for this error. Each stack frame will
258     * include a resource url, line number, and function name (possibly
259     * anonymous). If possible, these frames will also be linked for viewing the
260     * source and inspection.
261     * @return {HTMLDetailsElement} The stack trace node for this error, with
262     *     all stack frames nested in a details-summary object.
263     * @private
264     */
265    createStackNode_: function() {
266      var node = cloneTemplate('extension-error-stack-trace');
267      var listNode = node.querySelector('.extension-error-stack-trace-list');
268      this.error_.stackTrace.forEach(function(frame) {
269        if (!this.shouldDisplayForUrl_(frame.url))
270          return;
271        var frameNode = document.createElement('div');
272        var description = getRelativeUrl(frame.url, this.extensionUrl_) +
273                          ':' + frame.lineNumber;
274        if (frame.functionName) {
275          var functionName = frame.functionName == '(anonymous function)' ?
276              loadTimeData.getString('extensionErrorAnonymousFunction') :
277              frame.functionName;
278          description += ' (' + functionName + ')';
279        }
280        frameNode.appendChild(this.createViewSourceAndInspect_(
281            description, frame.url, frame.lineNumber, frame.columnNumber));
282        listNode.appendChild(
283            document.createElement('li')).appendChild(frameNode);
284      }, this);
285
286      if (listNode.childElementCount == 0)
287        return undefined;
288
289      return node;
290    },
291  };
292
293  /**
294   * A variable length list of runtime or manifest errors for a given extension.
295   * @param {Array.<Object>} errors The list of extension errors with which
296   *     to populate the list.
297   * @param {string} title The i18n key for the title of the error list, i.e.
298   *     'extensionErrors[Manifest,Runtime]Errors'.
299   * @constructor
300   * @extends {HTMLDivElement}
301   */
302  function ExtensionErrorList(errors, title) {
303    var div = cloneTemplate('extension-error-list');
304    div.__proto__ = ExtensionErrorList.prototype;
305    div.errors_ = errors;
306    div.title_ = title;
307    div.decorate();
308    return div;
309  }
310
311  ExtensionErrorList.prototype = {
312    __proto__: HTMLDivElement.prototype,
313
314    /**
315     * @private
316     * @const
317     * @type {number}
318     */
319    MAX_ERRORS_TO_SHOW_: 3,
320
321    /** @override */
322    decorate: function() {
323      this.querySelector('.extension-error-list-title').textContent =
324          loadTimeData.getString(this.title_);
325
326      this.contents_ = this.querySelector('.extension-error-list-contents');
327      this.errors_.forEach(function(error) {
328        this.contents_.appendChild(document.createElement('li')).appendChild(
329            new ExtensionError(error,
330                               error.contextUrl || error.stackTrace ?
331                                   'extension-error-detailed-wrapper' :
332                                   'extension-error-simple-wrapper'));
333      }, this);
334
335      if (this.contents_.children.length > this.MAX_ERRORS_TO_SHOW_) {
336        for (var i = this.MAX_ERRORS_TO_SHOW_;
337             i < this.contents_.children.length; ++i) {
338          this.contents_.children[i].hidden = true;
339        }
340        this.initShowMoreButton_();
341      }
342    },
343
344    /**
345     * Initialize the "Show More" button for the error list. If there are more
346     * than |MAX_ERRORS_TO_SHOW_| errors in the list.
347     * @private
348     */
349    initShowMoreButton_: function() {
350      var button = this.querySelector('.extension-error-list-show-more a');
351      button.hidden = false;
352      button.isShowingAll = false;
353      button.addEventListener('click', function(e) {
354        for (var i = this.MAX_ERRORS_TO_SHOW_;
355             i < this.contents_.children.length; ++i) {
356          this.contents_.children[i].hidden = button.isShowingAll;
357        }
358        var message = button.isShowingAll ? 'extensionErrorsShowMore' :
359                                            'extensionErrorsShowFewer';
360        button.textContent = loadTimeData.getString(message);
361        button.isShowingAll = !button.isShowingAll;
362      }.bind(this));
363    }
364  };
365
366  return {
367    ExtensionErrorList: ExtensionErrorList
368  };
369});
370