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 5cr.define('extensions', function() { 6 'use strict'; 7 8 /** 9 * Clear all the content of a given element. 10 * @param {HTMLElement} element The element to be cleared. 11 */ 12 function clearElement(element) { 13 while (element.firstChild) 14 element.removeChild(element.firstChild); 15 } 16 17 /** 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. 24 */ 25 function getRelativeUrl(url, extensionUrl) { 26 return url.substring(0, extensionUrl.length) == extensionUrl ? 27 url.substring(extensionUrl.length) : url; 28 } 29 30 /** 31 * The RuntimeErrorContent manages all content specifically associated with 32 * runtime errors; this includes stack frames and the context url. 33 * @constructor 34 * @extends {HTMLDivElement} 35 */ 36 function RuntimeErrorContent() { 37 var contentArea = $('template-collection-extension-error-overlay'). 38 querySelector('.extension-error-overlay-runtime-content'). 39 cloneNode(true); 40 contentArea.__proto__ = RuntimeErrorContent.prototype; 41 contentArea.init(); 42 return contentArea; 43 } 44 45 /** 46 * The name of the "active" class specific to extension errors (so as to 47 * not conflict with other rules). 48 * @type {string} 49 * @const 50 */ 51 RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active'; 52 53 /** 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). 59 */ 60 RuntimeErrorContent.shouldDisplayForUrl = function(url) { 61 // All our internal scripts are in the 'extensions::' namespace. 62 return !/^extensions::/.test(url); 63 }; 64 65 /** 66 * Send a call to chrome to open the developer tools for an error. 67 * This will call either the bound function in ExtensionErrorHandler or the 68 * API function from developerPrivate, depending on whether this is being 69 * used in the native chrome:extensions page or the Apps Developer Tool. 70 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h 71 * @param {Object} args The arguments to pass to openDevTools. 72 * @private 73 */ 74 RuntimeErrorContent.openDevtools_ = function(args) { 75 if (chrome.send) 76 chrome.send('extensionErrorOpenDevTools', [args]); 77 else if (chrome.developerPrivate) 78 chrome.developerPrivate.openDevTools(args); 79 else 80 assert(false, 'Cannot call either openDevTools function.'); 81 }; 82 83 RuntimeErrorContent.prototype = { 84 __proto__: HTMLDivElement.prototype, 85 86 /** 87 * The underlying error whose details are being displayed. 88 * @type {Object} 89 * @private 90 */ 91 error_: undefined, 92 93 /** 94 * The URL associated with this extension, i.e. chrome-extension://<id>/. 95 * @type {string} 96 * @private 97 */ 98 extensionUrl_: undefined, 99 100 /** 101 * The node of the stack trace which is currently active. 102 * @type {HTMLElement} 103 * @private 104 */ 105 currentFrameNode_: undefined, 106 107 /** 108 * Initialize the RuntimeErrorContent for the first time. 109 */ 110 init: function() { 111 /** 112 * The stack trace element in the overlay. 113 * @type {HTMLElement} 114 * @private 115 */ 116 this.stackTrace_ = 117 this.querySelector('.extension-error-overlay-stack-trace-list'); 118 assert(this.stackTrace_); 119 120 /** 121 * The context URL element in the overlay. 122 * @type {HTMLElement} 123 * @private 124 */ 125 this.contextUrl_ = 126 this.querySelector('.extension-error-overlay-context-url'); 127 assert(this.contextUrl_); 128 }, 129 130 /** 131 * Sets the error for the content. 132 * @param {Object} error The error whose content should be displayed. 133 * @param {string} extensionUrl The URL associated with this extension. 134 */ 135 setError: function(error, extensionUrl) { 136 this.error_ = error; 137 this.extensionUrl_ = extensionUrl; 138 this.contextUrl_.textContent = error.contextUrl ? 139 getRelativeUrl(error.contextUrl, this.extensionUrl_) : 140 loadTimeData.getString('extensionErrorOverlayContextUnknown'); 141 this.initStackTrace_(); 142 }, 143 144 /** 145 * Wipe content associated with a specific error. 146 */ 147 clearError: function() { 148 this.error_ = undefined; 149 this.extensionUrl_ = undefined; 150 this.currentFrameNode_ = undefined; 151 clearElement(this.stackTrace_); 152 this.stackTrace_.hidden = true; 153 }, 154 155 /** 156 * Makes |frame| active and deactivates the previously active frame (if 157 * there was one). 158 * @param {HTMLElement} frame The frame to activate. 159 * @private 160 */ 161 setActiveFrame_: function(frameNode) { 162 if (this.currentFrameNode_) { 163 this.currentFrameNode_.classList.remove( 164 RuntimeErrorContent.ACTIVE_CLASS_NAME); 165 } 166 167 this.currentFrameNode_ = frameNode; 168 this.currentFrameNode_.classList.add( 169 RuntimeErrorContent.ACTIVE_CLASS_NAME); 170 }, 171 172 /** 173 * Initialize the stack trace element of the overlay. 174 * @private 175 */ 176 initStackTrace_: function() { 177 for (var i = 0; i < this.error_.stackTrace.length; ++i) { 178 var frame = this.error_.stackTrace[i]; 179 // Don't include any internal calls (e.g., schemaBindings) in the 180 // stack trace. 181 if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url)) 182 continue; 183 184 var frameNode = document.createElement('li'); 185 // Attach the index of the frame to which this node refers (since we 186 // may skip some, this isn't a 1-to-1 match). 187 frameNode.indexIntoTrace = i; 188 189 // The description is a human-readable summation of the frame, in the 190 // form "<relative_url>:<line_number> (function)", e.g. 191 // "myfile.js:25 (myFunction)". 192 var description = getRelativeUrl(frame.url, this.extensionUrl_) + 193 ':' + frame.lineNumber; 194 if (frame.functionName) { 195 var functionName = frame.functionName == '(anonymous function)' ? 196 loadTimeData.getString('extensionErrorOverlayAnonymousFunction') : 197 frame.functionName; 198 description += ' (' + functionName + ')'; 199 } 200 frameNode.textContent = description; 201 202 // When the user clicks on a frame in the stack trace, we should 203 // highlight that overlay in the list, display the appropriate source 204 // code with the line highlighted, and link the "Open DevTools" button 205 // with that frame. 206 frameNode.addEventListener('click', function(frame, frameNode, e) { 207 if (this.currStackFrame_ == frameNode) 208 return; 209 210 this.setActiveFrame_(frameNode); 211 212 // Request the file source with the section highlighted; this will 213 // call ExtensionErrorOverlay.requestFileSourceResponse() when 214 // completed, which in turn calls setCode(). 215 ExtensionErrorOverlay.requestFileSource( 216 {extensionId: this.error_.extensionId, 217 message: this.error_.message, 218 pathSuffix: getRelativeUrl(frame.url, this.extensionUrl_), 219 lineNumber: frame.lineNumber}); 220 }.bind(this, frame, frameNode)); 221 222 this.stackTrace_.appendChild(frameNode); 223 } 224 225 // Set the current stack frame to the first stack frame and show the 226 // trace, if one exists. (We can't just check error.stackTrace, because 227 // it's possible the trace was purely internal, and we don't show 228 // internal frames.) 229 if (this.stackTrace_.children.length > 0) { 230 this.stackTrace_.hidden = false; 231 this.setActiveFrame_(this.stackTrace_.firstChild); 232 } 233 }, 234 235 /** 236 * Open the developer tools for the active stack frame. 237 */ 238 openDevtools: function() { 239 var stackFrame = 240 this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace]; 241 242 RuntimeErrorContent.openDevtools_( 243 {renderProcessId: this.error_.renderProcessId, 244 renderViewId: this.error_.renderViewId, 245 url: stackFrame.url, 246 lineNumber: stackFrame.lineNumber || 0, 247 columnNumber: stackFrame.columnNumber || 0}); 248 } 249 }; 250 251 /** 252 * The ExtensionErrorOverlay will show the contents of a file which pertains 253 * to the ExtensionError; this is either the manifest file (for manifest 254 * errors) or a source file (for runtime errors). If possible, the portion 255 * of the file which caused the error will be highlighted. 256 * @constructor 257 */ 258 function ExtensionErrorOverlay() { 259 /** 260 * The content section for runtime errors; this is re-used for all 261 * runtime errors and attached/detached from the overlay as needed. 262 * @type {RuntimeErrorContent} 263 * @private 264 */ 265 this.runtimeErrorContent_ = new RuntimeErrorContent(); 266 } 267 268 /** 269 * Value of ExtensionError::RUNTIME_ERROR enum. 270 * @see extensions/browser/extension_error.h 271 * @type {number} 272 * @const 273 * @private 274 */ 275 ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1; 276 277 /** 278 * The manifest filename. 279 * @type {string} 280 * @const 281 * @private 282 */ 283 ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json'; 284 285 /** 286 * Determine whether or not chrome can load the source for a given file; this 287 * can only be done if the file belongs to the extension. 288 * @param {string} file The file to load. 289 * @param {string} extensionUrl The url for the extension, in the form 290 * chrome-extension://<extension-id>/. 291 * @return {boolean} True if the file can be loaded, false otherwise. 292 * @private 293 */ 294 ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) { 295 return file.substr(0, extensionUrl.length) == extensionUrl || 296 file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_; 297 }; 298 299 /** 300 * Determine whether or not we can show an overlay with more details for 301 * the given extension error. 302 * @param {Object} error The extension error. 303 * @param {string} extensionUrl The url for the extension, in the form 304 * "chrome-extension://<extension-id>/". 305 * @return {boolean} True if we can show an overlay for the error, 306 * false otherwise. 307 */ 308 ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) { 309 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) 310 return true; 311 312 if (error.stackTrace) { 313 for (var i = 0; i < error.stackTrace.length; ++i) { 314 if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url)) 315 return true; 316 } 317 } 318 319 return false; 320 }; 321 322 /** 323 * Send a call to chrome to request the source of a given file. 324 * This will call either the bound function in ExtensionErrorHandler or the 325 * API function from developerPrivate, depending on whether this is being 326 * used in the native chrome:extensions page or the Apps Developer Tool. 327 * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h 328 * @param {Object} args The arguments to pass to requestFileSource. 329 */ 330 ExtensionErrorOverlay.requestFileSource = function(args) { 331 if (chrome.send) { 332 chrome.send('extensionErrorRequestFileSource', [args]); 333 } else if (chrome.developerPrivate) { 334 chrome.developerPrivate.requestFileSource(args, function(result) { 335 extensions.ExtensionErrorOverlay.requestFileSourceResponse(result); 336 }); 337 } else { 338 assert(false, 'Cannot call either requestFileSource function.'); 339 } 340 }; 341 342 cr.addSingletonGetter(ExtensionErrorOverlay); 343 344 ExtensionErrorOverlay.prototype = { 345 /** 346 * The underlying error whose details are being displayed. 347 * @type {Object} 348 * @private 349 */ 350 error_: undefined, 351 352 /** 353 * Initialize the page. 354 * @param {function(HTMLDivElement)} showOverlay The function to show or 355 * hide the ExtensionErrorOverlay; this should take a single parameter 356 * which is either the overlay Div if the overlay should be displayed, 357 * or null if the overlay should be hidden. 358 */ 359 initializePage: function(showOverlay) { 360 var overlay = $('overlay'); 361 cr.ui.overlay.setupOverlay(overlay); 362 cr.ui.overlay.globalInitialization(); 363 overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this)); 364 365 $('extension-error-overlay-dismiss').addEventListener( 366 'click', this.handleDismiss_.bind(this)); 367 368 /** 369 * The element of the full overlay. 370 * @type {HTMLDivElement} 371 * @private 372 */ 373 this.overlayDiv_ = $('extension-error-overlay'); 374 375 /** 376 * The portion of the overlay which shows the code relating to the error 377 * and the corresponding line numbers. 378 * @type {ExtensionCode} 379 * @private 380 */ 381 this.codeDiv_ = 382 new extensions.ExtensionCode($('extension-error-overlay-code')); 383 384 /** 385 * The function to show or hide the ExtensionErrorOverlay. 386 * @type {function} 387 * @param {boolean} isVisible Whether the overlay should be visible. 388 */ 389 this.setVisible = function(isVisible) { 390 showOverlay(isVisible ? this.overlayDiv_ : null); 391 if (isVisible) 392 this.codeDiv_.scrollToError(); 393 }; 394 395 /** 396 * The button to open the developer tools (only available for runtime 397 * errors). 398 * @type {HTMLButtonElement} 399 * @private 400 */ 401 this.openDevtoolsButton_ = $('extension-error-overlay-devtools-button'); 402 this.openDevtoolsButton_.addEventListener('click', function() { 403 this.runtimeErrorContent_.openDevtools(); 404 }.bind(this)); 405 }, 406 407 /** 408 * Handles a click on the dismiss ("OK" or close) buttons. 409 * @param {Event} e The click event. 410 * @private 411 */ 412 handleDismiss_: function(e) { 413 this.setVisible(false); 414 415 // There's a chance that the overlay receives multiple dismiss events; in 416 // this case, handle it gracefully and return (since all necessary work 417 // will already have been done). 418 if (!this.error_) 419 return; 420 421 // Remove all previous content. 422 this.codeDiv_.clear(); 423 424 this.openDevtoolsButton_.hidden = true; 425 426 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) { 427 this.overlayDiv_.querySelector('.content-area').removeChild( 428 this.runtimeErrorContent_); 429 this.runtimeErrorContent_.clearError(); 430 } 431 432 this.error_ = undefined; 433 }, 434 435 /** 436 * Associate an error with the overlay. This will set the error for the 437 * overlay, and, if possible, will populate the code section of the overlay 438 * with the relevant file, load the stack trace, and generate links for 439 * opening devtools (the latter two only happen for runtime errors). 440 * @param {Object} error The error to show in the overlay. 441 * @param {string} extensionUrl The URL of the extension, in the form 442 * "chrome-extension://<extension_id>". 443 */ 444 setErrorAndShowOverlay: function(error, extensionUrl) { 445 this.error_ = error; 446 447 if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) { 448 this.runtimeErrorContent_.setError(this.error_, extensionUrl); 449 this.overlayDiv_.querySelector('.content-area').insertBefore( 450 this.runtimeErrorContent_, 451 this.codeDiv_.nextSibling); 452 this.openDevtoolsButton_.hidden = false; 453 this.openDevtoolsButton_.disabled = !error.canInspect; 454 } 455 456 if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) { 457 var relativeUrl = getRelativeUrl(error.source, extensionUrl); 458 459 var requestFileSourceArgs = {extensionId: error.extensionId, 460 message: error.message, 461 pathSuffix: relativeUrl}; 462 463 if (relativeUrl.toLowerCase() == 464 ExtensionErrorOverlay.MANIFEST_FILENAME_) { 465 requestFileSourceArgs.manifestKey = error.manifestKey; 466 requestFileSourceArgs.manifestSpecific = error.manifestSpecific; 467 } else { 468 requestFileSourceArgs.lineNumber = 469 error.stackTrace && error.stackTrace[0] ? 470 error.stackTrace[0].lineNumber : 0; 471 } 472 ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs); 473 } else { 474 ExtensionErrorOverlay.requestFileSourceResponse(null); 475 } 476 }, 477 478 /** 479 * Set the code to be displayed in the code portion of the overlay. 480 * @see ExtensionErrorOverlay.requestFileSourceResponse(). 481 * @param {?Object} code The code to be displayed. If |code| is null, then 482 * a "Could not display code" message will be displayed instead. 483 */ 484 setCode: function(code) { 485 document.querySelector( 486 '#extension-error-overlay .extension-error-overlay-title'). 487 textContent = code.title; 488 489 this.codeDiv_.populate( 490 code, 491 loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay')); 492 }, 493 }; 494 495 /** 496 * Called by the ExtensionErrorHandler responding to the request for a file's 497 * source. Populate the content area of the overlay and display the overlay. 498 * @param {Object?} result An object with four strings - the title, 499 * beforeHighlight, afterHighlight, and highlight. The three 'highlight' 500 * strings represent three portions of the file's content to display - the 501 * portion which is most relevant and should be emphasized (highlight), 502 * and the parts both before and after this portion. These may be empty. 503 */ 504 ExtensionErrorOverlay.requestFileSourceResponse = function(result) { 505 var overlay = extensions.ExtensionErrorOverlay.getInstance(); 506 overlay.setCode(result); 507 overlay.setVisible(true); 508 }; 509 510 // Export 511 return { 512 ExtensionErrorOverlay: ExtensionErrorOverlay 513 }; 514}); 515