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 * Clear all the content of a given element.
10 * @param {HTMLElement} element The element to be cleared.
12 function clearElement(element) {
13 while (element.firstChild)
14 element.removeChild(element.firstChild);
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.
25 function getRelativeUrl(url, extensionUrl) {
26 return url.substring(0, extensionUrl.length) == extensionUrl ?
27 url.substring(extensionUrl.length) : url;
31 * The RuntimeErrorContent manages all content specifically associated with
32 * runtime errors; this includes stack frames and the context url.
34 * @extends {HTMLDivElement}
36 function RuntimeErrorContent() {
37 var contentArea = $('template-collection-extension-error-overlay').
38 querySelector('.extension-error-overlay-runtime-content').
40 contentArea.__proto__ = RuntimeErrorContent.prototype;
46 * The name of the "active" class specific to extension errors (so as to
47 * not conflict with other rules).
51 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
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).
60 RuntimeErrorContent.shouldDisplayForUrl = function(url) {
61 // All our internal scripts are in the 'extensions::' namespace.
62 return !/^extensions::/.test(url);
65 RuntimeErrorContent.prototype = {
66 __proto__: HTMLDivElement.prototype,
69 * The underlying error whose details are being displayed.
70 * @type {?(RuntimeError|ManifestError)}
76 * The URL associated with this extension, i.e. chrome-extension://<id>/.
83 * The node of the stack trace which is currently active.
84 * @type {?HTMLElement}
87 currentFrameNode_: null,
90 * Initialize the RuntimeErrorContent for the first time.
94 * The stack trace element in the overlay.
98 this.stackTrace_ = /** @type {HTMLElement} */(
99 this.querySelector('.extension-error-overlay-stack-trace-list'));
100 assert(this.stackTrace_);
103 * The context URL element in the overlay.
104 * @type {HTMLElement}
107 this.contextUrl_ = /** @type {HTMLElement} */(
108 this.querySelector('.extension-error-overlay-context-url'));
109 assert(this.contextUrl_);
113 * Sets the error for the content.
114 * @param {(RuntimeError|ManifestError)} error The error whose content
115 * should be displayed.
116 * @param {string} extensionUrl The URL associated with this extension.
118 setError: function(error, extensionUrl) {
122 this.extensionUrl_ = extensionUrl;
123 this.contextUrl_.textContent = error.contextUrl ?
124 getRelativeUrl(error.contextUrl, this.extensionUrl_) :
125 loadTimeData.getString('extensionErrorOverlayContextUnknown');
126 this.initStackTrace_();
130 * Wipe content associated with a specific error.
132 clearError: function() {
134 this.extensionUrl_ = null;
135 this.currentFrameNode_ = null;
136 clearElement(this.stackTrace_);
137 this.stackTrace_.hidden = true;
141 * Makes |frame| active and deactivates the previously active frame (if
143 * @param {HTMLElement} frameNode The frame to activate.
146 setActiveFrame_: function(frameNode) {
147 if (this.currentFrameNode_) {
148 this.currentFrameNode_.classList.remove(
149 RuntimeErrorContent.ACTIVE_CLASS_NAME);
152 this.currentFrameNode_ = frameNode;
153 this.currentFrameNode_.classList.add(
154 RuntimeErrorContent.ACTIVE_CLASS_NAME);
158 * Initialize the stack trace element of the overlay.
161 initStackTrace_: function() {
162 for (var i = 0; i < this.error_.stackTrace.length; ++i) {
163 var frame = this.error_.stackTrace[i];
164 // Don't include any internal calls (e.g., schemaBindings) in the
166 if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
169 var frameNode = document.createElement('li');
170 // Attach the index of the frame to which this node refers (since we
171 // may skip some, this isn't a 1-to-1 match).
172 frameNode.indexIntoTrace = i;
174 // The description is a human-readable summation of the frame, in the
175 // form "<relative_url>:<line_number> (function)", e.g.
176 // "myfile.js:25 (myFunction)".
177 var description = getRelativeUrl(frame.url,
178 assert(this.extensionUrl_)) + ':' + frame.lineNumber;
179 if (frame.functionName) {
180 var functionName = frame.functionName == '(anonymous function)' ?
181 loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
183 description += ' (' + functionName + ')';
185 frameNode.textContent = description;
187 // When the user clicks on a frame in the stack trace, we should
188 // highlight that overlay in the list, display the appropriate source
189 // code with the line highlighted, and link the "Open DevTools" button
191 frameNode.addEventListener('click', function(frame, frameNode, e) {
192 this.setActiveFrame_(frameNode);
194 // Request the file source with the section highlighted.
195 extensions.ExtensionErrorOverlay.getInstance().requestFileSource(
196 {extensionId: this.error_.extensionId,
197 message: this.error_.message,
198 pathSuffix: getRelativeUrl(frame.url,
199 assert(this.extensionUrl_)),
200 lineNumber: frame.lineNumber});
201 }.bind(this, frame, frameNode));
203 this.stackTrace_.appendChild(frameNode);
206 // Set the current stack frame to the first stack frame and show the
207 // trace, if one exists. (We can't just check error.stackTrace, because
208 // it's possible the trace was purely internal, and we don't show
210 if (this.stackTrace_.children.length > 0) {
211 this.stackTrace_.hidden = false;
212 this.setActiveFrame_(assertInstanceof(this.stackTrace_.firstChild,
218 * Open the developer tools for the active stack frame.
220 openDevtools: function() {
222 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
224 chrome.developerPrivate.openDevTools(
225 {renderProcessId: this.error_.renderProcessId || -1,
226 renderViewId: this.error_.renderViewId || -1,
228 lineNumber: stackFrame.lineNumber || 0,
229 columnNumber: stackFrame.columnNumber || 0});
234 * The ExtensionErrorOverlay will show the contents of a file which pertains
235 * to the ExtensionError; this is either the manifest file (for manifest
236 * errors) or a source file (for runtime errors). If possible, the portion
237 * of the file which caused the error will be highlighted.
240 function ExtensionErrorOverlay() {
242 * The content section for runtime errors; this is re-used for all
243 * runtime errors and attached/detached from the overlay as needed.
244 * @type {RuntimeErrorContent}
247 this.runtimeErrorContent_ = new RuntimeErrorContent();
251 * The manifest filename.
256 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
259 * Determine whether or not chrome can load the source for a given file; this
260 * can only be done if the file belongs to the extension.
261 * @param {string} file The file to load.
262 * @param {string} extensionUrl The url for the extension, in the form
263 * chrome-extension://<extension-id>/.
264 * @return {boolean} True if the file can be loaded, false otherwise.
267 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
268 return file.substr(0, extensionUrl.length) == extensionUrl ||
269 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
272 cr.addSingletonGetter(ExtensionErrorOverlay);
274 ExtensionErrorOverlay.prototype = {
276 * The underlying error whose details are being displayed.
277 * @type {?(RuntimeError|ManifestError)}
280 selectedError_: null,
283 * Initialize the page.
284 * @param {function(HTMLDivElement)} showOverlay The function to show or
285 * hide the ExtensionErrorOverlay; this should take a single parameter
286 * which is either the overlay Div if the overlay should be displayed,
287 * or null if the overlay should be hidden.
289 initializePage: function(showOverlay) {
290 var overlay = $('overlay');
291 cr.ui.overlay.setupOverlay(overlay);
292 cr.ui.overlay.globalInitialization();
293 overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
295 $('extension-error-overlay-dismiss').addEventListener('click',
297 cr.dispatchSimpleEvent(overlay, 'cancelOverlay');
301 * The element of the full overlay.
302 * @type {HTMLDivElement}
305 this.overlayDiv_ = /** @type {HTMLDivElement} */(
306 $('extension-error-overlay'));
309 * The portion of the overlay which shows the code relating to the error
310 * and the corresponding line numbers.
311 * @type {extensions.ExtensionCode}
315 new extensions.ExtensionCode($('extension-error-overlay-code'));
318 * The function to show or hide the ExtensionErrorOverlay.
319 * @param {boolean} isVisible Whether the overlay should be visible.
321 this.setVisible = function(isVisible) {
322 showOverlay(isVisible ? this.overlayDiv_ : null);
324 this.codeDiv_.scrollToError();
328 * The button to open the developer tools (only available for runtime
330 * @type {HTMLButtonElement}
333 this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */(
334 $('extension-error-overlay-devtools-button'));
335 this.openDevtoolsButton_.addEventListener('click', function() {
336 this.runtimeErrorContent_.openDevtools();
341 * Handles a click on the dismiss ("OK" or close) buttons.
342 * @param {Event} e The click event.
345 handleDismiss_: function(e) {
346 this.setVisible(false);
348 // There's a chance that the overlay receives multiple dismiss events; in
349 // this case, handle it gracefully and return (since all necessary work
350 // will already have been done).
351 if (!this.selectedError_)
354 // Remove all previous content.
355 this.codeDiv_.clear();
357 this.overlayDiv_.querySelector('.extension-error-list').onRemoved();
359 this.clearRuntimeContent_();
361 this.selectedError_ = null;
365 * Clears the current content.
368 clearRuntimeContent_: function() {
369 if (this.runtimeErrorContent_.parentNode) {
370 this.runtimeErrorContent_.parentNode.removeChild(
371 this.runtimeErrorContent_);
372 this.runtimeErrorContent_.clearError();
374 this.openDevtoolsButton_.hidden = true;
378 * Sets the active error for the overlay.
379 * @param {?(ManifestError|RuntimeError)} error The error to make active.
380 * TODO(dbeam): add URL externs and re-enable typechecking in this method.
381 * @suppress {missingProperties}
384 setActiveError_: function(error) {
385 this.selectedError_ = error;
387 // If there is no error (this can happen if, e.g., the user deleted all
388 // the errors), then clear the content.
390 this.codeDiv_.populate(
391 null, loadTimeData.getString('extensionErrorNoErrorsCodeMessage'));
392 this.clearRuntimeContent_();
396 var extensionUrl = 'chrome-extension://' + error.extensionId + '/';
397 // Set or hide runtime content.
398 if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) {
399 this.runtimeErrorContent_.setError(error, extensionUrl);
400 this.overlayDiv_.querySelector('.content-area').insertBefore(
401 this.runtimeErrorContent_,
402 this.codeDiv_.nextSibling);
403 this.openDevtoolsButton_.hidden = false;
404 this.openDevtoolsButton_.disabled = !error.canInspect;
406 this.clearRuntimeContent_();
409 // Read the file source to populate the code section, or set it to null if
410 // the file is unreadable.
411 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
412 // Use pathname instead of relativeUrl.
413 var requestFileSourceArgs = {extensionId: error.extensionId,
414 message: error.message};
415 switch (error.type) {
416 case chrome.developerPrivate.ErrorType.MANIFEST:
417 requestFileSourceArgs.pathSuffix = error.source;
418 requestFileSourceArgs.manifestKey = error.manifestKey;
419 requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
421 case chrome.developerPrivate.ErrorType.RUNTIME:
422 // slice(1) because pathname starts with a /.
423 var pathname = new URL(error.source).pathname.slice(1);
424 requestFileSourceArgs.pathSuffix = pathname;
425 requestFileSourceArgs.lineNumber =
426 error.stackTrace && error.stackTrace[0] ?
427 error.stackTrace[0].lineNumber : 0;
432 this.requestFileSource(requestFileSourceArgs);
434 this.onFileSourceResponse_(null);
439 * Associate an error with the overlay. This will set the error for the
440 * overlay, and, if possible, will populate the code section of the overlay
441 * with the relevant file, load the stack trace, and generate links for
442 * opening devtools (the latter two only happen for runtime errors).
443 * @param {Array<(RuntimeError|ManifestError)>} errors The error to show in
445 * @param {string} extensionId The id of the extension.
446 * @param {string} extensionName The name of the extension.
448 setErrorsAndShowOverlay: function(errors, extensionId, extensionName) {
449 document.querySelector(
450 '#extension-error-overlay .extension-error-overlay-title').
451 textContent = extensionName;
452 var errorsDiv = this.overlayDiv_.querySelector('.extension-error-list');
453 var extensionErrors =
454 new extensions.ExtensionErrorList(errors, extensionId);
455 errorsDiv.parentNode.replaceChild(extensionErrors, errorsDiv);
456 extensionErrors.addEventListener('activeExtensionErrorChanged',
458 this.setActiveError_(e.detail);
461 if (errors.length > 0)
462 this.setActiveError_(errors[0]);
463 this.setVisible(true);
467 * Requests a file's source.
468 * @param {RequestFileSourceProperties} args The arguments for the call.
470 requestFileSource: function(args) {
471 chrome.developerPrivate.requestFileSource(
472 args, this.onFileSourceResponse_.bind(this));
476 * Set the code to be displayed in the code portion of the overlay.
477 * @see ExtensionErrorOverlay.requestFileSourceResponse().
478 * @param {?RequestFileSourceResponse} response The response from the
479 * request file source call, which will be shown as code. If |response|
480 * is null, then a "Could not display code" message will be displayed
483 onFileSourceResponse_: function(response) {
484 this.codeDiv_.populate(
485 response, // ExtensionCode can handle a null response.
486 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay'));
487 this.setVisible(true);
493 ExtensionErrorOverlay: ExtensionErrorOverlay