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() {
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.ios = Components.classes["@mozilla.org/network/io-service;1"].
@ -40,6 +43,7 @@ Zotero.Styles = new function() {
"csl":"http://purl.org/net/xbiblio/csl"
};
/**
* Initializes styles cache, loading metadata for styles into memory
*/
@ -50,7 +54,7 @@ Zotero.Styles = new function() {
_styles = {};
_visibleStyles = [];
this.cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
_cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
this.lastCSL = null;
// main dir
@ -134,7 +138,7 @@ Zotero.Styles = new function() {
* @return {Zotero.Style[]} An array of Zotero.Style objects
*/
this.getVisible = function() {
if(!_initialized || !this.cacheTranslatorData) this.init();
if(!_initialized || !_cacheTranslatorData) this.init();
return _visibleStyles.slice(0);
}
@ -143,71 +147,110 @@ Zotero.Styles = new function() {
* @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects
*/
this.getAll = function() {
if(!_initialized || !this.cacheTranslatorData) this.init();
if(!_initialized || !_cacheTranslatorData) this.init();
return _styles;
}
/**
* Installs a style file
* @param {String|nsIFile} style An nsIFile representing a style on disk, or a string containing
* the style data
* @param {String} loadURI The URI this style file was loaded from
* @param {Boolean} hidden Whether style is to be hidden. If this parameter is true, UI alerts
* are silenced as well
* Validates a style
* @param {String} style The style, as a string
* @return {Promise} A promise representing the style file. This promise is rejected
* with the validation error if validation fails, or resolved if it is not.
*/
this.install = function(style, loadURI, hidden) {
const pathRe = /[^\/]+$/;
if(!_initialized || !this.cacheTranslatorData) this.init();
// handle nsIFiles
var styleFile = null;
if(style instanceof Components.interfaces.nsIFile) {
styleFile = style;
loadURI = style.leafName;
style = Zotero.File.getContents(styleFile);
this.validate = function(style) {
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;
}
var error = false;
try {
// CSL
/**
* 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) {
// handle nsIFiles
origin = style.leafName;
styleInstalled = Zotero.File.getContentsAsync(styleFile).when(function(style) {
return _install(style, origin);
});
} else {
styleInstalled = _install(style, origin);
}
styleInstalled.fail(function(error) {
// Unless user cancelled, show an alert with the error
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"]
.createInstance(Components.interfaces.nsIDOMParser),
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) {
if(!hidden) alert(Zotero.getString('styles.installError', loadURI));
if(error) throw error;
return false;
}
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),
// 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");
}
// look for a parent
var source = Zotero.Utilities.xpathText(doc,
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";
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";
var destFile = Zotero.getStylesDirectory();
destFile = Zotero.getStylesDirectory();
var destFileHidden = destFile.clone();
destFile.append(fileName);
destFileHidden.append("hidden");
@ -215,8 +258,7 @@ Zotero.Styles = new function() {
destFileHidden.append(fileName);
// look for an existing style with the same styleID or filename
var existingFile = null;
var existingTitle = null;
var existingTitle;
if(_styles[styleID]) {
existingFile = _styles[styleID].file;
existingTitle = _styles[styleID].title;
@ -229,7 +271,7 @@ Zotero.Styles = new function() {
if(existingFile) {
// find associated style
for each(var existingStyle in this._styles) {
for each(var existingStyle in _styles) {
if(destFile.equals(existingStyle.file)) {
existingTitle = existingStyle.title;
break;
@ -240,7 +282,7 @@ Zotero.Styles = new function() {
// also look for an existing style with the same title
if(!existingFile) {
for each(var existingStyle in this.getAll()) {
for each(var existingStyle in Zotero.Styles.getAll()) {
if(title === existingStyle.title) {
existingFile = existingStyle.file;
existingTitle = existingStyle.title;
@ -253,81 +295,78 @@ Zotero.Styles = new function() {
if(hidden) {
destFile = destFileHidden;
} else {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
if(existingTitle) {
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, loadURI]);
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
} else {
var text = Zotero.getString('styles.installStyle', [title, loadURI]);
var text = Zotero.getString('styles.installStyle', [title, origin]);
}
var index = ps.confirmEx(null, '',
var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'),
text,
((ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)),
((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");
}
}
if(hidden || index == 0) {
// user wants to install/update
return Zotero.Styles.validate(style).fail(function(validationErrors) {
Zotero.logError("Style from "+origin+" failed to validate:\n\n"+validationErrors);
// If validation fails on the parent of a dependent style, ignore it (for now)
if(hidden) return;
// If validation fails on a different style, we ask the user if s/he really
// wants to install it
Components.utils.import("resource://gre/modules/Services.jsm");
var shouldInstall = Services.prompt.confirmEx(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)
+ Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
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]) {
// need to fetch source
if(source.substr(0, 7) == "http://" || source.substr(0, 8) == "https://") {
Zotero.HTTP.doGet(source, function(xmlhttp) {
var success = false;
var error = null;
try {
var success = Zotero.Styles.install(xmlhttp.responseText, loadURI, true);
} catch(e) {
error = e;
}
if(success) {
_completeInstall(style, styleID, destFile, existingFile, styleFile);
// Need to fetch source
if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") {
return Zotero.HTTP.promise("GET", source).then(function(xmlhttp) {
return _install(xmlhttp.responseText, origin, true);
}).fail(function(error) {
if(error instanceof Zotero.Exception) {
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
"styles.install.title", error);
} else {
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source]));
throw error;
}
});
} else {
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source]));
throw "Source CSL URI is invalid";
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
"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;
}
/**
* Finishes installing a style, copying the file, reloading the style cache, and refreshing the
* styles list in any open windows
* @param {String} style The style string
* @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
// Remove any existing file with a different name
if(existingFile) existingFile.remove(false);
if(styleFile) {
styleFile.copyToFollowingLinks(destFile.parent, destFile.leafName);
} else {
Zotero.File.putContents(destFile, style);
}
// recache
return Zotero.File.putContentsAsync(destFile, style);
}).then(function() {
// Cache
Zotero.Styles.init();
// refresh preferences windows
// Refresh preferences windows
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("zotero:pref");
@ -335,6 +374,7 @@ Zotero.Styles = new function() {
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.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.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S?
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.deleteStyle = Are you sure you want to delete the style "%1$S"?
styles.deleteStyles = Are you sure you want to delete the selected styles?

File diff suppressed because one or more lines are too long