Move Webstore URL concepts to //extensions and out
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_error_overlay.js
blob5d5978c046e59565fd102ca2bf38c4646c526146
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.
5 /**
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,
10 * lineNumber: number,
11 * url: string}}
13 var StackTrace;
15 /**
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,
22 * level: number,
23 * manifestKey: string,
24 * manifestSpecific: string,
25 * message: string,
26 * renderProcessId: (number|undefined),
27 * renderViewId: (number|undefined),
28 * source: string,
29 * stackTrace: (Array.<StackTrace>|undefined),
30 * type: number}}
32 var RuntimeError;
34 cr.define('extensions', function() {
35 'use strict';
37 /**
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);
46 /**
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;
59 /**
60 * The RuntimeErrorContent manages all content specifically associated with
61 * runtime errors; this includes stack frames and the context url.
62 * @constructor
63 * @extends {HTMLDivElement}
65 function RuntimeErrorContent() {
66 var contentArea = $('template-collection-extension-error-overlay').
67 querySelector('.extension-error-overlay-runtime-content').
68 cloneNode(true);
69 contentArea.__proto__ = RuntimeErrorContent.prototype;
70 contentArea.init();
71 return contentArea;
74 /**
75 * The name of the "active" class specific to extension errors (so as to
76 * not conflict with other rules).
77 * @type {string}
78 * @const
80 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
82 /**
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);
94 /**
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 {Object} args The arguments to pass to openDevTools.
101 * @private
103 RuntimeErrorContent.openDevtools_ = function(args) {
104 if (chrome.send)
105 chrome.send('extensionErrorOpenDevTools', [args]);
106 else if (chrome.developerPrivate)
107 chrome.developerPrivate.openDevTools(args);
108 else
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}
118 * @private
120 error_: null,
123 * The URL associated with this extension, i.e. chrome-extension://<id>/.
124 * @type {?string}
125 * @private
127 extensionUrl_: null,
130 * The node of the stack trace which is currently active.
131 * @type {?HTMLElement}
132 * @private
134 currentFrameNode_: null,
137 * Initialize the RuntimeErrorContent for the first time.
139 init: function() {
141 * The stack trace element in the overlay.
142 * @type {HTMLElement}
143 * @private
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}
152 * @private
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
162 * be displayed.
163 * @param {string} extensionUrl The URL associated with this extension.
165 setError: function(error, extensionUrl) {
166 this.error_ = error;
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() {
178 this.error_ = null;
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
187 * there was one).
188 * @param {HTMLElement} frameNode The frame to activate.
189 * @private
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.
204 * @private
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
210 // stack trace.
211 if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
212 continue;
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') :
227 frame.functionName;
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
235 // with that frame.
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, this.extensionUrl_),
246 lineNumber: frame.lineNumber});
247 }.bind(this, frame, frameNode));
249 this.stackTrace_.appendChild(frameNode);
252 // Set the current stack frame to the first stack frame and show the
253 // trace, if one exists. (We can't just check error.stackTrace, because
254 // it's possible the trace was purely internal, and we don't show
255 // internal frames.)
256 if (this.stackTrace_.children.length > 0) {
257 this.stackTrace_.hidden = false;
258 this.setActiveFrame_(assertInstanceof(this.stackTrace_.firstChild,
259 HTMLElement));
264 * Open the developer tools for the active stack frame.
266 openDevtools: function() {
267 var stackFrame =
268 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
270 RuntimeErrorContent.openDevtools_(
271 {renderProcessId: this.error_.renderProcessId,
272 renderViewId: this.error_.renderViewId,
273 url: stackFrame.url,
274 lineNumber: stackFrame.lineNumber || 0,
275 columnNumber: stackFrame.columnNumber || 0});
280 * The ExtensionErrorOverlay will show the contents of a file which pertains
281 * to the ExtensionError; this is either the manifest file (for manifest
282 * errors) or a source file (for runtime errors). If possible, the portion
283 * of the file which caused the error will be highlighted.
284 * @constructor
286 function ExtensionErrorOverlay() {
288 * The content section for runtime errors; this is re-used for all
289 * runtime errors and attached/detached from the overlay as needed.
290 * @type {RuntimeErrorContent}
291 * @private
293 this.runtimeErrorContent_ = new RuntimeErrorContent();
297 * Value of ExtensionError::RUNTIME_ERROR enum.
298 * @see extensions/browser/extension_error.h
299 * @type {number}
300 * @const
301 * @private
303 ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1;
306 * The manifest filename.
307 * @type {string}
308 * @const
309 * @private
311 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
314 * Determine whether or not chrome can load the source for a given file; this
315 * can only be done if the file belongs to the extension.
316 * @param {string} file The file to load.
317 * @param {string} extensionUrl The url for the extension, in the form
318 * chrome-extension://<extension-id>/.
319 * @return {boolean} True if the file can be loaded, false otherwise.
320 * @private
322 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
323 return file.substr(0, extensionUrl.length) == extensionUrl ||
324 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
328 * Determine whether or not we can show an overlay with more details for
329 * the given extension error.
330 * @param {Object} error The extension error.
331 * @param {string} extensionUrl The url for the extension, in the form
332 * "chrome-extension://<extension-id>/".
333 * @return {boolean} True if we can show an overlay for the error,
334 * false otherwise.
336 ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) {
337 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl))
338 return true;
340 if (error.stackTrace) {
341 for (var i = 0; i < error.stackTrace.length; ++i) {
342 if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url))
343 return true;
347 return false;
351 * Send a call to chrome to request the source of a given file.
352 * This will call either the bound function in ExtensionErrorHandler or the
353 * API function from developerPrivate, depending on whether this is being
354 * used in the native chrome:extensions page or the Apps Developer Tool.
355 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
356 * @param {Object} args The arguments to pass to requestFileSource.
358 ExtensionErrorOverlay.requestFileSource = function(args) {
359 if (chrome.send) {
360 chrome.send('extensionErrorRequestFileSource', [args]);
361 } else if (chrome.developerPrivate) {
362 chrome.developerPrivate.requestFileSource(args, function(result) {
363 extensions.ExtensionErrorOverlay.requestFileSourceResponse(result);
365 } else {
366 assertNotReached('Cannot call either requestFileSource function.');
370 cr.addSingletonGetter(ExtensionErrorOverlay);
372 ExtensionErrorOverlay.prototype = {
374 * The underlying error whose details are being displayed.
375 * @type {?RuntimeError}
376 * @private
378 error_: null,
381 * Initialize the page.
382 * @param {function(HTMLDivElement)} showOverlay The function to show or
383 * hide the ExtensionErrorOverlay; this should take a single parameter
384 * which is either the overlay Div if the overlay should be displayed,
385 * or null if the overlay should be hidden.
387 initializePage: function(showOverlay) {
388 var overlay = $('overlay');
389 cr.ui.overlay.setupOverlay(overlay);
390 cr.ui.overlay.globalInitialization();
391 overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
393 $('extension-error-overlay-dismiss').addEventListener(
394 'click', this.handleDismiss_.bind(this));
397 * The element of the full overlay.
398 * @type {HTMLDivElement}
399 * @private
401 this.overlayDiv_ = /** @type {HTMLDivElement} */(
402 $('extension-error-overlay'));
405 * The portion of the overlay which shows the code relating to the error
406 * and the corresponding line numbers.
407 * @type {extensions.ExtensionCode}
408 * @private
410 this.codeDiv_ =
411 new extensions.ExtensionCode($('extension-error-overlay-code'));
414 * The function to show or hide the ExtensionErrorOverlay.
415 * @param {boolean} isVisible Whether the overlay should be visible.
417 this.setVisible = function(isVisible) {
418 showOverlay(isVisible ? this.overlayDiv_ : null);
419 if (isVisible)
420 this.codeDiv_.scrollToError();
424 * The button to open the developer tools (only available for runtime
425 * errors).
426 * @type {HTMLButtonElement}
427 * @private
429 this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */(
430 $('extension-error-overlay-devtools-button'));
431 this.openDevtoolsButton_.addEventListener('click', function() {
432 this.runtimeErrorContent_.openDevtools();
433 }.bind(this));
437 * Handles a click on the dismiss ("OK" or close) buttons.
438 * @param {Event} e The click event.
439 * @private
441 handleDismiss_: function(e) {
442 this.setVisible(false);
444 // There's a chance that the overlay receives multiple dismiss events; in
445 // this case, handle it gracefully and return (since all necessary work
446 // will already have been done).
447 if (!this.error_)
448 return;
450 // Remove all previous content.
451 this.codeDiv_.clear();
453 this.openDevtoolsButton_.hidden = true;
455 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
456 this.overlayDiv_.querySelector('.content-area').removeChild(
457 this.runtimeErrorContent_);
458 this.runtimeErrorContent_.clearError();
461 this.error_ = null;
465 * Associate an error with the overlay. This will set the error for the
466 * overlay, and, if possible, will populate the code section of the overlay
467 * with the relevant file, load the stack trace, and generate links for
468 * opening devtools (the latter two only happen for runtime errors).
469 * @param {RuntimeError} error The error to show in the overlay.
470 * @param {string} extensionUrl The URL of the extension, in the form
471 * "chrome-extension://<extension_id>".
473 setErrorAndShowOverlay: function(error, extensionUrl) {
474 this.error_ = error;
476 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
477 this.runtimeErrorContent_.setError(this.error_, extensionUrl);
478 this.overlayDiv_.querySelector('.content-area').insertBefore(
479 this.runtimeErrorContent_,
480 this.codeDiv_.nextSibling);
481 this.openDevtoolsButton_.hidden = false;
482 this.openDevtoolsButton_.disabled = !error.canInspect;
485 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
486 var relativeUrl = getRelativeUrl(error.source, extensionUrl);
488 var requestFileSourceArgs = {extensionId: error.extensionId,
489 message: error.message,
490 pathSuffix: relativeUrl};
492 if (relativeUrl.toLowerCase() ==
493 ExtensionErrorOverlay.MANIFEST_FILENAME_) {
494 requestFileSourceArgs.manifestKey = error.manifestKey;
495 requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
496 } else {
497 requestFileSourceArgs.lineNumber =
498 error.stackTrace && error.stackTrace[0] ?
499 error.stackTrace[0].lineNumber : 0;
501 ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs);
502 } else {
503 ExtensionErrorOverlay.requestFileSourceResponse(null);
509 * Set the code to be displayed in the code portion of the overlay.
510 * @see ExtensionErrorOverlay.requestFileSourceResponse().
511 * @param {?ExtensionHighlight} code The code to be displayed. If |code| is
512 * null, then
513 * a "Could not display code" message will be displayed instead.
515 setCode: function(code) {
516 document.querySelector(
517 '#extension-error-overlay .extension-error-overlay-title').
518 textContent = code.title;
520 this.codeDiv_.populate(
521 code,
522 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay'));
527 * Called by the ExtensionErrorHandler responding to the request for a file's
528 * source. Populate the content area of the overlay and display the overlay.
529 * @param {?ExtensionHighlight} result The three 'highlight' strings represent
530 * three portions of the file's content to display - the portion which is
531 * most relevant and should be emphasized (highlight), and the parts both
532 * before and after this portion. These may be empty.
534 ExtensionErrorOverlay.requestFileSourceResponse = function(result) {
535 var overlay = extensions.ExtensionErrorOverlay.getInstance();
536 overlay.setCode(result);
537 overlay.setVisible(true);
540 // Export
541 return {
542 ExtensionErrorOverlay: ExtensionErrorOverlay