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"].
@ -40,6 +43,7 @@ Zotero.Styles = new function() {
"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");
worker.onmessage = function(event) {
if(event.data) {
deferred.reject(event.data);
} else {
deferred.resolve();
}
};
worker.postMessage(style);
return deferred.promise;
}
if(!_initialized || !this.cacheTranslatorData) this.init(); /**
* Installs a style file, getting the contents of an nsIFile and showing appropriate
// handle nsIFiles * error messages
var styleFile = null; * @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");
}
} catch(e) {
error = e;
}
if(!doc || error) { var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
if(!hidden) alert(Zotero.getString('styles.installError', loadURI)); Zotero.Styles.ns),
if(error) throw error; // Get file name from URL
return false; 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);
var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]', if(!styleID || !title) {
Zotero.Styles.ns); // If it's not valid XML, we'll return a promise that immediately resolves
// get file name from URL // to an error
var m = pathRe.exec(styleID); throw new Zotero.Exception.Alert("styles.installError", origin,
var fileName = Zotero.File.getValidFileName(m ? m[0] : styleID); "styles.install.title", "Style is not valid XML, or the styleID or title is missing");
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) { // look for a parent
// find associated style source = Zotero.Utilities.xpathText(doc,
for each(var existingStyle in this._styles) { '/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
if(destFile.equals(existingStyle.file)) { Zotero.Styles.ns);
if(source == styleID) {
throw Zotero.Exception.Alert("styles.installError", origin,
"styles.install.title", "Style references itself as source");
}
// ensure csl extension
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
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;
} }
} }
} }
}
// also look for an existing style with the same title // display a dialog to tell the user we're about to install the style
if(!existingFile) { if(hidden) {
for each(var existingStyle in this.getAll()) { destFile = destFileHidden;
if(title === existingStyle.title) { } else {
existingFile = existingStyle.file; if(existingTitle) {
existingTitle = existingStyle.title; var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
break; } else {
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 {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
if(existingTitle) { // If validation fails on the parent of a dependent style, ignore it (for now)
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, loadURI]); if(hidden) return;
} else {
var text = Zotero.getString('styles.installStyle', [title, loadURI]);
}
var index = ps.confirmEx(null, '', // If validation fails on a different style, we ask the user if s/he really
text, // wants to install it
((ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) Components.utils.import("resource://gre/modules/Services.jsm");
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)), var shouldInstall = Services.prompt.confirmEx(null,
Zotero.getString('general.install'), null, null, null, {} Zotero.getString('styles.install.title'),
); Zotero.getString('styles.validationWarning', origin),
} (Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)
if(hidden || index == 0) { + Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
// user wants to install/update null, null, null, null, {}
);
if(shouldInstall !== 0) {
throw new Zotero.Exception.UserCancelled("style installation");
}
});
}).then(function() {
// 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);
/** return Zotero.File.putContentsAsync(destFile, style);
* Finishes installing a style, copying the file, reloading the style cache, and refreshing the }).then(function() {
* styles list in any open windows // Cache
* @param {String} style The style string Zotero.Styles.init();
* @param {String} styleID The style ID
* @param {nsIFile} destFile The destination for the style
* @param {nsIFile} [existingFile] The existing file to delete before copying this one
* @param {nsIFile} [styleFile] The file that contains the style to be installed
* @private
*/
function _completeInstall(style, styleID, destFile, existingFile, styleFile) {
// remove any existing file with a different name
if(existingFile) existingFile.remove(false);
if(styleFile) { // Refresh preferences windows
styleFile.copyToFollowingLinks(destFile.parent, destFile.leafName); var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
} else { getService(Components.interfaces.nsIWindowMediator);
Zotero.File.putContents(destFile, style); var enumerator = wm.getEnumerator("zotero:pref");
} while(enumerator.hasMoreElements()) {
var win = enumerator.getNext();
// recache win.refreshStylesList(styleID);
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