Validate CSL styles on installation, and restructure Zotero.Styles.install() to use Q.

Closes https://www.zotero.org/trac/ticket/1681, automatic CSL validation
This commit is contained in:
Simon Kornblith 2012-07-10 02:46:57 -04:00
parent bf4c5c1158
commit 06825c4767
3 changed files with 210 additions and 166 deletions

View file

@ -30,7 +30,10 @@
*/ */
Zotero.Styles = new function() { Zotero.Styles = new function() {
var _initialized = false; var _initialized = false;
var _styles, _visibleStyles; var _styles, _visibleStyles, _cacheTranslatorData;
Components.utils.import("resource://zotero/q.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
this.xsltProcessor = null; this.xsltProcessor = null;
this.ios = Components.classes["@mozilla.org/network/io-service;1"]. this.ios = Components.classes["@mozilla.org/network/io-service;1"].
@ -39,6 +42,7 @@ Zotero.Styles = new function() {
this.ns = { this.ns = {
"csl":"http://purl.org/net/xbiblio/csl" "csl":"http://purl.org/net/xbiblio/csl"
}; };
/** /**
* Initializes styles cache, loading metadata for styles into memory * Initializes styles cache, loading metadata for styles into memory
@ -50,7 +54,7 @@ Zotero.Styles = new function() {
_styles = {}; _styles = {};
_visibleStyles = []; _visibleStyles = [];
this.cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData"); _cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
this.lastCSL = null; this.lastCSL = null;
// main dir // main dir
@ -134,7 +138,7 @@ Zotero.Styles = new function() {
* @return {Zotero.Style[]} An array of Zotero.Style objects * @return {Zotero.Style[]} An array of Zotero.Style objects
*/ */
this.getVisible = function() { this.getVisible = function() {
if(!_initialized || !this.cacheTranslatorData) this.init(); if(!_initialized || !_cacheTranslatorData) this.init();
return _visibleStyles.slice(0); return _visibleStyles.slice(0);
} }
@ -143,198 +147,234 @@ Zotero.Styles = new function() {
* @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects * @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects
*/ */
this.getAll = function() { this.getAll = function() {
if(!_initialized || !this.cacheTranslatorData) this.init(); if(!_initialized || !_cacheTranslatorData) this.init();
return _styles; return _styles;
} }
/** /**
* Installs a style file * Validates a style
* @param {String|nsIFile} style An nsIFile representing a style on disk, or a string containing * @param {String} style The style, as a string
* the style data * @return {Promise} A promise representing the style file. This promise is rejected
* @param {String} loadURI The URI this style file was loaded from * with the validation error if validation fails, or resolved if it is not.
* @param {Boolean} hidden Whether style is to be hidden. If this parameter is true, UI alerts
* are silenced as well
*/ */
this.install = function(style, loadURI, hidden) { this.validate = function(style) {
const pathRe = /[^\/]+$/; var deferred = Q.defer(),
worker = new Worker("resource://zotero/csl-validator.js");
if(!_initialized || !this.cacheTranslatorData) this.init(); worker.onmessage = function(event) {
if(event.data) {
// handle nsIFiles deferred.reject(event.data);
var styleFile = null; } else {
deferred.resolve();
}
};
worker.postMessage(style);
return deferred.promise;
}
/**
* Installs a style file, getting the contents of an nsIFile and showing appropriate
* error messages
* @param {String|nsIFile} style An nsIFile representing a style on disk, or a string
* containing the style data
* @param {String} origin The origin of the style, either a filename or URL, to be
* displayed in dialogs referencing the style
*/
this.install = function(style, origin) {
var styleFile = null, styleInstalled;
if(style instanceof Components.interfaces.nsIFile) { if(style instanceof Components.interfaces.nsIFile) {
styleFile = style; // handle nsIFiles
loadURI = style.leafName; origin = style.leafName;
style = Zotero.File.getContents(styleFile); styleInstalled = Zotero.File.getContentsAsync(styleFile).when(function(style) {
return _install(style, origin);
});
} else {
styleInstalled = _install(style, origin);
} }
var error = false; styleInstalled.fail(function(error) {
try { // Unless user cancelled, show an alert with the error
// CSL if(error instanceof Zotero.Exception.UserCancelled) return;
if(error instanceof Zotero.Exception.Alert) {
error.present();
error.log();
} else {
Zotero.logError(error);
(new Zotero.Exception.Alert("styles.install.unexpectedError",
origin, "styles.install.title", error)).present();
}
});
}
/**
* Installs a style
* @param {String} style The style as a string
* @param {String} origin The origin of the style, either a filename or URL, to be
* displayed in dialogs referencing the style
* @param {Boolean} [hidden] Whether style is to be hidden.
* @return {Promise}
*/
function _install(style, origin, hidden) {
if(!_initialized || !_cacheTranslatorData) Zotero.Styles.init();
var existingFile, destFile, source;
return Q.fcall(function() {
// First, parse style and make sure it's valid XML
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser), .createInstance(Components.interfaces.nsIDOMParser),
doc = parser.parseFromString(style, "application/xml"); doc = parser.parseFromString(style, "application/xml");
if(doc.documentElement.localName === "parsererror") {
throw new Error("File is not valid XML"); var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
Zotero.Styles.ns),
// Get file name from URL
m = /[^\/]+$/.exec(styleID),
fileName = Zotero.File.getValidFileName(m ? m[0] : styleID),
title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
Zotero.Styles.ns);
if(!styleID || !title) {
// If it's not valid XML, we'll return a promise that immediately resolves
// to an error
throw new Zotero.Exception.Alert("styles.installError", origin,
"styles.install.title", "Style is not valid XML, or the styleID or title is missing");
} }
} catch(e) {
error = e; // look for a parent
} source = Zotero.Utilities.xpathText(doc,
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
if(!doc || error) { Zotero.Styles.ns);
if(!hidden) alert(Zotero.getString('styles.installError', loadURI)); if(source == styleID) {
if(error) throw error; throw Zotero.Exception.Alert("styles.installError", origin,
return false; "styles.install.title", "Style references itself as source");
}
var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
Zotero.Styles.ns);
// get file name from URL
var m = pathRe.exec(styleID);
var fileName = Zotero.File.getValidFileName(m ? m[0] : styleID);
var title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
Zotero.Styles.ns);
// look for a parent
var source = Zotero.Utilities.xpathText(doc,
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
Zotero.Styles.ns);
if(source == styleID) {
if(!hidden) alert(Zotero.getString('styles.installError', loadURI));
throw "Style with ID "+styleID+" references itself as source";
}
// ensure csl extension
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
var destFile = Zotero.getStylesDirectory();
var destFileHidden = destFile.clone();
destFile.append(fileName);
destFileHidden.append("hidden");
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
destFileHidden.append(fileName);
// look for an existing style with the same styleID or filename
var existingFile = null;
var existingTitle = null;
if(_styles[styleID]) {
existingFile = _styles[styleID].file;
existingTitle = _styles[styleID].title;
} else {
if(destFile.exists()) {
existingFile = destFile;
} else if(destFileHidden.exists()) {
existingFile = destFileHidden;
} }
if(existingFile) { // ensure csl extension
// find associated style if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
for each(var existingStyle in this._styles) {
if(destFile.equals(existingStyle.file)) { destFile = Zotero.getStylesDirectory();
var destFileHidden = destFile.clone();
destFile.append(fileName);
destFileHidden.append("hidden");
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
destFileHidden.append(fileName);
// look for an existing style with the same styleID or filename
var existingTitle;
if(_styles[styleID]) {
existingFile = _styles[styleID].file;
existingTitle = _styles[styleID].title;
} else {
if(destFile.exists()) {
existingFile = destFile;
} else if(destFileHidden.exists()) {
existingFile = destFileHidden;
}
if(existingFile) {
// find associated style
for each(var existingStyle in _styles) {
if(destFile.equals(existingStyle.file)) {
existingTitle = existingStyle.title;
break;
}
}
}
}
// also look for an existing style with the same title
if(!existingFile) {
for each(var existingStyle in Zotero.Styles.getAll()) {
if(title === existingStyle.title) {
existingFile = existingStyle.file;
existingTitle = existingStyle.title; existingTitle = existingStyle.title;
break; break;
} }
} }
} }
}
// display a dialog to tell the user we're about to install the style
// also look for an existing style with the same title if(hidden) {
if(!existingFile) { destFile = destFileHidden;
for each(var existingStyle in this.getAll()) { } else {
if(title === existingStyle.title) { if(existingTitle) {
existingFile = existingStyle.file; var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
existingTitle = existingStyle.title; } else {
break; var text = Zotero.getString('styles.installStyle', [title, origin]);
}
var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'),
text,
((Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_IS_STRING)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)),
Zotero.getString('general.install'), null, null, null, {}
);
if(index !== 0) {
throw new Zotero.Exception.UserCancelled("style installation");
} }
} }
}
// display a dialog to tell the user we're about to install the style return Zotero.Styles.validate(style).fail(function(validationErrors) {
if(hidden) { Zotero.logError("Style from "+origin+" failed to validate:\n\n"+validationErrors);
destFile = destFileHidden;
} else { // If validation fails on the parent of a dependent style, ignore it (for now)
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] if(hidden) return;
.getService(Components.interfaces.nsIPromptService);
// If validation fails on a different style, we ask the user if s/he really
if(existingTitle) { // wants to install it
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, loadURI]); Components.utils.import("resource://gre/modules/Services.jsm");
} else { var shouldInstall = Services.prompt.confirmEx(null,
var text = Zotero.getString('styles.installStyle', [title, loadURI]); Zotero.getString('styles.install.title'),
} Zotero.getString('styles.validationWarning', origin),
(Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK)
var index = ps.confirmEx(null, '', + (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)
text, + Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
((ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) null, null, null, null, {}
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)), );
Zotero.getString('general.install'), null, null, null, {} if(shouldInstall !== 0) {
); throw new Zotero.Exception.UserCancelled("style installation");
} }
});
if(hidden || index == 0) { }).then(function() {
// user wants to install/update // User wants to install/update
if(source && !_styles[source]) { if(source && !_styles[source]) {
// need to fetch source // Need to fetch source
if(source.substr(0, 7) == "http://" || source.substr(0, 8) == "https://") { if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") {
Zotero.HTTP.doGet(source, function(xmlhttp) { return Zotero.HTTP.promise("GET", source).then(function(xmlhttp) {
var success = false; return _install(xmlhttp.responseText, origin, true);
var error = null; }).fail(function(error) {
try { if(error instanceof Zotero.Exception) {
var success = Zotero.Styles.install(xmlhttp.responseText, loadURI, true); throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
} catch(e) { "styles.install.title", error);
error = e;
}
if(success) {
_completeInstall(style, styleID, destFile, existingFile, styleFile);
} else { } else {
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source]));
throw error; throw error;
} }
}); });
} else { } else {
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source])); throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
throw "Source CSL URI is invalid"; "styles.install.title", "Source CSL URI is invalid");
} }
} else {
_completeInstall(style, styleID, destFile, existingFile, styleFile);
} }
return styleID; }).then(function() {
} // Dependent style has been retrieved if there was one, so we're ready to
// continue
return false;
} // Remove any existing file with a different name
if(existingFile) existingFile.remove(false);
/**
* Finishes installing a style, copying the file, reloading the style cache, and refreshing the return Zotero.File.putContentsAsync(destFile, style);
* styles list in any open windows }).then(function() {
* @param {String} style The style string // Cache
* @param {String} styleID The style ID Zotero.Styles.init();
* @param {nsIFile} destFile The destination for the style
* @param {nsIFile} [existingFile] The existing file to delete before copying this one // Refresh preferences windows
* @param {nsIFile} [styleFile] The file that contains the style to be installed var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
* @private getService(Components.interfaces.nsIWindowMediator);
*/ var enumerator = wm.getEnumerator("zotero:pref");
function _completeInstall(style, styleID, destFile, existingFile, styleFile) { while(enumerator.hasMoreElements()) {
// remove any existing file with a different name var win = enumerator.getNext();
if(existingFile) existingFile.remove(false); win.refreshStylesList(styleID);
}
if(styleFile) { });
styleFile.copyToFollowingLinks(destFile.parent, destFile.leafName);
} else {
Zotero.File.putContents(destFile, style);
}
// recache
Zotero.Styles.init();
// refresh preferences windows
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("zotero:pref");
while(enumerator.hasMoreElements()) {
var win = enumerator.getNext();
win.refreshStylesList(styleID);
}
} }
} }

