Home | History | Annotate | Download | only in extensions
      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