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 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}} 12 */ 13 var StackTrace; 14 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}} 31 */ 32 var RuntimeError; 33 34 cr.define('extensions', function() { 35 'use strict'; 36 37 /** 38 * Clear all the content of a given element. 39 * @param {HTMLElement} element The element to be cleared. 40 */ 41 function clearElement(element) { 42 while (element.firstChild) 43 element.removeChild(element.firstChild); 44 } 45 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. 53 */ 54 function getRelativeUrl(url, extensionUrl) { 55 return url.substring(0, extensionUrl.length) == extensionUrl ? 56 url.substring(extensionUrl.length) : url; 57 } 58 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} 64 */ 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; 72 } 73 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 79 */ 80 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active'; 81 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). 88 */ 89 RuntimeErrorContent.shouldDisplayForUrl = function(url) { 90 // All our internal scripts are in the 'extensions::' namespace. 91 return !/^extensions::/.test(url); 92 }; 93 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 102 */ 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.'); 110 }; 111 112 RuntimeErrorContent.prototype = { 113 __proto__: HTMLDivElement.prototype, 114 115 /** 116 * The underlying error whose details are being displayed. 117 * @type {?RuntimeError} 118 * @private 119 */ 120 error_: null, 121 122 /** 123 * The URL associated with this extension, i.e. chrome-extension://<id>/. 124 * @type {?string} 125 * @private 126 */ 127 extensionUrl_: null, 128 129 /** 130 * The node of the stack trace which is currently active. 131 * @type {?HTMLElement} 132 * @private 133 */ 134 currentFrameNode_: null, 135 136 /** 137 * Initialize the RuntimeErrorContent for the first time. 138 */ 139 init: function() { 140 /** 141 * The stack trace element in the overlay. 142 * @type {HTMLElement} 143 * @private 144 */ 145 this.stackTrace_ = /** @type {HTMLElement} */( 146 this.querySelector('.extension-error-overlay-stack-trace-list')); 147 assert(this.stackTrace_); 148 149 /** 150 * The context URL element in the overlay. 151 * @type {HTMLElement} 152 * @private 153 */ 154 this.contextUrl_ = /** @type {HTMLElement} */( 155 this.querySelector('.extension-error-overlay-context-url')); 156 assert(this.contextUrl_); 157 }, 158 159 /** 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. 164 */ 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_(); 172 }, 173 174 /** 175 * Wipe content associated with a specific error. 176 */ 177 clearError: function() { 178 this.error_ = null; 179 this.extensionUrl_ = null; 180 this.currentFrameNode_ = null; 181 clearElement(this.stackTrace_); 182 this.stackTrace_.hidden = true; 183 }, 184 185 /** 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 190 */ 191 setActiveFrame_: function(frameNode) { 192 if (this.currentFrameNode_) { 193 this.currentFrameNode_.classList.remove( 194 RuntimeErrorContent.ACTIVE_CLASS_NAME); 195 } 196 197 this.currentFrameNode_ = frameNode; 198 this.currentFrameNode_.classList.add( 199 RuntimeErrorContent.ACTIVE_CLASS_NAME); 200 }, 201 202 /** 203 * Initialize the stack trace element of the overlay. 204 * @private 205 */ 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; 213 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; 218 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 + ')'; 229 } 230 frameNode.textContent = description; 231 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); 238 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)); 248 249 this.stackTrace_.appendChild(frameNode); 250 } 251 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)); 260 } 261 }, 262 263 /** 264 * Open the developer tools for the active stack frame. 265 */ 266 openDevtools: function() { 267 var stackFrame = 268 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace]; 269 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}); 276 } 277 }; 278 279 /** 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 285 */ 286 function ExtensionErrorOverlay() { 287 /** 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 292 */ 293 this.runtimeErrorContent_ = new RuntimeErrorContent(); 294 } 295 296 /** 297 * Value of ExtensionError::RUNTIME_ERROR enum. 298 * @see extensions/browser/extension_error.h 299 * @type {number} 300 * @const 301 * @private 302 */ 303 ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1; 304 305 /** 306 * The manifest filename. 307 * @type {string} 308 * @const 309 * @private 310 */ 311 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json'; 312 313 /** 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 321 */ 322 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) { 323 return file.substr(0, extensionUrl.length) == extensionUrl || 324 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_; 325 }; 326 327 /** 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. 335 */ 336 ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) { 337 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) 338 return true; 339 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; 344 } 345 } 346 347 return false; 348 }; 349 350 /** 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. 357 */ 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); 364 }); 365 } else { 366 assertNotReached('Cannot call either requestFileSource function.'); 367 } 368 }; 369 370 cr.addSingletonGetter(ExtensionErrorOverlay); 371 372 ExtensionErrorOverlay.prototype = { 373 /** 374 * The underlying error whose details are being displayed. 375 * @type {?RuntimeError} 376 * @private 377 */ 378 error_: null, 379 380 /** 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. 386 */ 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)); 392 393 $('extension-error-overlay-dismiss').addEventListener( 394 'click', this.handleDismiss_.bind(this)); 395 396 /** 397 * The element of the full overlay. 398 * @type {HTMLDivElement} 399 * @private 400 */ 401 this.overlayDiv_ = /** @type {HTMLDivElement} */( 402 $('extension-error-overlay')); 403 404 /** 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 409 */ 410 this.codeDiv_ = 411 new extensions.ExtensionCode($('extension-error-overlay-code')); 412 413 /** 414 * The function to show or hide the ExtensionErrorOverlay. 415 * @param {boolean} isVisible Whether the overlay should be visible. 416 */ 417 this.setVisible = function(isVisible) { 418 showOverlay(isVisible ? this.overlayDiv_ : null); 419 if (isVisible) 420 this.codeDiv_.scrollToError(); 421 }; 422 423 /** 424 * The button to open the developer tools (only available for runtime 425 * errors). 426 * @type {HTMLButtonElement} 427 * @private 428 */ 429 this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */( 430 $('extension-error-overlay-devtools-button')); 431 this.openDevtoolsButton_.addEventListener('click', function() { 432 this.runtimeErrorContent_.openDevtools(); 433 }.bind(this)); 434 }, 435 436 /** 437 * Handles a click on the dismiss ("OK" or close) buttons. 438 * @param {Event} e The click event. 439 * @private 440 */ 441 handleDismiss_: function(e) { 442 this.setVisible(false); 443 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; 449 450 // Remove all previous content. 451 this.codeDiv_.clear(); 452 453 this.openDevtoolsButton_.hidden = true; 454 455 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) { 456 this.overlayDiv_.querySelector('.content-area').removeChild( 457 this.runtimeErrorContent_); 458 this.runtimeErrorContent_.clearError(); 459 } 460 461 this.error_ = null; 462 }, 463 464 /** 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>". 472 */ 473 setErrorAndShowOverlay: function(error, extensionUrl) { 474 this.error_ = error; 475 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; 483 } 484 485 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) { 486 var relativeUrl = getRelativeUrl(error.source, extensionUrl); 487 488 var requestFileSourceArgs = {extensionId: error.extensionId, 489 message: error.message, 490 pathSuffix: relativeUrl}; 491 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; 500 } 501 ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs); 502 } else { 503 ExtensionErrorOverlay.requestFileSourceResponse(null); 504 } 505 }, 506 507 508 /** 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. 514 */ 515 setCode: function(code) { 516 document.querySelector( 517 '#extension-error-overlay .extension-error-overlay-title'). 518 textContent = code.title; 519 520 this.codeDiv_.populate( 521 code, 522 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay')); 523 }, 524 }; 525 526 /** 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. 533 */ 534 ExtensionErrorOverlay.requestFileSourceResponse = function(result) { 535 var overlay = extensions.ExtensionErrorOverlay.getInstance(); 536 overlay.setCode(result); 537 overlay.setVisible(true); 538 }; 539 540 // Export 541 return { 542 ExtensionErrorOverlay: ExtensionErrorOverlay 543 }; 544 }); 545