View file

@ -638,10 +638,13 @@ integration.citationChanged = You have modified this citation since Zotero ge
integration.citationChanged.description = Clicking "Yes" will prevent Zotero from updating this citation if you add additional citations, switch styles, or modify the item to which it refers. Clicking "No" will erase your changes. integration.citationChanged.description = Clicking "Yes" will prevent Zotero from updating this citation if you add additional citations, switch styles, or modify the item to which it refers. Clicking "No" will erase your changes.
integration.citationChanged.edit = You have modified this citation since Zotero generated it. Editing will clear your modifications. Do you want to continue? integration.citationChanged.edit = You have modified this citation since Zotero generated it. Editing will clear your modifications. Do you want to continue?
styles.install.title = Install Style
styles.install.unexpectedError = An unexpected error occurred while installing "%1$S"
styles.installStyle = Install style "%1$S" from %2$S? styles.installStyle = Install style "%1$S" from %2$S?
styles.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S? styles.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S?
styles.installed = The style "%S" was installed successfully. styles.installed = The style "%S" was installed successfully.
styles.installError = %S does not appear to be a valid style file. styles.installError = "%S" is not a valid style file.
styles.validationWarning = "%S" is not valid CSL 1.0, and may not work properly with Zotero.\n\nAre you sure you want to continue?
styles.installSourceError = %1$S references an invalid or non-existent CSL file at %2$S as its source. styles.installSourceError = %1$S references an invalid or non-existent CSL file at %2$S as its source.
styles.deleteStyle = Are you sure you want to delete the style "%1$S"? styles.deleteStyle = Are you sure you want to delete the style "%1$S"?
styles.deleteStyles = Are you sure you want to delete the selected styles? styles.deleteStyles = Are you sure you want to delete the selected styles?

File diff suppressed because one or more lines are too long