blob: 0b2efef4ef7a0933e5b435c9e4a82abd504e0bde [file] [log] [blame]
// 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 <summary> 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.<Object>} 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
};
});