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 cr.define('extensions', function() {
      6   'use strict';
      7 
      8   /**
      9    * Returns whether or not a given |url| is associated with an extension.
     10    * @param {string} url The url to examine.
     11    * @param {string} extensionUrl The url of the extension.
     12    * @return {boolean} Whether or not the url is associated with the extension.
     13    */
     14   function isExtensionUrl(url, extensionUrl) {
     15     return url.substring(0, extensionUrl.length) == extensionUrl;
     16   }
     17 
     18   /**
     19    * Get the url relative to the main extension url. If the url is
     20    * unassociated with the extension, this will be the full url.
     21    * @param {string} url The url to make relative.
     22    * @param {string} extensionUrl The host for which the url is relative.
     23    * @return {string} The url relative to the host.
     24    */
     25   function getRelativeUrl(url, extensionUrl) {
     26     return isExtensionUrl(url, extensionUrl) ?
     27         url.substring(extensionUrl.length) : url;
     28   }
     29 
     30   /**
     31    * Clone a template within the extension error template collection.
     32    * @param {string} templateName The class name of the template to clone.
     33    * @return {HTMLElement} The clone of the template.
     34    */
     35   function cloneTemplate(templateName) {
     36     return $('template-collection-extension-error').
     37         querySelector('.' + templateName).cloneNode(true);
     38   }
     39 
     40   /**
     41    * Creates a new ExtensionError HTMLElement; this is used to show a
     42    * notification to the user when an error is caused by an extension.
     43    * @param {Object} error The error the element should represent.
     44    * @param {string} templateName The name of the template to clone for the
     45    *     error ('extension-error-[detailed|simple]-wrapper').
     46    * @constructor
     47    * @extends {HTMLDivElement}
     48    */
     49   function ExtensionError(error, templateName) {
     50     var div = cloneTemplate(templateName);
     51     div.__proto__ = ExtensionError.prototype;
     52     div.error_ = error;
     53     div.decorate();
     54     return div;
     55   }
     56 
     57   ExtensionError.prototype = {
     58     __proto__: HTMLDivElement.prototype,
     59 
     60     /** @override */
     61     decorate: function() {
     62       var metadata = cloneTemplate('extension-error-metadata');
     63 
     64       // Add an additional class for the severity level.
     65       if (this.error_.level == 0)
     66         metadata.classList.add('extension-error-severity-info');
     67       else if (this.error_.level == 1)
     68         metadata.classList.add('extension-error-severity-warning');
     69       else
     70         metadata.classList.add('extension-error-severity-fatal');
     71 
     72       var iconNode = document.createElement('img');
     73       iconNode.className = 'extension-error-icon';
     74       metadata.insertBefore(iconNode, metadata.firstChild);
     75 
     76       // Add a property for the extension's base url in order to determine if
     77       // a url belongs to the extension.
     78       this.extensionUrl_ =
     79           'chrome-extension://' + this.error_.extensionId + '/';
     80 
     81       metadata.querySelector('.extension-error-message').textContent =
     82           this.error_.message;
     83 
     84       metadata.appendChild(this.createViewSourceAndInspect_(
     85           getRelativeUrl(this.error_.source, this.extensionUrl_),
     86           this.error_.source));
     87 
     88       // The error template may specify a <summary> to put template metadata in.
     89       // If not, just append it to the top-level element.
     90       var metadataContainer = this.querySelector('summary') || this;
     91       metadataContainer.appendChild(metadata);
     92 
     93       var detailsNode = this.querySelector('.extension-error-details');
     94       if (detailsNode && this.error_.contextUrl)
     95         detailsNode.appendChild(this.createContextNode_());
     96       if (detailsNode && this.error_.stackTrace) {
     97         var stackNode = this.createStackNode_();
     98         if (stackNode)
     99           detailsNode.appendChild(this.createStackNode_());
    100       }
    101     },
    102 
    103     /**
    104      * Return a div with text |description|. If it's possible to view the source
    105      * for |url|, linkify the div to do so. Attach an inspect button if it's
    106      * possible to open the inspector for |url|.
    107      * @param {string} description a human-friendly description the location
    108      *     (e.g., filename, line).
    109      * @param {string} url The url of the resource to view.
    110      * @param {?number} line An optional line number of the resource.
    111      * @param {?number} column An optional column number of the resource.
    112      * @return {HTMLElement} The created node, either a link or plaintext.
    113      * @private
    114      */
    115     createViewSourceAndInspect_: function(description, url, line, column) {
    116       var errorLinks = document.createElement('div');
    117       errorLinks.className = 'extension-error-links';
    118 
    119       if (this.error_.canInspect)
    120         errorLinks.appendChild(this.createInspectLink_(url, line, column));
    121 
    122       if (this.canViewSource_(url))
    123         var viewSource = this.createViewSourceLink_(url, line);
    124       else
    125         var viewSource = document.createElement('div');
    126       viewSource.className = 'extension-error-view-source';
    127       viewSource.textContent = description;
    128       errorLinks.appendChild(viewSource);
    129       return errorLinks;
    130     },
    131 
    132     /**
    133      * Determine whether we can view the source of a given url.
    134      * @param {string} url The url of the resource to view.
    135      * @return {boolean} Whether or not we can view the source for the url.
    136      * @private
    137      */
    138     canViewSource_: function(url) {
    139       return isExtensionUrl(url, this.extensionUrl_) || url == 'manifest.json';
    140     },
    141 
    142     /**
    143      * Determine whether or not we should display the url to the user. We don't
    144      * want to include any of our own code in stack traces.
    145      * @param {string} url The url in question.
    146      * @return {boolean} True if the url should be displayed, and false
    147      *     otherwise (i.e., if it is an internal script).
    148      */
    149     shouldDisplayForUrl_: function(url) {
    150       var extensionsNamespace = 'extensions::';
    151       // All our internal scripts are in the 'extensions::' namespace.
    152       return url.substr(0, extensionsNamespace.length) != extensionsNamespace;
    153     },
    154 
    155     /**
    156      * Create a clickable node to view the source for the given url.
    157      * @param {string} url The url to the resource to view.
    158      * @param {?number} line An optional line number of the resource (for
    159      *     source files).
    160      * @return {HTMLElement} The clickable node to view the source.
    161      * @private
    162      */
    163     createViewSourceLink_: function(url, line) {
    164       var viewSource = document.createElement('a');
    165       viewSource.href = 'javascript:void(0)';
    166       var relativeUrl = getRelativeUrl(url, this.extensionUrl_);
    167       var requestFileSourceArgs = { 'extensionId': this.error_.extensionId,
    168                                     'message': this.error_.message,
    169                                     'pathSuffix': relativeUrl };
    170       if (relativeUrl == 'manifest.json') {
    171         requestFileSourceArgs.manifestKey = this.error_.manifestKey;
    172         requestFileSourceArgs.manifestSpecific = this.error_.manifestSpecific;
    173       } else {
    174         // Prefer |line| if available, or default to the line of the last stack
    175         // frame.
    176         requestFileSourceArgs.lineNumber =
    177             line ? line : this.getLastPosition_('lineNumber');
    178       }
    179 
    180       viewSource.addEventListener('click', function(e) {
    181         chrome.send('extensionErrorRequestFileSource', [requestFileSourceArgs]);
    182       });
    183       viewSource.title = loadTimeData.getString('extensionErrorViewSource');
    184       return viewSource;
    185     },
    186 
    187     /**
    188      * Check the most recent stack frame to get the last position in the code.
    189      * @param {string} type The position type, i.e. '[line|column]Number'.
    190      * @return {?number} The last position of the given |type|, or undefined if
    191      *     there is no stack trace to check.
    192      * @private
    193      */
    194     getLastPosition_: function(type) {
    195       var stackTrace = this.error_.stackTrace;
    196       return stackTrace && stackTrace[0] ? stackTrace[0][type] : undefined;
    197     },
    198 
    199     /**
    200      * Create an "Inspect" link, in the form of an icon.
    201      * @param {?string} url The url of the resource to inspect; if absent, the
    202      *     render view (and no particular resource) is inspected.
    203      * @param {?number} line An optional line number of the resource.
    204      * @param {?number} column An optional column number of the resource.
    205      * @return {HTMLImageElement} The created "Inspect" link for the resource.
    206      * @private
    207      */
    208     createInspectLink_: function(url, line, column) {
    209       var linkWrapper = document.createElement('a');
    210       linkWrapper.href = 'javascript:void(0)';
    211       var inspectIcon = document.createElement('img');
    212       inspectIcon.className = 'extension-error-inspect';
    213       inspectIcon.title = loadTimeData.getString('extensionErrorInspect');
    214 
    215       inspectIcon.addEventListener('click', function(e) {
    216           chrome.send('extensionErrorOpenDevTools',
    217                       [{'renderProcessId': this.error_.renderProcessId,
    218                         'renderViewId': this.error_.renderViewId,
    219                         'url': url,
    220                         'lineNumber': line ? line :
    221                             this.getLastPosition_('lineNumber'),
    222                         'columnNumber': column ? column :
    223                             this.getLastPosition_('columnNumber')}]);
    224       }.bind(this));
    225       linkWrapper.appendChild(inspectIcon);
    226       return linkWrapper;
    227     },
    228 
    229     /**
    230      * Get the context node for this error. This will attempt to link to the
    231      * context in which the error occurred, and can be either an extension page
    232      * or an external page.
    233      * @return {HTMLDivElement} The context node for the error, including the
    234      *     label and a link to the context.
    235      * @private
    236      */
    237     createContextNode_: function() {
    238       var node = cloneTemplate('extension-error-context-wrapper');
    239       var linkNode = node.querySelector('a');
    240       if (isExtensionUrl(this.error_.contextUrl, this.extensionUrl_)) {
    241         linkNode.textContent = getRelativeUrl(this.error_.contextUrl,
    242                                               this.extensionUrl_);
    243       } else {
    244         linkNode.textContent = this.error_.contextUrl;
    245       }
    246 
    247       // Prepend a link to inspect the context page, if possible.
    248       if (this.error_.canInspect)
    249         node.insertBefore(this.createInspectLink_(), linkNode);
    250 
    251       linkNode.href = this.error_.contextUrl;
    252       linkNode.target = '_blank';
    253       return node;
    254     },
    255 
    256     /**
    257      * Get a node for the stack trace for this error. Each stack frame will
    258      * include a resource url, line number, and function name (possibly
    259      * anonymous). If possible, these frames will also be linked for viewing the
    260      * source and inspection.
    261      * @return {HTMLDetailsElement} The stack trace node for this error, with
    262      *     all stack frames nested in a details-summary object.
    263      * @private
    264      */
    265     createStackNode_: function() {
    266       var node = cloneTemplate('extension-error-stack-trace');
    267       var listNode = node.querySelector('.extension-error-stack-trace-list');
    268       this.error_.stackTrace.forEach(function(frame) {
    269         if (!this.shouldDisplayForUrl_(frame.url))
    270           return;
    271         var frameNode = document.createElement('div');
    272         var description = getRelativeUrl(frame.url, this.extensionUrl_) +
    273                           ':' + frame.lineNumber;
    274         if (frame.functionName) {
    275           var functionName = frame.functionName == '(anonymous function)' ?
    276               loadTimeData.getString('extensionErrorAnonymousFunction') :
    277               frame.functionName;
    278           description += ' (' + functionName + ')';
    279         }
    280         frameNode.appendChild(this.createViewSourceAndInspect_(
    281             description, frame.url, frame.lineNumber, frame.columnNumber));
    282         listNode.appendChild(
    283             document.createElement('li')).appendChild(frameNode);
    284       }, this);
    285 
    286       if (listNode.childElementCount == 0)
    287         return undefined;
    288 
    289       return node;
    290     },
    291   };
    292 
    293   /**
    294    * A variable length list of runtime or manifest errors for a given extension.
    295    * @param {Array.<Object>} errors The list of extension errors with which
    296    *     to populate the list.
    297    * @param {string} title The i18n key for the title of the error list, i.e.
    298    *     'extensionErrors[Manifest,Runtime]Errors'.
    299    * @constructor
    300    * @extends {HTMLDivElement}
    301    */
    302   function ExtensionErrorList(errors, title) {
    303     var div = cloneTemplate('extension-error-list');
    304     div.__proto__ = ExtensionErrorList.prototype;
    305     div.errors_ = errors;
    306     div.title_ = title;
    307     div.decorate();
    308     return div;
    309   }
    310 
    311   ExtensionErrorList.prototype = {
    312     __proto__: HTMLDivElement.prototype,
    313 
    314     /**
    315      * @private
    316      * @const
    317      * @type {number}
    318      */
    319     MAX_ERRORS_TO_SHOW_: 3,
    320 
    321     /** @override */
    322     decorate: function() {
    323       this.querySelector('.extension-error-list-title').textContent =
    324           loadTimeData.getString(this.title_);
    325 
    326       this.contents_ = this.querySelector('.extension-error-list-contents');
    327       this.errors_.forEach(function(error) {
    328         this.contents_.appendChild(document.createElement('li')).appendChild(
    329             new ExtensionError(error,
    330                                error.contextUrl || error.stackTrace ?
    331                                    'extension-error-detailed-wrapper' :
    332                                    'extension-error-simple-wrapper'));
    333       }, this);
    334 
    335       if (this.contents_.children.length > this.MAX_ERRORS_TO_SHOW_) {
    336         for (var i = this.MAX_ERRORS_TO_SHOW_;
    337              i < this.contents_.children.length; ++i) {
    338           this.contents_.children[i].hidden = true;
    339         }
    340         this.initShowMoreButton_();
    341       }
    342     },
    343 
    344     /**
    345      * Initialize the "Show More" button for the error list. If there are more
    346      * than |MAX_ERRORS_TO_SHOW_| errors in the list.
    347      * @private
    348      */
    349     initShowMoreButton_: function() {
    350       var button = this.querySelector('.extension-error-list-show-more a');
    351       button.hidden = false;
    352       button.isShowingAll = false;
    353       button.addEventListener('click', function(e) {
    354         for (var i = this.MAX_ERRORS_TO_SHOW_;
    355              i < this.contents_.children.length; ++i) {
    356           this.contents_.children[i].hidden = button.isShowingAll;
    357         }
    358         var message = button.isShowingAll ? 'extensionErrorsShowMore' :
    359                                             'extensionErrorsShowFewer';
    360         button.textContent = loadTimeData.getString(message);
    361         button.isShowingAll = !button.isShowingAll;
    362       }.bind(this));
    363     }
    364   };
    365 
    366   return {
    367     ExtensionErrorList: ExtensionErrorList
    368   };
    369 });
    370