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.
6 * The type of the stack trace object. The definition is based on
7 * extensions/browser/extension_error.cc:RuntimeError::ToValue().
8 * @typedef {{columnNumber: number,
9 * functionName: string,
16 * The type of the extension error trace object. The definition is based on
17 * extensions/browser/extension_error.cc:RuntimeError::ToValue().
18 * @typedef {{canInspect: (boolean|undefined),
19 * contextUrl: (string|undefined),
20 * extensionId: string,
21 * fromIncognito: boolean,
23 * manifestKey: string,
24 * manifestSpecific: string,
26 * renderProcessId: (number|undefined),
27 * renderViewId: (number|undefined),
29 * stackTrace: (Array<StackTrace>|undefined),
34 cr.define('extensions', function() {
38 * Clear all the content of a given element.
39 * @param {HTMLElement} element The element to be cleared.
41 function clearElement(element) {
42 while (element.firstChild)
43 element.removeChild(element.firstChild);
47 * Get the url relative to the main extension url. If the url is
48 * unassociated with the extension, this will be the full url.
49 * @param {string} url The url to make relative.
50 * @param {string} extensionUrl The url for the extension resources, in the
51 * form "chrome-etxension://<extension_id>/".
52 * @return {string} The url relative to the host.
54 function getRelativeUrl(url, extensionUrl) {
55 return url.substring(0, extensionUrl.length) == extensionUrl ?
56 url.substring(extensionUrl.length) : url;
60 * The RuntimeErrorContent manages all content specifically associated with
61 * runtime errors; this includes stack frames and the context url.
63 * @extends {HTMLDivElement}
65 function RuntimeErrorContent() {
66 var contentArea = $('template-collection-extension-error-overlay').
67 querySelector('.extension-error-overlay-runtime-content').
69 contentArea.__proto__ = RuntimeErrorContent.prototype;
75 * The name of the "active" class specific to extension errors (so as to
76 * not conflict with other rules).
80 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
83 * Determine whether or not we should display the url to the user. We don't
84 * want to include any of our own code in stack traces.
85 * @param {string} url The url in question.
86 * @return {boolean} True if the url should be displayed, and false
87 * otherwise (i.e., if it is an internal script).
89 RuntimeErrorContent.shouldDisplayForUrl = function(url) {
90 // All our internal scripts are in the 'extensions::' namespace.
91 return !/^extensions::/.test(url);
95 * Send a call to chrome to open the developer tools for an error.
96 * This will call either the bound function in ExtensionErrorHandler or the
97 * API function from developerPrivate, depending on whether this is being
98 * used in the native chrome:extensions page or the Apps Developer Tool.
99 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
100 * @param {OpenDevToolsProperties} args The arguments to pass to openDevTools.
103 RuntimeErrorContent.openDevtools_ = function(args) {
105 chrome.send('extensionErrorOpenDevTools', [args]);
106 else if (chrome.developerPrivate)
107 chrome.developerPrivate.openDevTools(args);
109 assertNotReached('Cannot call either openDevTools function.');
112 RuntimeErrorContent.prototype = {
113 __proto__: HTMLDivElement.prototype,
116 * The underlying error whose details are being displayed.
117 * @type {?RuntimeError}
123 * The URL associated with this extension, i.e. chrome-extension://<id>/.
130 * The node of the stack trace which is currently active.
131 * @type {?HTMLElement}
134 currentFrameNode_: null,
137 * Initialize the RuntimeErrorContent for the first time.
141 * The stack trace element in the overlay.
142 * @type {HTMLElement}
145 this.stackTrace_ = /** @type {HTMLElement} */(
146 this.querySelector('.extension-error-overlay-stack-trace-list'));
147 assert(this.stackTrace_);
150 * The context URL element in the overlay.
151 * @type {HTMLElement}
154 this.contextUrl_ = /** @type {HTMLElement} */(
155 this.querySelector('.extension-error-overlay-context-url'));
156 assert(this.contextUrl_);
160 * Sets the error for the content.
161 * @param {RuntimeError} error The error whose content should
163 * @param {string} extensionUrl The URL associated with this extension.
165 setError: function(error, extensionUrl) {
167 this.extensionUrl_ = extensionUrl;
168 this.contextUrl_.textContent = error.contextUrl ?
169 getRelativeUrl(error.contextUrl, this.extensionUrl_) :
170 loadTimeData.getString('extensionErrorOverlayContextUnknown');
171 this.initStackTrace_();
175 * Wipe content associated with a specific error.
177 clearError: function() {
179 this.extensionUrl_ = null;
180 this.currentFrameNode_ = null;
181 clearElement(this.stackTrace_);
182 this.stackTrace_.hidden = true;
186 * Makes |frame| active and deactivates the previously active frame (if
188 * @param {HTMLElement} frameNode The frame to activate.
191 setActiveFrame_: function(frameNode) {
192 if (this.currentFrameNode_) {
193 this.currentFrameNode_.classList.remove(
194 RuntimeErrorContent.ACTIVE_CLASS_NAME);
197 this.currentFrameNode_ = frameNode;
198 this.currentFrameNode_.classList.add(
199 RuntimeErrorContent.ACTIVE_CLASS_NAME);
203 * Initialize the stack trace element of the overlay.
206 initStackTrace_: function() {
207 for (var i = 0; i < this.error_.stackTrace.length; ++i) {
208 var frame = this.error_.stackTrace[i];
209 // Don't include any internal calls (e.g., schemaBindings) in the
211 if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
214 var frameNode = document.createElement('li');
215 // Attach the index of the frame to which this node refers (since we
216 // may skip some, this isn't a 1-to-1 match).
217 frameNode.indexIntoTrace = i;
219 // The description is a human-readable summation of the frame, in the
220 // form "<relative_url>:<line_number> (function)", e.g.
221 // "myfile.js:25 (myFunction)".
222 var description = getRelativeUrl(frame.url,
223 assert(this.extensionUrl_)) + ':' + frame.lineNumber;
224 if (frame.functionName) {
225 var functionName = frame.functionName == '(anonymous function)' ?
226 loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
228 description += ' (' + functionName + ')';
230 frameNode.textContent = description;
232 // When the user clicks on a frame in the stack trace, we should
233 // highlight that overlay in the list, display the appropriate source
234 // code with the line highlighted, and link the "Open DevTools" button
236 frameNode.addEventListener('click', function(frame, frameNode, e) {
237 this.setActiveFrame_(frameNode);
239 // Request the file source with the section highlighted; this will
240 // call ExtensionErrorOverlay.requestFileSourceResponse() when
241 // completed, which in turn calls setCode().
242 ExtensionErrorOverlay.requestFileSource(
243 {extensionId: this.error_.extensionId,
244 message: this.error_.message,
245 pathSuffix: getRelativeUrl(frame.url,
246 assert(this.extensionUrl_)),
247 lineNumber: frame.lineNumber});
248 }.bind(this, frame, frameNode));
250 this.stackTrace_.appendChild(frameNode);
253 // Set the current stack frame to the first stack frame and show the
254 // trace, if one exists. (We can't just check error.stackTrace, because
255 // it's possible the trace was purely internal, and we don't show
257 if (this.stackTrace_.children.length > 0) {
258 this.stackTrace_.hidden = false;
259 this.setActiveFrame_(assertInstanceof(this.stackTrace_.firstChild,
265 * Open the developer tools for the active stack frame.
267 openDevtools: function() {
269 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
271 RuntimeErrorContent.openDevtools_(
272 {renderProcessId: this.error_.renderProcessId || -1,
273 renderViewId: this.error_.renderViewId || -1,
275 lineNumber: stackFrame.lineNumber || 0,
276 columnNumber: stackFrame.columnNumber || 0});
281 * The ExtensionErrorOverlay will show the contents of a file which pertains
282 * to the ExtensionError; this is either the manifest file (for manifest
283 * errors) or a source file (for runtime errors). If possible, the portion
284 * of the file which caused the error will be highlighted.
287 function ExtensionErrorOverlay() {
289 * The content section for runtime errors; this is re-used for all
290 * runtime errors and attached/detached from the overlay as needed.
291 * @type {RuntimeErrorContent}
294 this.runtimeErrorContent_ = new RuntimeErrorContent();
298 * Value of ExtensionError::RUNTIME_ERROR enum.
299 * @see extensions/browser/extension_error.h
304 ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1;
307 * The manifest filename.
312 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
315 * Determine whether or not chrome can load the source for a given file; this
316 * can only be done if the file belongs to the extension.
317 * @param {string} file The file to load.
318 * @param {string} extensionUrl The url for the extension, in the form
319 * chrome-extension://<extension-id>/.
320 * @return {boolean} True if the file can be loaded, false otherwise.
323 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
324 return file.substr(0, extensionUrl.length) == extensionUrl ||
325 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
329 * Determine whether or not we can show an overlay with more details for
330 * the given extension error.
331 * @param {Object} error The extension error.
332 * @param {string} extensionUrl The url for the extension, in the form
333 * "chrome-extension://<extension-id>/".
334 * @return {boolean} True if we can show an overlay for the error,
337 ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) {
338 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl))
341 if (error.stackTrace) {
342 for (var i = 0; i < error.stackTrace.length; ++i) {
343 if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url))
352 * Send a call to chrome to request the source of a given file.
353 * This will call either the bound function in ExtensionErrorHandler or the
354 * API function from developerPrivate, depending on whether this is being
355 * used in the native chrome:extensions page or the Apps Developer Tool.
356 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
357 * @param {RequestFileSourceProperties} args The arguments to pass to
360 ExtensionErrorOverlay.requestFileSource = function(args) {
362 chrome.send('extensionErrorRequestFileSource', [args]);
363 } else if (chrome.developerPrivate) {
364 chrome.developerPrivate.requestFileSource(args, function(result) {
365 extensions.ExtensionErrorOverlay.requestFileSourceResponse(result);
368 assertNotReached('Cannot call either requestFileSource function.');
372 cr.addSingletonGetter(ExtensionErrorOverlay);
374 ExtensionErrorOverlay.prototype = {
376 * The underlying error whose details are being displayed.
377 * @type {?RuntimeError}
383 * Initialize the page.
384 * @param {function(HTMLDivElement)} showOverlay The function to show or
385 * hide the ExtensionErrorOverlay; this should take a single parameter
386 * which is either the overlay Div if the overlay should be displayed,
387 * or null if the overlay should be hidden.
389 initializePage: function(showOverlay) {
390 var overlay = $('overlay');
391 cr.ui.overlay.setupOverlay(overlay);
392 cr.ui.overlay.globalInitialization();
393 overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
395 $('extension-error-overlay-dismiss').addEventListener(
396 'click', this.handleDismiss_.bind(this));
399 * The element of the full overlay.
400 * @type {HTMLDivElement}
403 this.overlayDiv_ = /** @type {HTMLDivElement} */(
404 $('extension-error-overlay'));
407 * The portion of the overlay which shows the code relating to the error
408 * and the corresponding line numbers.
409 * @type {extensions.ExtensionCode}
413 new extensions.ExtensionCode($('extension-error-overlay-code'));
416 * The function to show or hide the ExtensionErrorOverlay.
417 * @param {boolean} isVisible Whether the overlay should be visible.
419 this.setVisible = function(isVisible) {
420 showOverlay(isVisible ? this.overlayDiv_ : null);
422 this.codeDiv_.scrollToError();
426 * The button to open the developer tools (only available for runtime
428 * @type {HTMLButtonElement}
431 this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */(
432 $('extension-error-overlay-devtools-button'));
433 this.openDevtoolsButton_.addEventListener('click', function() {
434 this.runtimeErrorContent_.openDevtools();
439 * Handles a click on the dismiss ("OK" or close) buttons.
440 * @param {Event} e The click event.
443 handleDismiss_: function(e) {
444 this.setVisible(false);
446 // There's a chance that the overlay receives multiple dismiss events; in
447 // this case, handle it gracefully and return (since all necessary work
448 // will already have been done).
452 // Remove all previous content.
453 this.codeDiv_.clear();
455 this.openDevtoolsButton_.hidden = true;
457 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
458 this.overlayDiv_.querySelector('.content-area').removeChild(
459 this.runtimeErrorContent_);
460 this.runtimeErrorContent_.clearError();
467 * Associate an error with the overlay. This will set the error for the
468 * overlay, and, if possible, will populate the code section of the overlay
469 * with the relevant file, load the stack trace, and generate links for
470 * opening devtools (the latter two only happen for runtime errors).
471 * @param {RuntimeError} error The error to show in the overlay.
472 * @param {string} extensionUrl The URL of the extension, in the form
473 * "chrome-extension://<extension_id>".
475 setErrorAndShowOverlay: function(error, extensionUrl) {
478 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
479 this.runtimeErrorContent_.setError(this.error_, extensionUrl);
480 this.overlayDiv_.querySelector('.content-area').insertBefore(
481 this.runtimeErrorContent_,
482 this.codeDiv_.nextSibling);
483 this.openDevtoolsButton_.hidden = false;
484 this.openDevtoolsButton_.disabled = !error.canInspect;
487 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
488 var relativeUrl = getRelativeUrl(error.source, extensionUrl);
490 var requestFileSourceArgs = {extensionId: error.extensionId,
491 message: error.message,
492 pathSuffix: relativeUrl};
494 if (relativeUrl.toLowerCase() ==
495 ExtensionErrorOverlay.MANIFEST_FILENAME_) {
496 requestFileSourceArgs.manifestKey = error.manifestKey;
497 requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
499 requestFileSourceArgs.lineNumber =
500 error.stackTrace && error.stackTrace[0] ?
501 error.stackTrace[0].lineNumber : 0;
503 ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs);
505 ExtensionErrorOverlay.requestFileSourceResponse(null);
511 * Set the code to be displayed in the code portion of the overlay.
512 * @see ExtensionErrorOverlay.requestFileSourceResponse().
513 * @param {?ExtensionHighlight} code The code to be displayed. If |code| is
515 * a "Could not display code" message will be displayed instead.
517 setCode: function(code) {
518 document.querySelector(
519 '#extension-error-overlay .extension-error-overlay-title').
520 textContent = code.title;
522 this.codeDiv_.populate(
524 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay'));
529 * Called by the ExtensionErrorHandler responding to the request for a file's
530 * source. Populate the content area of the overlay and display the overlay.
531 * @param {?ExtensionHighlight} result The three 'highlight' strings represent
532 * three portions of the file's content to display - the portion which is
533 * most relevant and should be emphasized (highlight), and the parts both
534 * before and after this portion. These may be empty.
536 ExtensionErrorOverlay.requestFileSourceResponse = function(result) {
537 var overlay = extensions.ExtensionErrorOverlay.getInstance();
538 overlay.setCode(result);
539 overlay.setVisible(true);
544 ExtensionErrorOverlay: ExtensionErrorOverlay