• 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   * Clear all the content of a given element.
10   * @param {HTMLElement} element The element to be cleared.
11   */
12  function clearElement(element) {
13    while (element.firstChild)
14      element.removeChild(element.firstChild);
15  }
16
17  /**
18   * Get the url relative to the main extension url. If the url is
19   * unassociated with the extension, this will be the full url.
20   * @param {string} url The url to make relative.
21   * @param {string} extensionUrl The url for the extension resources, in the
22   *     form "chrome-etxension://<extension_id>/".
23   * @return {string} The url relative to the host.
24   */
25  function getRelativeUrl(url, extensionUrl) {
26    return url.substring(0, extensionUrl.length) == extensionUrl ?
27        url.substring(extensionUrl.length) : url;
28  }
29
30  /**
31   * The RuntimeErrorContent manages all content specifically associated with
32   * runtime errors; this includes stack frames and the context url.
33   * @constructor
34   * @extends {HTMLDivElement}
35   */
36  function RuntimeErrorContent() {
37    var contentArea = $('template-collection-extension-error-overlay').
38        querySelector('.extension-error-overlay-runtime-content').
39        cloneNode(true);
40    contentArea.__proto__ = RuntimeErrorContent.prototype;
41    contentArea.init();
42    return contentArea;
43  }
44
45  /**
46   * The name of the "active" class specific to extension errors (so as to
47   * not conflict with other rules).
48   * @type {string}
49   * @const
50   */
51  RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
52
53  /**
54   * Determine whether or not we should display the url to the user. We don't
55   * want to include any of our own code in stack traces.
56   * @param {string} url The url in question.
57   * @return {boolean} True if the url should be displayed, and false
58   *     otherwise (i.e., if it is an internal script).
59   */
60  RuntimeErrorContent.shouldDisplayForUrl = function(url) {
61    // All our internal scripts are in the 'extensions::' namespace.
62    return !/^extensions::/.test(url);
63  };
64
65  /**
66   * Send a call to chrome to open the developer tools for an error.
67   * This will call either the bound function in ExtensionErrorHandler or the
68   * API function from developerPrivate, depending on whether this is being
69   * used in the native chrome:extensions page or the Apps Developer Tool.
70   * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
71   * @param {Object} args The arguments to pass to openDevTools.
72   * @private
73   */
74  RuntimeErrorContent.openDevtools_ = function(args) {
75    if (chrome.send)
76      chrome.send('extensionErrorOpenDevTools', [args]);
77    else if (chrome.developerPrivate)
78      chrome.developerPrivate.openDevTools(args);
79    else
80      assert(false, 'Cannot call either openDevTools function.');
81  };
82
83  RuntimeErrorContent.prototype = {
84    __proto__: HTMLDivElement.prototype,
85
86    /**
87     * The underlying error whose details are being displayed.
88     * @type {Object}
89     * @private
90     */
91    error_: undefined,
92
93    /**
94     * The URL associated with this extension, i.e. chrome-extension://<id>/.
95     * @type {string}
96     * @private
97     */
98    extensionUrl_: undefined,
99
100    /**
101     * The node of the stack trace which is currently active.
102     * @type {HTMLElement}
103     * @private
104     */
105    currentFrameNode_: undefined,
106
107    /**
108     * Initialize the RuntimeErrorContent for the first time.
109     */
110    init: function() {
111      /**
112       * The stack trace element in the overlay.
113       * @type {HTMLElement}
114       * @private
115       */
116      this.stackTrace_ =
117          this.querySelector('.extension-error-overlay-stack-trace-list');
118      assert(this.stackTrace_);
119
120      /**
121       * The context URL element in the overlay.
122       * @type {HTMLElement}
123       * @private
124       */
125      this.contextUrl_ =
126          this.querySelector('.extension-error-overlay-context-url');
127      assert(this.contextUrl_);
128    },
129
130    /**
131     * Sets the error for the content.
132     * @param {Object} error The error whose content should be displayed.
133     * @param {string} extensionUrl The URL associated with this extension.
134     */
135    setError: function(error, extensionUrl) {
136      this.error_ = error;
137      this.extensionUrl_ = extensionUrl;
138      this.contextUrl_.textContent = error.contextUrl ?
139          getRelativeUrl(error.contextUrl, this.extensionUrl_) :
140          loadTimeData.getString('extensionErrorOverlayContextUnknown');
141      this.initStackTrace_();
142    },
143
144    /**
145     * Wipe content associated with a specific error.
146     */
147    clearError: function() {
148      this.error_ = undefined;
149      this.extensionUrl_ = undefined;
150      this.currentFrameNode_ = undefined;
151      clearElement(this.stackTrace_);
152      this.stackTrace_.hidden = true;
153    },
154
155    /**
156     * Makes |frame| active and deactivates the previously active frame (if
157     * there was one).
158     * @param {HTMLElement} frame The frame to activate.
159     * @private
160     */
161    setActiveFrame_: function(frameNode) {
162      if (this.currentFrameNode_) {
163        this.currentFrameNode_.classList.remove(
164            RuntimeErrorContent.ACTIVE_CLASS_NAME);
165      }
166
167      this.currentFrameNode_ = frameNode;
168      this.currentFrameNode_.classList.add(
169          RuntimeErrorContent.ACTIVE_CLASS_NAME);
170    },
171
172    /**
173     * Initialize the stack trace element of the overlay.
174     * @private
175     */
176    initStackTrace_: function() {
177      for (var i = 0; i < this.error_.stackTrace.length; ++i) {
178        var frame = this.error_.stackTrace[i];
179        // Don't include any internal calls (e.g., schemaBindings) in the
180        // stack trace.
181        if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
182          continue;
183
184        var frameNode = document.createElement('li');
185        // Attach the index of the frame to which this node refers (since we
186        // may skip some, this isn't a 1-to-1 match).
187        frameNode.indexIntoTrace = i;
188
189        // The description is a human-readable summation of the frame, in the
190        // form "<relative_url>:<line_number> (function)", e.g.
191        // "myfile.js:25 (myFunction)".
192        var description = getRelativeUrl(frame.url, this.extensionUrl_) +
193                          ':' + frame.lineNumber;
194        if (frame.functionName) {
195          var functionName = frame.functionName == '(anonymous function)' ?
196              loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
197              frame.functionName;
198          description += ' (' + functionName + ')';
199        }
200        frameNode.textContent = description;
201
202        // When the user clicks on a frame in the stack trace, we should
203        // highlight that overlay in the list, display the appropriate source
204        // code with the line highlighted, and link the "Open DevTools" button
205        // with that frame.
206        frameNode.addEventListener('click', function(frame, frameNode, e) {
207          if (this.currStackFrame_ == frameNode)
208            return;
209
210          this.setActiveFrame_(frameNode);
211
212          // Request the file source with the section highlighted; this will
213          // call ExtensionErrorOverlay.requestFileSourceResponse() when
214          // completed, which in turn calls setCode().
215          ExtensionErrorOverlay.requestFileSource(
216              {extensionId: this.error_.extensionId,
217               message: this.error_.message,
218               pathSuffix: getRelativeUrl(frame.url, this.extensionUrl_),
219               lineNumber: frame.lineNumber});
220        }.bind(this, frame, frameNode));
221
222        this.stackTrace_.appendChild(frameNode);
223      }
224
225      // Set the current stack frame to the first stack frame and show the
226      // trace, if one exists. (We can't just check error.stackTrace, because
227      // it's possible the trace was purely internal, and we don't show
228      // internal frames.)
229      if (this.stackTrace_.children.length > 0) {
230        this.stackTrace_.hidden = false;
231        this.setActiveFrame_(this.stackTrace_.firstChild);
232      }
233    },
234
235    /**
236     * Open the developer tools for the active stack frame.
237     */
238    openDevtools: function() {
239      var stackFrame =
240          this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
241
242      RuntimeErrorContent.openDevtools_(
243          {renderProcessId: this.error_.renderProcessId,
244           renderViewId: this.error_.renderViewId,
245           url: stackFrame.url,
246           lineNumber: stackFrame.lineNumber || 0,
247           columnNumber: stackFrame.columnNumber || 0});
248    }
249  };
250
251  /**
252   * The ExtensionErrorOverlay will show the contents of a file which pertains
253   * to the ExtensionError; this is either the manifest file (for manifest
254   * errors) or a source file (for runtime errors). If possible, the portion
255   * of the file which caused the error will be highlighted.
256   * @constructor
257   */
258  function ExtensionErrorOverlay() {
259    /**
260     * The content section for runtime errors; this is re-used for all
261     * runtime errors and attached/detached from the overlay as needed.
262     * @type {RuntimeErrorContent}
263     * @private
264     */
265    this.runtimeErrorContent_ = new RuntimeErrorContent();
266  }
267
268  /**
269   * Value of ExtensionError::RUNTIME_ERROR enum.
270   * @see extensions/browser/extension_error.h
271   * @type {number}
272   * @const
273   * @private
274   */
275  ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1;
276
277  /**
278   * The manifest filename.
279   * @type {string}
280   * @const
281   * @private
282   */
283  ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
284
285  /**
286   * Determine whether or not chrome can load the source for a given file; this
287   * can only be done if the file belongs to the extension.
288   * @param {string} file The file to load.
289   * @param {string} extensionUrl The url for the extension, in the form
290   *     chrome-extension://<extension-id>/.
291   * @return {boolean} True if the file can be loaded, false otherwise.
292   * @private
293   */
294  ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
295    return file.substr(0, extensionUrl.length) == extensionUrl ||
296           file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
297  };
298
299  /**
300   * Determine whether or not we can show an overlay with more details for
301   * the given extension error.
302   * @param {Object} error The extension error.
303   * @param {string} extensionUrl The url for the extension, in the form
304   *     "chrome-extension://<extension-id>/".
305   * @return {boolean} True if we can show an overlay for the error,
306   *     false otherwise.
307   */
308  ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) {
309    if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl))
310      return true;
311
312    if (error.stackTrace) {
313      for (var i = 0; i < error.stackTrace.length; ++i) {
314        if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url))
315          return true;
316      }
317    }
318
319    return false;
320  };
321
322  /**
323   * Send a call to chrome to request the source of a given file.
324   * This will call either the bound function in ExtensionErrorHandler or the
325   * API function from developerPrivate, depending on whether this is being
326   * used in the native chrome:extensions page or the Apps Developer Tool.
327   * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
328   * @param {Object} args The arguments to pass to requestFileSource.
329   */
330  ExtensionErrorOverlay.requestFileSource = function(args) {
331    if (chrome.send) {
332      chrome.send('extensionErrorRequestFileSource', [args]);
333    } else if (chrome.developerPrivate) {
334      chrome.developerPrivate.requestFileSource(args, function(result) {
335        extensions.ExtensionErrorOverlay.requestFileSourceResponse(result);
336      });
337    } else {
338      assert(false, 'Cannot call either requestFileSource function.');
339    }
340  };
341
342  cr.addSingletonGetter(ExtensionErrorOverlay);
343
344  ExtensionErrorOverlay.prototype = {
345    /**
346     * The underlying error whose details are being displayed.
347     * @type {Object}
348     * @private
349     */
350    error_: undefined,
351
352    /**
353     * Initialize the page.
354     * @param {function(HTMLDivElement)} showOverlay The function to show or
355     *     hide the ExtensionErrorOverlay; this should take a single parameter
356     *     which is either the overlay Div if the overlay should be displayed,
357     *     or null if the overlay should be hidden.
358     */
359    initializePage: function(showOverlay) {
360      var overlay = $('overlay');
361      cr.ui.overlay.setupOverlay(overlay);
362      cr.ui.overlay.globalInitialization();
363      overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
364
365      $('extension-error-overlay-dismiss').addEventListener(
366          'click', this.handleDismiss_.bind(this));
367
368      /**
369       * The element of the full overlay.
370       * @type {HTMLDivElement}
371       * @private
372       */
373      this.overlayDiv_ = $('extension-error-overlay');
374
375      /**
376       * The portion of the overlay which shows the code relating to the error
377       * and the corresponding line numbers.
378       * @type {ExtensionCode}
379       * @private
380       */
381      this.codeDiv_ =
382          new extensions.ExtensionCode($('extension-error-overlay-code'));
383
384      /**
385       * The function to show or hide the ExtensionErrorOverlay.
386       * @type {function}
387       * @param {boolean} isVisible Whether the overlay should be visible.
388       */
389      this.setVisible = function(isVisible) {
390        showOverlay(isVisible ? this.overlayDiv_ : null);
391        if (isVisible)
392          this.codeDiv_.scrollToError();
393      };
394
395      /**
396       * The button to open the developer tools (only available for runtime
397       * errors).
398       * @type {HTMLButtonElement}
399       * @private
400       */
401      this.openDevtoolsButton_ = $('extension-error-overlay-devtools-button');
402      this.openDevtoolsButton_.addEventListener('click', function() {
403          this.runtimeErrorContent_.openDevtools();
404      }.bind(this));
405    },
406
407    /**
408     * Handles a click on the dismiss ("OK" or close) buttons.
409     * @param {Event} e The click event.
410     * @private
411     */
412    handleDismiss_: function(e) {
413      this.setVisible(false);
414
415      // There's a chance that the overlay receives multiple dismiss events; in
416      // this case, handle it gracefully and return (since all necessary work
417      // will already have been done).
418      if (!this.error_)
419        return;
420
421      // Remove all previous content.
422      this.codeDiv_.clear();
423
424      this.openDevtoolsButton_.hidden = true;
425
426      if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
427        this.overlayDiv_.querySelector('.content-area').removeChild(
428            this.runtimeErrorContent_);
429        this.runtimeErrorContent_.clearError();
430      }
431
432      this.error_ = undefined;
433    },
434
435    /**
436     * Associate an error with the overlay. This will set the error for the
437     * overlay, and, if possible, will populate the code section of the overlay
438     * with the relevant file, load the stack trace, and generate links for
439     * opening devtools (the latter two only happen for runtime errors).
440     * @param {Object} error The error to show in the overlay.
441     * @param {string} extensionUrl The URL of the extension, in the form
442     *     "chrome-extension://<extension_id>".
443     */
444    setErrorAndShowOverlay: function(error, extensionUrl) {
445      this.error_ = error;
446
447      if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
448        this.runtimeErrorContent_.setError(this.error_, extensionUrl);
449        this.overlayDiv_.querySelector('.content-area').insertBefore(
450            this.runtimeErrorContent_,
451            this.codeDiv_.nextSibling);
452        this.openDevtoolsButton_.hidden = false;
453        this.openDevtoolsButton_.disabled = !error.canInspect;
454      }
455
456      if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
457        var relativeUrl = getRelativeUrl(error.source, extensionUrl);
458
459        var requestFileSourceArgs = {extensionId: error.extensionId,
460                                     message: error.message,
461                                     pathSuffix: relativeUrl};
462
463        if (relativeUrl.toLowerCase() ==
464                ExtensionErrorOverlay.MANIFEST_FILENAME_) {
465          requestFileSourceArgs.manifestKey = error.manifestKey;
466          requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
467        } else {
468          requestFileSourceArgs.lineNumber =
469              error.stackTrace && error.stackTrace[0] ?
470                  error.stackTrace[0].lineNumber : 0;
471        }
472        ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs);
473      } else {
474        ExtensionErrorOverlay.requestFileSourceResponse(null);
475      }
476    },
477
478    /**
479     * Set the code to be displayed in the code portion of the overlay.
480     * @see ExtensionErrorOverlay.requestFileSourceResponse().
481     * @param {?Object} code The code to be displayed. If |code| is null, then
482     *     a "Could not display code" message will be displayed instead.
483     */
484    setCode: function(code) {
485      document.querySelector(
486          '#extension-error-overlay .extension-error-overlay-title').
487              textContent = code.title;
488
489      this.codeDiv_.populate(
490          code,
491          loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay'));
492    },
493  };
494
495  /**
496   * Called by the ExtensionErrorHandler responding to the request for a file's
497   * source. Populate the content area of the overlay and display the overlay.
498   * @param {Object?} result An object with four strings - the title,
499   *     beforeHighlight, afterHighlight, and highlight. The three 'highlight'
500   *     strings represent three portions of the file's content to display - the
501   *     portion which is most relevant and should be emphasized (highlight),
502   *     and the parts both before and after this portion. These may be empty.
503   */
504  ExtensionErrorOverlay.requestFileSourceResponse = function(result) {
505    var overlay = extensions.ExtensionErrorOverlay.getInstance();
506    overlay.setCode(result);
507    overlay.setVisible(true);
508  };
509
510  // Export
511  return {
512    ExtensionErrorOverlay: ExtensionErrorOverlay
513  };
514});
515