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 cr.define('extensions', function() {
9 * Get the url relative to the main extension url. If the url is
10 * unassociated with the extension, this will be the full url.
11 * @param {string} url The url to make relative.
12 * @param {string} extensionUrl The url for the extension resources, in the
13 * form "chrome-etxension://<extension_id>/".
14 * @return {string} The url relative to the host.
16 function getRelativeUrl(url, extensionUrl) {
17 return url.substring(0, extensionUrl.length) == extensionUrl ?
18 url.substring(extensionUrl.length) : url;
22 * The RuntimeErrorContent manages all content specifically associated with
23 * runtime errors; this includes stack frames and the context url.
25 * @extends {HTMLDivElement}
27 function RuntimeErrorContent() {
28 var contentArea = $('template-collection-extension-error-overlay').
29 querySelector('.extension-error-overlay-runtime-content').
31 contentArea.__proto__ = RuntimeErrorContent.prototype;
37 * The name of the "active" class specific to extension errors (so as to
38 * not conflict with other rules).
42 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
45 * Determine whether or not we should display the url to the user. We don't
46 * want to include any of our own code in stack traces.
47 * @param {string} url The url in question.
48 * @return {boolean} True if the url should be displayed, and false
49 * otherwise (i.e., if it is an internal script).
51 RuntimeErrorContent.shouldDisplayForUrl = function(url) {
52 // All our internal scripts are in the 'extensions::' namespace.
53 return !/^extensions::/.test(url);
57 * Send a call to chrome to open the developer tools for an error.
58 * This will call either the bound function in ExtensionErrorHandler or the
59 * API function from developerPrivate, depending on whether this is being
60 * used in the native chrome:extensions page or the Apps Developer Tool.
61 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
62 * @param {Object} args The arguments to pass to openDevTools.
65 RuntimeErrorContent.openDevtools_ = function(args) {
67 chrome.send('extensionErrorOpenDevTools', [args]);
68 else if (chrome.developerPrivate)
69 chrome.developerPrivate.openDevTools(args);
71 assert(false, 'Cannot call either openDevTools function.');
74 RuntimeErrorContent.prototype = {
75 __proto__: HTMLDivElement.prototype,
78 * The underlying error whose details are being displayed.
85 * The URL associated with this extension, i.e. chrome-extension://<id>/.
89 extensionUrl_: undefined,
92 * The node of the stack trace which is currently active.
96 currentFrameNode_: undefined,
99 * Initialize the RuntimeErrorContent for the first time.
103 * The stack trace element in the overlay.
104 * @type {HTMLElement}
108 this.querySelector('.extension-error-overlay-stack-trace-list');
109 assert(this.stackTrace_);
112 * The context URL element in the overlay.
113 * @type {HTMLElement}
117 this.querySelector('.extension-error-overlay-context-url');
118 assert(this.contextUrl_);
122 * Sets the error for the content.
123 * @param {Object} error The error whose content should be displayed.
124 * @param {string} extensionUrl The URL associated with this extension.
126 setError: function(error, extensionUrl) {
128 this.extensionUrl_ = extensionUrl;
129 this.contextUrl_.textContent = error.contextUrl ?
130 getRelativeUrl(error.contextUrl, this.extensionUrl_) :
131 loadTimeData.getString('extensionErrorOverlayContextUnknown');
132 this.initStackTrace_();
136 * Wipe content associated with a specific error.
138 clearError: function() {
139 this.error_ = undefined;
140 this.extensionUrl_ = undefined;
141 this.currentFrameNode_ = undefined;
142 this.stackTrace_.innerHTML = '';
143 this.stackTrace_.hidden = true;
147 * Makes |frame| active and deactivates the previously active frame (if
149 * @param {HTMLElement} frame The frame to activate.
152 setActiveFrame_: function(frameNode) {
153 if (this.currentFrameNode_) {
154 this.currentFrameNode_.classList.remove(
155 RuntimeErrorContent.ACTIVE_CLASS_NAME);
158 this.currentFrameNode_ = frameNode;
159 this.currentFrameNode_.classList.add(
160 RuntimeErrorContent.ACTIVE_CLASS_NAME);
164 * Initialize the stack trace element of the overlay.
167 initStackTrace_: function() {
168 for (var i = 0; i < this.error_.stackTrace.length; ++i) {
169 var frame = this.error_.stackTrace[i];
170 // Don't include any internal calls (e.g., schemaBindings) in the
172 if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
175 var frameNode = document.createElement('li');
176 // Attach the index of the frame to which this node refers (since we
177 // may skip some, this isn't a 1-to-1 match).
178 frameNode.indexIntoTrace = i;
180 // The description is a human-readable summation of the frame, in the
181 // form "<relative_url>:<line_number> (function)", e.g.
182 // "myfile.js:25 (myFunction)".
183 var description = getRelativeUrl(frame.url, this.extensionUrl_) +
184 ':' + frame.lineNumber;
185 if (frame.functionName) {
186 var functionName = frame.functionName == '(anonymous function)' ?
187 loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
189 description += ' (' + functionName + ')';
191 frameNode.textContent = description;
193 // When the user clicks on a frame in the stack trace, we should
194 // highlight that overlay in the list, display the appropriate source
195 // code with the line highlighted, and link the "Open DevTools" button
197 frameNode.addEventListener('click', function(frame, frameNode, e) {
198 if (this.currStackFrame_ == frameNode)
201 this.setActiveFrame_(frameNode);
203 // Request the file source with the section highlighted; this will
204 // call ExtensionErrorOverlay.requestFileSourceResponse() when
205 // completed, which in turn calls setCode().
206 ExtensionErrorOverlay.requestFileSource(
207 {extensionId: this.error_.extensionId,
208 message: this.error_.message,
209 pathSuffix: getRelativeUrl(frame.url, this.extensionUrl_),
210 lineNumber: frame.lineNumber});
211 }.bind(this, frame, frameNode));
213 this.stackTrace_.appendChild(frameNode);
216 // Set the current stack frame to the first stack frame and show the
217 // trace, if one exists. (We can't just check error.stackTrace, because
218 // it's possible the trace was purely internal, and we don't show
220 if (this.stackTrace_.children.length > 0) {
221 this.stackTrace_.hidden = false;
222 this.setActiveFrame_(this.stackTrace_.firstChild);
227 * Open the developer tools for the active stack frame.
229 openDevtools: function() {
231 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
233 RuntimeErrorContent.openDevtools_(
234 {renderProcessId: this.error_.renderProcessId,
235 renderViewId: this.error_.renderViewId,
237 lineNumber: stackFrame.lineNumber || 0,
238 columnNumber: stackFrame.columnNumber || 0});
243 * The ExtensionErrorOverlay will show the contents of a file which pertains
244 * to the ExtensionError; this is either the manifest file (for manifest
245 * errors) or a source file (for runtime errors). If possible, the portion
246 * of the file which caused the error will be highlighted.
249 function ExtensionErrorOverlay() {
251 * The content section for runtime errors; this is re-used for all
252 * runtime errors and attached/detached from the overlay as needed.
253 * @type {RuntimeErrorContent}
256 this.runtimeErrorContent_ = new RuntimeErrorContent();
260 * Value of ExtensionError::RUNTIME_ERROR enum.
261 * @see extensions/browser/extension_error.h
266 ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1;
269 * The manifest filename.
274 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
277 * Determine whether or not chrome can load the source for a given file; this
278 * can only be done if the file belongs to the extension.
279 * @param {string} file The file to load.
280 * @param {string} extensionUrl The url for the extension, in the form
281 * chrome-extension://<extension-id>/.
282 * @return {boolean} True if the file can be loaded, false otherwise.
285 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
286 return file.substr(0, extensionUrl.length) == extensionUrl ||
287 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
291 * Determine whether or not we can show an overlay with more details for
292 * the given extension error.
293 * @param {Object} error The extension error.
294 * @param {string} extensionUrl The url for the extension, in the form
295 * "chrome-extension://<extension-id>/".
296 * @return {boolean} True if we can show an overlay for the error,
299 ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) {
300 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl))
303 if (error.stackTrace) {
304 for (var i = 0; i < error.stackTrace.length; ++i) {
305 if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url))
314 * Send a call to chrome to request the source of a given file.
315 * This will call either the bound function in ExtensionErrorHandler or the
316 * API function from developerPrivate, depending on whether this is being
317 * used in the native chrome:extensions page or the Apps Developer Tool.
318 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
319 * @param {Object} args The arguments to pass to requestFileSource.
321 ExtensionErrorOverlay.requestFileSource = function(args) {
323 chrome.send('extensionErrorRequestFileSource', [args]);
324 } else if (chrome.developerPrivate) {
325 chrome.developerPrivate.requestFileSource(args, function(result) {
326 extensions.ExtensionErrorOverlay.requestFileSourceResponse(result);
329 assert(false, 'Cannot call either requestFileSource function.');
333 cr.addSingletonGetter(ExtensionErrorOverlay);
335 ExtensionErrorOverlay.prototype = {
337 * The underlying error whose details are being displayed.
344 * Initialize the page.
345 * @param {function(HTMLDivElement)} showOverlay The function to show or
346 * hide the ExtensionErrorOverlay; this should take a single parameter
347 * which is either the overlay Div if the overlay should be displayed,
348 * or null if the overlay should be hidden.
350 initializePage: function(showOverlay) {
351 var overlay = $('overlay');
352 cr.ui.overlay.setupOverlay(overlay);
353 cr.ui.overlay.globalInitialization();
354 overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
356 $('extension-error-overlay-dismiss').addEventListener(
357 'click', this.handleDismiss_.bind(this));
360 * The element of the full overlay.
361 * @type {HTMLDivElement}
364 this.overlayDiv_ = $('extension-error-overlay');
367 * The portion of the overlay which shows the code relating to the error.
368 * @type {HTMLElement}
371 this.codeDiv_ = $('extension-error-overlay-code');
374 * The function to show or hide the ExtensionErrorOverlay.
376 * @param {boolean} isVisible Whether the overlay should be visible.
378 this.setVisible = function(isVisible) {
379 showOverlay(isVisible ? this.overlayDiv_ : null);
383 * The button to open the developer tools (only available for runtime
385 * @type {HTMLButtonElement}
388 this.openDevtoolsButton_ = $('extension-error-overlay-devtools-button');
389 this.openDevtoolsButton_.addEventListener('click', function() {
390 this.runtimeErrorContent_.openDevtools();
395 * Handles a click on the dismiss ("OK" or close) buttons.
396 * @param {Event} e The click event.
399 handleDismiss_: function(e) {
400 this.setVisible(false);
402 // There's a chance that the overlay receives multiple dismiss events; in
403 // this case, handle it gracefully and return (since all necessary work
404 // will already have been done).
408 this.codeDiv_.innerHTML = '';
409 this.openDevtoolsButton_.hidden = true;
411 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
412 this.overlayDiv_.querySelector('.content-area').removeChild(
413 this.runtimeErrorContent_);
414 this.runtimeErrorContent_.clearError();
417 this.error_ = undefined;
421 * Associate an error with the overlay. This will set the error for the
422 * overlay, and, if possible, will populate the code section of the overlay
423 * with the relevant file, load the stack trace, and generate links for
424 * opening devtools (the latter two only happen for runtime errors).
425 * @param {Object} error The error to show in the overlay.
426 * @param {string} extensionUrl The URL of the extension, in the form
427 * "chrome-extension://<extension_id>".
429 setErrorAndShowOverlay: function(error, extensionUrl) {
432 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
433 this.runtimeErrorContent_.setError(this.error_, extensionUrl);
434 this.overlayDiv_.querySelector('.content-area').insertBefore(
435 this.runtimeErrorContent_,
436 this.codeDiv_.nextSibling);
437 this.openDevtoolsButton_.hidden = false;
438 this.openDevtoolsButton_.disabled = !error.canInspect;
441 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
442 var relativeUrl = getRelativeUrl(error.source, extensionUrl);
444 var requestFileSourceArgs = {extensionId: error.extensionId,
445 message: error.message,
446 pathSuffix: relativeUrl};
448 if (relativeUrl.toLowerCase() ==
449 ExtensionErrorOverlay.MANIFEST_FILENAME_) {
450 requestFileSourceArgs.manifestKey = error.manifestKey;
451 requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
453 requestFileSourceArgs.lineNumber =
454 error.stackTrace && error.stackTrace[0] ?
455 error.stackTrace[0].lineNumber : 0;
457 ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs);
459 ExtensionErrorOverlay.requestFileSourceResponse(null);
464 * Set the code to be displayed in the code portion of the overlay.
465 * @see ExtensionErrorOverlay.requestFileSourceResponse().
466 * @param {?Object} code The code to be displayed. If |code| is null, then
467 * a "Could not display code" message will be displayed instead.
469 setCode: function(code) {
470 document.querySelector(
471 '#extension-error-overlay .extension-error-overlay-title').
472 textContent = code.title;
473 this.codeDiv_.innerHTML = '';
475 // If there's no code, then display an appropriate message.
477 var span = document.createElement('span');
479 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay');
480 this.codeDiv_.appendChild(span);
484 var createSpan = function(source, isHighlighted) {
485 var span = document.createElement('span');
486 span.className = isHighlighted ? 'highlighted-source' : 'normal-source';
487 source = source.replace(/ /g, ' ').replace(/\n|\r/g, '<br>');
488 span.innerHTML = source;
492 if (code.beforeHighlight)
493 this.codeDiv_.appendChild(createSpan(code.beforeHighlight, false));
495 if (code.highlight) {
496 var highlightSpan = createSpan(code.highlight, true);
497 highlightSpan.title = code.message;
498 this.codeDiv_.appendChild(highlightSpan);
501 if (code.afterHighlight)
502 this.codeDiv_.appendChild(createSpan(code.afterHighlight, false));
507 * Called by the ExtensionErrorHandler responding to the request for a file's
508 * source. Populate the content area of the overlay and display the overlay.
509 * @param {Object?} result An object with four strings - the title,
510 * beforeHighlight, afterHighlight, and highlight. The three 'highlight'
511 * strings represent three portions of the file's content to display - the
512 * portion which is most relevant and should be emphasized (highlight),
513 * and the parts both before and after this portion. These may be empty.
515 ExtensionErrorOverlay.requestFileSourceResponse = function(result) {
516 var overlay = extensions.ExtensionErrorOverlay.getInstance();
517 overlay.setCode(result);
518 overlay.setVisible(true);
523 ExtensionErrorOverlay: ExtensionErrorOverlay