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 * 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