// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * Returns whether or not a given |url| is associated with an extension. * @param {string} url The url to examine. * @param {string} extensionUrl The url of the extension. * @return {boolean} Whether or not the url is associated with the extension. */ function isExtensionUrl(url, extensionUrl) { return url.substring(0, extensionUrl.length) == extensionUrl; } /** * Get the url relative to the main extension url. If the url is * unassociated with the extension, this will be the full url. * @param {string} url The url to make relative. * @param {string} extensionUrl The host for which the url is relative. * @return {string} The url relative to the host. */ function getRelativeUrl(url, extensionUrl) { return isExtensionUrl(url, extensionUrl) ? url.substring(extensionUrl.length) : url; } /** * Clone a template within the extension error template collection. * @param {string} templateName The class name of the template to clone. * @return {HTMLElement} The clone of the template. */ function cloneTemplate(templateName) { return $('template-collection-extension-error'). querySelector('.' + templateName).cloneNode(true); } /** * Creates a new ExtensionError HTMLElement; this is used to show a * notification to the user when an error is caused by an extension. * @param {Object} error The error the element should represent. * @param {string} templateName The name of the template to clone for the * error ('extension-error-[detailed|simple]-wrapper'). * @constructor * @extends {HTMLDivElement} */ function ExtensionError(error, templateName) { var div = cloneTemplate(templateName); div.__proto__ = ExtensionError.prototype; div.error_ = error; div.decorate(); return div; } ExtensionError.prototype = { __proto__: HTMLDivElement.prototype, /** @override */ decorate: function() { var metadata = cloneTemplate('extension-error-metadata'); // Add an additional class for the severity level. if (this.error_.level == 0) metadata.classList.add('extension-error-severity-info'); else if (this.error_.level == 1) metadata.classList.add('extension-error-severity-warning'); else metadata.classList.add('extension-error-severity-fatal'); var iconNode = document.createElement('img'); iconNode.className = 'extension-error-icon'; metadata.insertBefore(iconNode, metadata.firstChild); // Add a property for the extension's base url in order to determine if // a url belongs to the extension. this.extensionUrl_ = 'chrome-extension://' + this.error_.extensionId + '/'; metadata.querySelector('.extension-error-message').textContent = this.error_.message; metadata.appendChild(this.createViewSourceAndInspect_( getRelativeUrl(this.error_.source, this.extensionUrl_), this.error_.source)); // The error template may specify a to put template metadata in. // If not, just append it to the top-level element. var metadataContainer = this.querySelector('summary') || this; metadataContainer.appendChild(metadata); var detailsNode = this.querySelector('.extension-error-details'); if (detailsNode && this.error_.contextUrl) detailsNode.appendChild(this.createContextNode_()); if (detailsNode && this.error_.stackTrace) { var stackNode = this.createStackNode_(); if (stackNode) detailsNode.appendChild(this.createStackNode_()); } }, /** * Return a div with text |description|. If it's possible to view the source * for |url|, linkify the div to do so. Attach an inspect button if it's * possible to open the inspector for |url|. * @param {string} description a human-friendly description the location * (e.g., filename, line). * @param {string} url The url of the resource to view. * @param {?number} line An optional line number of the resource. * @param {?number} column An optional column number of the resource. * @return {HTMLElement} The created node, either a link or plaintext. * @private */ createViewSourceAndInspect_: function(description, url, line, column) { var errorLinks = document.createElement('div'); errorLinks.className = 'extension-error-links'; if (this.error_.canInspect) errorLinks.appendChild(this.createInspectLink_(url, line, column)); if (this.canViewSource_(url)) var viewSource = this.createViewSourceLink_(url, line); else var viewSource = document.createElement('div'); viewSource.className = 'extension-error-view-source'; viewSource.textContent = description; errorLinks.appendChild(viewSource); return errorLinks; }, /** * Determine whether we can view the source of a given url. * @param {string} url The url of the resource to view. * @return {boolean} Whether or not we can view the source for the url. * @private */ canViewSource_: function(url) { return isExtensionUrl(url, this.extensionUrl_) || url == 'manifest.json'; }, /** * Determine whether or not we should display the url to the user. We don't * want to include any of our own code in stack traces. * @param {string} url The url in question. * @return {boolean} True if the url should be displayed, and false * otherwise (i.e., if it is an internal script). */ shouldDisplayForUrl_: function(url) { var extensionsNamespace = 'extensions::'; // All our internal scripts are in the 'extensions::' namespace. return url.substr(0, extensionsNamespace.length) != extensionsNamespace; }, /** * Create a clickable node to view the source for the given url. * @param {string} url The url to the resource to view. * @param {?number} line An optional line number of the resource (for * source files). * @return {HTMLElement} The clickable node to view the source. * @private */ createViewSourceLink_: function(url, line) { var viewSource = document.createElement('a'); viewSource.href = 'javascript:void(0)'; var relativeUrl = getRelativeUrl(url, this.extensionUrl_); var requestFileSourceArgs = { 'extensionId': this.error_.extensionId, 'message': this.error_.message, 'pathSuffix': relativeUrl }; if (relativeUrl == 'manifest.json') { requestFileSourceArgs.manifestKey = this.error_.manifestKey; requestFileSourceArgs.manifestSpecific = this.error_.manifestSpecific; } else { // Prefer |line| if available, or default to the line of the last stack // frame. requestFileSourceArgs.lineNumber = line ? line : this.getLastPosition_('lineNumber'); } viewSource.addEventListener('click', function(e) { chrome.send('extensionErrorRequestFileSource', [requestFileSourceArgs]); }); viewSource.title = loadTimeData.getString('extensionErrorViewSource'); return viewSource; }, /** * Check the most recent stack frame to get the last position in the code. * @param {string} type The position type, i.e. '[line|column]Number'. * @return {?number} The last position of the given |type|, or undefined if * there is no stack trace to check. * @private */ getLastPosition_: function(type) { var stackTrace = this.error_.stackTrace; return stackTrace && stackTrace[0] ? stackTrace[0][type] : undefined; }, /** * Create an "Inspect" link, in the form of an icon. * @param {?string} url The url of the resource to inspect; if absent, the * render view (and no particular resource) is inspected. * @param {?number} line An optional line number of the resource. * @param {?number} column An optional column number of the resource. * @return {HTMLImageElement} The created "Inspect" link for the resource. * @private */ createInspectLink_: function(url, line, column) { var linkWrapper = document.createElement('a'); linkWrapper.href = 'javascript:void(0)'; var inspectIcon = document.createElement('img'); inspectIcon.className = 'extension-error-inspect'; inspectIcon.title = loadTimeData.getString('extensionErrorInspect'); inspectIcon.addEventListener('click', function(e) { chrome.send('extensionErrorOpenDevTools', [{'renderProcessId': this.error_.renderProcessId, 'renderViewId': this.error_.renderViewId, 'url': url, 'lineNumber': line ? line : this.getLastPosition_('lineNumber'), 'columnNumber': column ? column : this.getLastPosition_('columnNumber')}]); }.bind(this)); linkWrapper.appendChild(inspectIcon); return linkWrapper; }, /** * Get the context node for this error. This will attempt to link to the * context in which the error occurred, and can be either an extension page * or an external page. * @return {HTMLDivElement} The context node for the error, including the * label and a link to the context. * @private */ createContextNode_: function() { var node = cloneTemplate('extension-error-context-wrapper'); var linkNode = node.querySelector('a'); if (isExtensionUrl(this.error_.contextUrl, this.extensionUrl_)) { linkNode.textContent = getRelativeUrl(this.error_.contextUrl, this.extensionUrl_); } else { linkNode.textContent = this.error_.contextUrl; } // Prepend a link to inspect the context page, if possible. if (this.error_.canInspect) node.insertBefore(this.createInspectLink_(), linkNode); linkNode.href = this.error_.contextUrl; linkNode.target = '_blank'; return node; }, /** * Get a node for the stack trace for this error. Each stack frame will * include a resource url, line number, and function name (possibly * anonymous). If possible, these frames will also be linked for viewing the * source and inspection. * @return {HTMLDetailsElement} The stack trace node for this error, with * all stack frames nested in a details-summary object. * @private */ createStackNode_: function() { var node = cloneTemplate('extension-error-stack-trace'); var listNode = node.querySelector('.extension-error-stack-trace-list'); this.error_.stackTrace.forEach(function(frame) { if (!this.shouldDisplayForUrl_(frame.url)) return; var frameNode = document.createElement('div'); var description = getRelativeUrl(frame.url, this.extensionUrl_) + ':' + frame.lineNumber; if (frame.functionName) { var functionName = frame.functionName == '(anonymous function)' ? loadTimeData.getString('extensionErrorAnonymousFunction') : frame.functionName; description += ' (' + functionName + ')'; } frameNode.appendChild(this.createViewSourceAndInspect_( description, frame.url, frame.lineNumber, frame.columnNumber)); listNode.appendChild( document.createElement('li')).appendChild(frameNode); }, this); if (listNode.childElementCount == 0) return undefined; return node; }, }; /** * A variable length list of runtime or manifest errors for a given extension. * @param {Array.} errors The list of extension errors with which * to populate the list. * @param {string} title The i18n key for the title of the error list, i.e. * 'extensionErrors[Manifest,Runtime]Errors'. * @constructor * @extends {HTMLDivElement} */ function ExtensionErrorList(errors, title) { var div = cloneTemplate('extension-error-list'); div.__proto__ = ExtensionErrorList.prototype; div.errors_ = errors; div.title_ = title; div.decorate(); return div; } ExtensionErrorList.prototype = { __proto__: HTMLDivElement.prototype, /** * @private * @const * @type {number} */ MAX_ERRORS_TO_SHOW_: 3, /** @override */ decorate: function() { this.querySelector('.extension-error-list-title').textContent = loadTimeData.getString(this.title_); this.contents_ = this.querySelector('.extension-error-list-contents'); this.errors_.forEach(function(error) { this.contents_.appendChild(document.createElement('li')).appendChild( new ExtensionError(error, error.contextUrl || error.stackTrace ? 'extension-error-detailed-wrapper' : 'extension-error-simple-wrapper')); }, this); if (this.contents_.children.length > this.MAX_ERRORS_TO_SHOW_) { for (var i = this.MAX_ERRORS_TO_SHOW_; i < this.contents_.children.length; ++i) { this.contents_.children[i].hidden = true; } this.initShowMoreButton_(); } }, /** * Initialize the "Show More" button for the error list. If there are more * than |MAX_ERRORS_TO_SHOW_| errors in the list. * @private */ initShowMoreButton_: function() { var button = this.querySelector('.extension-error-list-show-more a'); button.hidden = false; button.isShowingAll = false; button.addEventListener('click', function(e) { for (var i = this.MAX_ERRORS_TO_SHOW_; i < this.contents_.children.length; ++i) { this.contents_.children[i].hidden = button.isShowingAll; } var message = button.isShowingAll ? 'extensionErrorsShowMore' : 'extensionErrorsShowFewer'; button.textContent = loadTimeData.getString(message); button.isShowingAll = !button.isShowingAll; }.bind(this)); } }; return { ExtensionErrorList: ExtensionErrorList }; });