From d5cf33a798a08ea5ad56ac777fc199d0f9c1daf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 21 Mar 2017 12:47:32 +0200 Subject: [PATCH 1/5] Fix document preferences dialog failing when styles unloaded. Closes #1084 --- chrome/content/zotero/bibliography.js | 6 ++++-- chrome/content/zotero/xpcom/style.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 1d7d4e9c5..0ff60b8f4 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -45,7 +45,7 @@ var Zotero_File_Interface_Bibliography = new function() { * Initialize some variables and prepare event listeners for when chrome is done * loading */ - this.init = function () { + this.init = Zotero.Promise.coroutine(function* () { // Set font size from pref // Affects bibliography.xul and integrationDocPrefs.xul var bibContainer = document.getElementById("zotero-bibliography-container"); @@ -68,6 +68,8 @@ var Zotero_File_Interface_Bibliography = new function() { } // add styles to list + + yield Zotero.Styles.init(); var styles = Zotero.Styles.getVisible(); var index = 0; var nStyles = styles.length; @@ -171,7 +173,7 @@ var Zotero_File_Interface_Bibliography = new function() { // set style to false, in case this is cancelled _io.style = false; - }; + }); /* * Called when locale is changed diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js index bdd97be0a..927385891 100644 --- a/chrome/content/zotero/xpcom/style.js +++ b/chrome/content/zotero/xpcom/style.js @@ -49,7 +49,6 @@ Zotero.Styles = new function() { this.reinit = Zotero.Promise.coroutine(function* () { Zotero.debug("Initializing styles"); var start = new Date; - _initialized = true; // Upgrade style locale prefs for 4.0.27 var bibliographyLocale = Zotero.Prefs.get("export.bibliographyLocale"); @@ -114,6 +113,7 @@ Zotero.Styles = new function() { _renamedStyles = xmlhttp.response; } }) + _initialized = true; }); this.init = Zotero.lazy(this.reinit); From 269a250b4f950bf917c3a680f1d2c5aad2c75cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Fri, 31 Mar 2017 14:27:22 +0300 Subject: [PATCH 2/5] Fetch a style if it is not installed on document preferences load --- chrome/content/zotero/bibliography.js | 14 +-- chrome/content/zotero/xpcom/file.js | 4 +- chrome/content/zotero/xpcom/integration.js | 101 +++++++++--------- chrome/content/zotero/xpcom/style.js | 32 ++++-- test/tests/data/cell.csl | 116 +++++++++++++++++++++ test/tests/styleTest.js | 45 ++++++++ 6 files changed, 245 insertions(+), 67 deletions(-) create mode 100644 test/tests/data/cell.csl create mode 100644 test/tests/styleTest.js diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 0ff60b8f4..13c591a54 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -67,23 +67,25 @@ var Zotero_File_Interface_Bibliography = new function() { _io.style = Zotero.Prefs.get("export.lastStyle"); } + // Initialize styles and try to load the style, attempting a download + yield Zotero.Styles.init(); + if (!Zotero.Styles.get(_io.style)) { + yield Zotero.Styles.install({url: _io.style}, data.style.styleID, true); + } + // add styles to list - yield Zotero.Styles.init(); var styles = Zotero.Styles.getVisible(); - var index = 0; - var nStyles = styles.length; var selectIndex = null; - for(var i=0; i + diff --git a/test/tests/styleTest.js b/test/tests/styleTest.js new file mode 100644 index 000000000..1e627a401 --- /dev/null +++ b/test/tests/styleTest.js @@ -0,0 +1,45 @@ +"use strict"; + +describe("Zotero.Styles", function() { + var styleID = "http://www.zotero.org/styles/cell"; + var stylePath = OS.Path.join(getTestDataDirectory().path, 'cell.csl'); + var styleFile = Zotero.File.pathToFile(stylePath); + var style; + + before(function* () { + yield Zotero.Styles.init(); + style = yield Zotero.File.getContentsAsync(stylePath); + }); + + describe("Zotero.Styles.install", function() { + afterEach(function* (){ + assert.isOk(Zotero.Styles.get(styleID)); + yield Zotero.Styles.get(styleID).remove(); + }); + + it("should install the style from string", function* () { + yield Zotero.Styles.install(style, styleID, true); + }); + + it("should install the style from nsIFile", function* () { + yield Zotero.Styles.install(styleFile, styleID, true); + }); + + it("should install the style from url", function* () { + var getContentsFromURLAsync = Zotero.File.getContentsFromURLAsync; + sinon.stub(Zotero.File, 'getContentsFromURLAsync', function(style) { + if (style.url == styleID) { + return Zotero.Promise.resolve(style); + } else { + return getContentsFromURLAsync.apply(Zotero.File, arguments); + } + }); + yield Zotero.Styles.install({url: styleID}, styleID, true); + Zotero.File.getContentsFromURLAsync.restore(); + }); + + it("should install the style from file path", function* () { + yield Zotero.Styles.install({file: stylePath}, styleID, true); + }) + }); +}); From 02c43c3643c5059d2a42de8543bd3e488d2a1ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Thu, 6 Apr 2017 14:19:25 +0300 Subject: [PATCH 3/5] Add integrationTests.js Contains a dummy doc plugin, which is useful for: - Testing integration.js functionality - Serving as succint documentation for development of new integration plugins --- chrome/content/zotero/xpcom/integration.js | 81 +++---- test/tests/integrationTests.js | 241 +++++++++++++++++++++ test/tests/styleTest.js | 2 +- 3 files changed, 286 insertions(+), 38 deletions(-) create mode 100644 test/tests/integrationTests.js diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index bae225e10..f03266baa 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -40,6 +40,19 @@ const FORCE_CITATIONS_RESET_TEXT = 2; // this is used only for update checking const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; + +// These must match the constants defined in zoteroIntegration.idl +const DIALOG_ICON_STOP = 0; +const DIALOG_ICON_WARNING = 1; +const DIALOG_ICON_CAUTION = 2; + +const DIALOG_BUTTONS_OK = 0; +const DIALOG_BUTTONS_OK_CANCEL = 1; +const DIALOG_BUTTONS_YES_NO = 2; +const DIALOG_BUTTONS_YES_NO_CANCEL = 3; + +const NOTE_FOOTNOTE = 1; +const NOTE_ENDNOTE = 2; Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); @@ -210,6 +223,19 @@ Zotero.Integration = new function() { }; } + this.getApplication = function(agent, command, docId) { + // Try to load the appropriate Zotero component; otherwise display an error + try { + var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; + Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); + return Components.classes[componentClass] + .getService(Components.interfaces.zoteroIntegrationApplication); + } catch(e) { + throw new Zotero.Exception.Alert("integration.error.notInstalled", + [], "integration.error.title"); + } + }, + /** * Executes an integration command, first checking to make sure that versions are compatible */ @@ -230,17 +256,8 @@ Zotero.Integration = new function() { inProgress = true; // Check integration component versions - _checkPluginVersions().then(function() { - // Try to load the appropriate Zotero component; otherwise display an error - try { - var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; - Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); - var application = Components.classes[componentClass] - .getService(Components.interfaces.zoteroIntegrationApplication); - } catch(e) { - throw new Zotero.Exception.Alert("integration.error.notInstalled", - [], "integration.error.title"); - } + return _checkPluginVersions().then(function() { + var application = Zotero.Integration.getApplication(agent, command, docId); // Try to execute the command; otherwise display an error in alert service or word processor // (depending on what is possible) @@ -267,9 +284,7 @@ Zotero.Integration = new function() { if(document) { try { document.activate(); - document.displayAlert(displayError, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + document.displayAlert(displayError, DIALOG_ICON_STOP, DIALOG_BUTTONS_OK); } catch(e) { showErrorInFirefox = true; } @@ -821,10 +836,7 @@ Zotero.Integration.CorruptFieldException.prototype = { field = this.fieldGetter._fields[this.fieldIndex]; field.select(); this.fieldGetter._doc.activate(); - var result = this.fieldGetter._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL); - + var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); if(result == 0) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update")); } else if(result == 1) { // No @@ -878,9 +890,7 @@ Zotero.Integration.CorruptBibliographyException.prototype = { var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ Zotero.getString('integration.corruptBibliography.description'); - var result = this.fieldGetter._doc.displayAlert(msg, - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); if(result == 0) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("clearing corrupted bibliography")); } else { @@ -991,8 +1001,7 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun } var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); if(!warning) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document upgrade")); } @@ -1007,9 +1016,11 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun [], "integration.error.title")); } - if(Zotero.Integration.sessions[data.sessionID]) { + if (Zotero.Integration.sessions[data.sessionID]) { + // If communication occured with this document since restart this._session = Zotero.Integration.sessions[data.sessionID]; } else { + // Document has zotero data, but has not communicated since Zotero restart this._session = this._createNewSession(data); try { yield this._session.setData(data); @@ -1027,7 +1038,6 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun } } - this._doc.setDocumentData(this._session.data.serializeXML()); this._session.reload = true; } return Zotero.Promise.resolve(this._session); @@ -1156,8 +1166,7 @@ Zotero.Integration.Document.prototype.removeCodes = function() { return fieldGetter.get() }).then(function(fields) { var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); if(result) { for(var i=fields.length-1; i>=0; i--) { fields[i].removeCode(); @@ -1301,8 +1310,8 @@ Zotero.Integration.Fields.prototype.addField = function(note) { var field = this._doc.cursorInField(this._session.data.prefs['fieldType']); if(field) { if(!this._doc.displayAlert(Zotero.getString("integration.replace"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { + DIALOG_ICON_STOP, + DIALOG_BUTTONS_OK_CANCEL)) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("inserting citation")); } } @@ -1607,8 +1616,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, field.select(); var result = this._doc.displayAlert( Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO); + DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); if(result) { citation.properties.dontUpdate = true; } @@ -1752,8 +1760,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f + "Current: " + field.getText() ); if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"), - Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, - Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { throw new Zotero.Exception.UserCancelled("editing citation"); } } @@ -2351,9 +2358,9 @@ Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(func this.updateIndices[index] = true; } } else { - if(citationItem.key) { + if(citationItem.key && citationItem.libraryID) { // DEBUG: why no library id? - zoteroItem = Zotero.Items.getByLibraryAndKey(0, citationItem.key); + zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); } else if(citationItem.itemID) { zoteroItem = Zotero.Items.get(citationItem.itemID); } else if(citationItem.id) { @@ -3085,9 +3092,9 @@ Zotero.Integration.DocumentData.prototype.unserialize = function(input) { "storeReferences":false}; if(prefParameters[2] == "note") { if(prefParameters[4] == "1" || prefParameters[4] == "True") { - this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_ENDNOTE; + this.prefs.noteType = NOTE_ENDNOTE; } else { - this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_FOOTNOTE; + this.prefs.noteType = NOTE_FOOTNOTE; } } else { this.prefs.noteType = 0; diff --git a/test/tests/integrationTests.js b/test/tests/integrationTests.js new file mode 100644 index 000000000..1c3c3ea43 --- /dev/null +++ b/test/tests/integrationTests.js @@ -0,0 +1,241 @@ +"use strict"; + +describe("Zotero.Integration", function () { + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + // Fully functional document plugin dummy based on word-for-windows-integration + // Functions should be stubbed when testing as needed + var DocumentPluginDummy = {}; + + DocumentPluginDummy.Application = function() { + this.doc = new DocumentPluginDummy.Document(); + this.primaryFieldType = "Field"; + this.secondaryFieldType = "Bookmark"; + this.fields = []; + }; + DocumentPluginDummy.Application.prototype = { + getActiveDocument: function() {return this.doc}, + getDocument: function() {return this.doc}, + QueryInterface: function() {return this}, + }; + + DocumentPluginDummy.Document = function() {this.fields = []}; + DocumentPluginDummy.Document.prototype = { + // Needs to be stubbed for expected return values depending on prompt type + // - Yes: 2, No: 1, Cancel: 0 + // - Yes: 1, No: 0 + // - Ok: 1, Cancel: 0 + // - Ok: 0 + displayAlert: () => 0, + activate: () => 0, + canInsertField: () => true, + cursorInField: () => false, + getDocumentData: function() {return this.data}, + setDocumentData: function(data) {this.data = data}, + insertField: function() { var field = new DocumentPluginDummy.Field(this); this.fields.push(field); return field }, + getFields: function() {return new DocumentPluginDummy.FieldEnumerator(this)}, + getFieldsAsync: function(fieldType, observer) { + observer.observe(this.getFields(fieldType), 'fields-available', null) + }, + setBibliographyStyle: () => 0, + convert: () => 0, + cleanup: () => 0, + complete: () => 0, + QueryInterface: function() {return this}, + }; + + DocumentPluginDummy.FieldEnumerator = function(doc) {this.doc = doc; this.idx = 0}; + DocumentPluginDummy.FieldEnumerator.prototype = { + hasMoreElements: function() {return this.idx < this.doc.fields.length;}, + getNext: function() {return this.doc.fields[this.idx++]}, + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, + Components.interfaces.nsISimpleEnumerator]) + }; + + DocumentPluginDummy.Field = function(doc) { + this.doc = doc; + this.code = this.text = ''; + this.noteIndex = DocumentPluginDummy.Field.noteIndex++; + this.wrappedJSObject = this; + }; + DocumentPluginDummy.Field.noteIndex = 0; + DocumentPluginDummy.Field.prototype = { + delete: function() {this.doc.fields.filter((field) => field != this)}, + removeCode: function() {this.code = ""}, + select: () => 0, + setText: function(text) {this.text = text}, + getText: function() {return this.text}, + setCode: function(code) {this.code = code}, + getCode: function() {return this.code}, + equals: function(field) {return this == field}, + getNoteIndex: function() {return this.noteIndex}, + QueryInterface: function() {return this}, + }; + + for (let cls of ['Application', 'Document', 'FieldEnumerator', 'Field']) { + for (let methodName in DocumentPluginDummy[cls].prototype) { + if (methodName !== 'QueryInterface') { + let method = DocumentPluginDummy[cls].prototype[methodName]; + DocumentPluginDummy[cls].prototype[methodName] = function() { + try { + Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${JSON.stringify(arguments)}`, 2); + } catch (e) { + Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${arguments}`, 2); + } + var result = method.apply(this, arguments); + try { + Zotero.debug(`Result: ${JSON.stringify(result)}`, 2); + } catch (e) { + Zotero.debug(`Result: ${result}`, 2); + } + return result; + } + } + } + } + + var testItems; + var applications = {}; + var addEditCitationSpy, displayDialogStub; + var styleID = "http://www.zotero.org/styles/cell"; + var stylePath = OS.Path.join(getTestDataDirectory().path, 'cell.csl'); + + var commandList = [ + 'addCitation', 'editCitation', 'addEditCitation', + 'addBibliography', 'editBibliography', 'addEditBibliography', + 'refresh', 'removeCodes', 'setDocPrefs' + ]; + + function execCommand(command, docID) { + if (! commandList.includes(command)) { + throw new Error(`${command} is not a valid document command`); + } + if (typeof docID === "undefined") { + throw new Error(`docID cannot be undefined`) + } + return Zotero.Integration.execCommand("dummy", command, docID); + } + + var dialogResults = { + addCitationDialog: {}, + quickFormat: {}, + integrationDocPrefs: {}, + selectItemsDialog: {}, + editBibliographyDialog: {} + }; + + function initDoc(docID, options={}) { + applications[docID] = new DocumentPluginDummy.Application(); + var data = new Zotero.Integration.DocumentData(); + data.prefs = { + noteType: 0, + fieldType: "Field", + storeReferences: true, + automaticJournalAbbreviations: true + }; + data.style = {styleID, locale: 'en-US', hasBibliography: true, bibliographyStyleHasBeenSet: true}; + data.sessionID = Zotero.Utilities.randomString(10); + Object.assign(data, options); + applications[docID].getActiveDocument().setDocumentData(data.serializeXML()); + } + + function setDefaultIntegrationDocPrefs() { + dialogResults.integrationDocPrefs = { + style: "http://www.zotero.org/styles/cell", + locale: 'en-US', + fieldType: 'Field', + storeReferences: true, + automaticJournalAbbreviations: false, + useEndnotes: 0 + }; + } + setDefaultIntegrationDocPrefs(); + + function setAddEditItems(items) { + if (items.length == undefined) items = [items]; + dialogResults.quickFormat = function(doc, dialogName) { + var citationItems = items.map((i) => {return {id: i.id} }); + var field = doc.insertField(); + field.setCode('TEMP'); + var integrationDoc = addEditCitationSpy.lastCall.thisValue; + var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); + var io = new Zotero.Integration.CitationEditInterface( + { citationItems, properties: {} }, + field, + fieldGetter, + integrationDoc._session + ); + io._acceptDeferred.resolve(); + return io; + } + } + + before(function* () { + yield Zotero.Styles.init(); + yield Zotero.Styles.install({file: stylePath}, styleID, true); + + testItems = []; + for (let i = 0; i < 5; i++) { + testItems.push(yield createDataObject('item', {libraryID: Zotero.Libraries.userLibraryID})); + } + setAddEditItems(testItems[0]); + + sinon.stub(Zotero.Integration, 'getApplication', function(agent, command, docID) { + if (!applications[docID]) { + applications[docID] = new DocumentPluginDummy.Application(); + } + return applications[docID]; + }); + + displayDialogStub = sinon.stub(Zotero.Integration, 'displayDialog', function(doc, dialogName, prefs, io) { + var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; + if (typeof ioResult == 'function') { + ioResult = ioResult(doc, dialogName); + } + Object.assign(io, ioResult); + return Zotero.Promise.resolve(); + }); + + addEditCitationSpy = sinon.spy(Zotero.Integration.Document.prototype, 'addEditCitation'); + }); + + after(function() { + Zotero.Integration.getApplication.restore(); + displayDialogStub.restore(); + addEditCitationSpy.restore(); + }); + + describe('Document', function() { + describe('#addEditCitation', function() { + var setDocumentDataSpy; + var docID = this.fullTitle(); + + before(function() { + setDocumentDataSpy = sinon.spy(DocumentPluginDummy.Document.prototype, 'setDocumentData'); + }); + + afterEach(function() { + setDocumentDataSpy.reset(); + }); + + after(function() { + setDocumentDataSpy.restore(); + }); + + it('should call doc.setDocumentData on a fresh document', function* () { + yield execCommand('addEditCitation', docID); + assert.isTrue(setDocumentDataSpy.calledOnce); + }); + + it('should not call doc.setDocumentData on subsequent invocations', function* () { + yield execCommand('addEditCitation', docID); + assert.isFalse(setDocumentDataSpy.called); + }); + + it('should not call doc.setDocumentData when document communicates for first time since restart, but has data', function* () { + Zotero.Integration.sessions = {}; + yield execCommand('addEditCitation', docID); + assert.isFalse(setDocumentDataSpy.called); + }); + }) + }) +}); diff --git a/test/tests/styleTest.js b/test/tests/styleTest.js index 1e627a401..198750380 100644 --- a/test/tests/styleTest.js +++ b/test/tests/styleTest.js @@ -12,7 +12,7 @@ describe("Zotero.Styles", function() { }); describe("Zotero.Styles.install", function() { - afterEach(function* (){ + afterEach(`${styleID} style should be installed`, function* (){ assert.isOk(Zotero.Styles.get(styleID)); yield Zotero.Styles.get(styleID).remove(); }); From 3e69da7e4c12f7a0e2b5f35ac5bfdf6b7eed4e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 12 Apr 2017 11:47:57 +0300 Subject: [PATCH 4/5] Prompt if style in document is not from official source --- chrome/content/zotero/bibliography.js | 5 +---- chrome/content/zotero/xpcom/integration.js | 15 ++++++++++----- chrome/locale/en-US/zotero/zotero.properties | 1 + 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 13c591a54..5e198255f 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -67,11 +67,8 @@ var Zotero_File_Interface_Bibliography = new function() { _io.style = Zotero.Prefs.get("export.lastStyle"); } - // Initialize styles and try to load the style, attempting a download + // Initialize styles yield Zotero.Styles.init(); - if (!Zotero.Styles.get(_io.style)) { - yield Zotero.Styles.install({url: _io.style}, data.style.styleID, true); - } // add styles to list diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index f03266baa..226117f35 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -1027,6 +1027,16 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun } catch(e) { // make sure style is defined if(e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { + if (data.style.styleID) { + let displayError = Zotero.getString("integration.error.styleMissing", data.style.styleID); + if (/^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID) || + me._doc.displayAlert(displayError, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { + + yield Zotero.Styles.install({url: data.style.styleID}, data.style.styleID, true); + yield this._session.setData(data, true); + return Zotero.Promise.resolve(this._session); + } + } return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, this._app.secondaryFieldType).then(function(status) { me._doc.setDocumentData(me._session.data.serializeXML()); @@ -2081,10 +2091,6 @@ Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function try { yield Zotero.Styles.init(); var getStyle = Zotero.Styles.get(data.style.styleID); - if (!getStyle) { - yield Zotero.Styles.install({url: data.style.styleID}, data.style.styleID, true); - getStyle = Zotero.Styles.get(data.style.styleID); - } data.style.hasBibliography = getStyle.hasBibliography; this.style = getStyle.getCiteProc(data.style.locale, data.prefs.automaticJournalAbbreviations); this.style.setOutputFormat("rtf"); @@ -2092,7 +2098,6 @@ Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function this.dateModified = new Object(); } catch (e) { Zotero.logError(e); - data.style.styleID = undefined; throw new Zotero.Exception.Alert("integration.error.invalidStyle"); } diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 896dc86a5..c3452564b 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -818,6 +818,7 @@ integration.error.noBibliography = The current bibliographic style does not def integration.error.deletePipe = The pipe that Zotero uses to communicate with the word processor could not be initialized. Would you like Zotero to attempt to correct this error? You will be prompted for your password. integration.error.invalidStyle = The style you have selected does not appear to be valid. If you have created this style yourself, please ensure that it passes validation as described at https://github.com/citation-style-language/styles/wiki/Validation. Alternatively, try selecting another style. integration.error.fieldTypeMismatch = Zotero cannot update this document because it was created by a different word processing application with an incompatible field encoding. In order to make a document compatible with both Word and LibreOffice, open the document in the word processor with which it was originally created and switch the field type to Bookmarks in the Zotero Document Preferences. +integration.error.styleMissing = The citation style used in this document is missing. Would you like to install it from %S? integration.replace = Replace this Zotero field? integration.missingItem.single = The highlighted citation no longer exists in your Zotero database. Do you want to select a substitute item? From ea535bceb0aabc7276a6cef91a8debf3bcc08dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 12 Apr 2017 11:40:28 +0300 Subject: [PATCH 5/5] Add tests for style from untrusted source prompt --- test/tests/integrationTests.js | 86 +++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/test/tests/integrationTests.js b/test/tests/integrationTests.js index 1c3c3ea43..170950b5b 100644 --- a/test/tests/integrationTests.js +++ b/test/tests/integrationTests.js @@ -236,6 +236,88 @@ describe("Zotero.Integration", function () { yield execCommand('addEditCitation', docID); assert.isFalse(setDocumentDataSpy.called); }); - }) - }) + + describe('when style used in the document does not exist', function() { + var docID = this.fullTitle(); + var displayAlertStub; + var style; + before(function* () { + displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').returns(0); + }); + + beforeEach(function() { + // 🦉birds? + style = {styleID: "http://www.example.com/csl/waterbirds", locale: 'en-US'}; + + // Make sure style not in library + try { + Zotero.Styles.get(style.styleID).remove(); + } catch (e) {} + initDoc(docID, {style}); + displayDialogStub.reset(); + displayAlertStub.reset(); + }); + + after(function* () { + displayAlertStub.restore(); + }); + + describe('when the style is not from a trusted source', function() { + it('should download the style and not call doc.setDocumentData if user clicks YES', function* () { + setDocumentDataSpy.reset(); + var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves(); + var style = Zotero.Styles.get(styleID); + var styleGetCalledOnce = false; + var styleGetStub = sinon.stub(Zotero.Styles, 'get', function() { + if (!styleGetCalledOnce) { + styleGetCalledOnce = true; + return false; + } + return style; + }); + displayAlertStub.returns(1); + yield execCommand('addEditCitation', docID); + assert.isTrue(displayAlertStub.calledOnce); + assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul')); + assert.isTrue(styleInstallStub.calledOnce); + assert.isFalse(setDocumentDataSpy.called); + assert.isOk(Zotero.Styles.get(style.styleID)); + styleInstallStub.restore(); + styleGetStub.restore(); + }); + + it('should prompt with the document preferences dialog if user clicks NO', function* () { + displayAlertStub.returns(0); + yield execCommand('addEditCitation', docID); + assert.isTrue(displayAlertStub.calledOnce); + // Prefs to select a new style and quickFormat + assert.isTrue(displayDialogStub.calledTwice); + assert.isNotOk(Zotero.Styles.get(style.styleID)); + }); + }); + + it('should download the style without prompting if it is from zotero.org', function* (){ + initDoc(docID, {styleID: "http://www.zotero.org/styles/waterbirds", locale: 'en-US'}); + var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves(); + var style = Zotero.Styles.get(styleID); + var styleGetCalledOnce = false; + var styleGetStub = sinon.stub(Zotero.Styles, 'get', function() { + if (!styleGetCalledOnce) { + styleGetCalledOnce = true; + return false; + } + return style; + }); + displayAlertStub.returns(1); + yield execCommand('addEditCitation', docID); + assert.isFalse(displayAlertStub.called); + assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul')); + assert.isTrue(styleInstallStub.calledOnce); + assert.isOk(Zotero.Styles.get(style.styleID)); + styleInstallStub.restore(); + styleGetStub.restore(); + }); + }); + }); + }); });