www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 3fc38d750bc0b9efe3273f157019103ba3d52300
parent e10deedc7e98d5e1fd4d1102b5bceeb68399293a
Author: Simon Kornblith <simon@simonster.com>
Date:   Thu,  4 Jun 2015 00:47:58 -0400

Use Zotero.Item.fromJSON() for saving from translators

Also:
  - Move some canonicalization of items returned by translators to
    Zotero.Translate
  - Make Zotero.Translate#translate return a promise
  - Add tests

Diffstat:
Mchrome/content/zotero/xpcom/translation/translate.js | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mchrome/content/zotero/xpcom/translation/translate_item.js | 540+++++++++++++++++++++++++++++++++++--------------------------------------------
Mtest/runtests.sh | 1+
Atest/tests/data/snapshot/index.html | 16++++++++++++++++
Mtest/tests/translateTest.js | 554++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 921 insertions(+), 338 deletions(-)

diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js @@ -90,14 +90,13 @@ Zotero.Translate.Sandbox = { const allowedObjects = ["complete", "attachments", "seeAlso", "creators", "tags", "notes"]; - delete item.complete; + // Create a new object here, so that we strip the "complete" property + // (But don't create a new object if we're in a child translator, since that + // would be a problem for the sandbox) + var newItem = translate._parentTranslator ? item : {}; for(var i in item) { var val = item[i]; - if(!val && val !== 0) { - // remove null, undefined, and false properties, and convert objects to strings - delete item[i]; - continue; - } + if(i === "complete" || (!val && val !== 0)) continue; var type = typeof val; var isObject = type === "object" || type === "xml" || type === "function", @@ -105,15 +104,18 @@ Zotero.Translate.Sandbox = { if(isObject && !shouldBeObject) { // Convert things that shouldn't be objects to objects translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to string"); - item[i] = val.toString(); + newItem[i] = val.toString(); } else if(shouldBeObject && !isObject) { translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to array"); - item[i] = [val]; + newItem[i] = [val]; } else if(type === "string") { // trim strings - item[i] = val.trim(); + newItem[i] = val.trim(); + } else { + newItem[i] = val; } } + item = newItem; // Clean empty creators if (item.creators) { @@ -125,6 +127,9 @@ Zotero.Translate.Sandbox = { } } } + + // Canonicalize tags + if(item.tags) item.tags = translate._cleanTags(item.tags); // if we're not supposed to save the item or we're in a child translator, // just return the item array @@ -137,15 +142,35 @@ Zotero.Translate.Sandbox = { // We use this within the connector to keep track of items as they are saved if(!item.id) item.id = Zotero.Utilities.randomString(); - // don't save documents as documents in connector, since we can't pass them around - if(Zotero.isConnector) { + if(item.attachments) { var attachments = item.attachments; - var nAttachments = attachments.length; - for(var j=0; j<nAttachments; j++) { - if(attachments[j].document) { - attachments[j].url = attachments[j].document.documentURI || attachments[j].document.URL; - attachments[j].mimeType = "text/html"; - delete attachments[j].document; + for(var j=0; j<attachments.length; j++) { + var attachment = attachments[j]; + + // Don't save documents as documents in connector, since we can't pass them around + if(Zotero.isConnector && attachment.document) { + attachment.url = attachment.document.documentURI || attachment.document.URL; + attachment.mimeType = "text/html"; + delete attachment.document; + } + + // Canonicalize tags + if(attachment.tags !== undefined) attachment.tags = translate._cleanTags(attachment.tags); + } + } + + if(item.notes) { + var notes = item.notes; + for(var j=0; j<notes.length; j++) { + var note = notes[j]; + if(!note) { + notes.splice(j--, 1); + } else if(typeof(note) == "object") { + // Canonicalize tags + if(note.tags !== undefined) note.tags = translate._cleanTags(note.tags); + } else { + // Convert to object + notes[j] = {"note":note.toString()} } } } @@ -320,7 +345,7 @@ Zotero.Translate.Sandbox = { errorHandlerSet = true; translation.setHandler("error", function(obj, error) { translate.complete(false, error) }); } - return translation.translate(false); + translation.translate(false); }; safeTranslator.getTranslatorObject = function(callback) { @@ -717,13 +742,9 @@ Zotero.Translate.Sandbox = { * @param {SandboxCollection} collection */ "_collectionDone":function(translate, collection) { + translate.newCollections.push(collection); if(translate._libraryID == false) { - translate.newCollections.push(collection); translate._runHandler("collectionDone", collection); - } else { - var newCollection = translate._itemSaver.saveCollection(collection); - translate.newCollections.push(newCollection); - translate._runHandler("collectionDone", newCollection); } }, @@ -1139,6 +1160,8 @@ Zotero.Translate.Base.prototype = { * or NULL for default library; * if FALSE, don't save items * @param {Boolean} [saveAttachments=true] Exclude attachments (e.g., snapshots) on import + * @returns {Promise} Promise resolved with saved items + * when translation complete */ "translate":function(libraryID, saveAttachments) { // initialize properties specific to each translation if(!this.translator || !this.translator.length) { @@ -1163,6 +1186,22 @@ Zotero.Translate.Base.prototype = { this._savingAttachments = []; this._savingItems = 0; this._waitingForSave = false; + + // Attach handlers for promise + var me = this, + deferred = Zotero.Promise.defer(); + var doneHandler = function (obj, returnValue) { + if (returnValue) deferred.resolve(me.newItems); + me.removeHandler("done", doneHandler); + me.removeHandler("error", errorHandler); + }; + var errorHandler = function (obj, error) { + deferred.reject(error); + me.removeHandler("done", doneHandler); + me.removeHandler("error", errorHandler); + }; + this.setHandler("done", doneHandler); + this.setHandler("error", errorHandler); var me = this; if(typeof this.translator[0] === "object") { @@ -1174,6 +1213,7 @@ Zotero.Translate.Base.prototype = { this.translator[0] = translator; this._loadTranslator(translator).then(function() { me._translateTranslatorLoaded() }); } + return deferred.promise; }, /** @@ -1368,6 +1408,31 @@ Zotero.Translate.Base.prototype = { return errorString; }, + + /** + * Canonicalize an array of tags such that they are all objects with the tag stored in the + * "tag" property and a type (if specified) is stored in the "type" property + * @returns {Object[]} Array of new tag objects + */ + "_cleanTags":function(tags) { + var newTags = []; + if(!tags) return newTags; + for(var i=0; i<tags.length; i++) { + var tag = tags[i]; + if(!tag) continue; + if(typeof(tag) == "object") { + var tagString = tag.tag || tag.name; + if(tagString) { + var newTag = {"tag":tagString}; + if(tag.type) newTag.type = tag.type; + newTags.push(newTag); + } + } else { + newTags.push({"tag":tag.toString()}); + } + } + return newTags; + }, /** * Saves items to the database, taking care to defer attachmentProgress notifications @@ -1443,7 +1508,18 @@ Zotero.Translate.Base.prototype = { */ "_checkIfDone":function() { if(!this._savingItems && !this._savingAttachments.length && (!this._currentState || this._waitingForSave)) { - this._runHandler("done", true); + if(this.newCollections) { + var me = this; + this._itemSaver.saveCollections(this.newCollections).then(function (newCollections) { + me.newCollections = newCollections; + me._runHandler("done", true); + }, function (err) { + me._runHandler("error", err); + me._runHandler("done", false); + }); + } else { + this._runHandler("done", true); + } } }, @@ -1737,9 +1813,13 @@ Zotero.Translate.Web.prototype._getParameters = function() { * Prepare translation */ Zotero.Translate.Web.prototype._prepareTranslation = function() { - this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID, - Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], 1, - this.document, this._cookieSandbox, this.location); + this._itemSaver = new Zotero.Translate.ItemSaver({ + "libraryID":this._libraryID, + "attachmentMode":Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], + "forceTagType":1, + "cookieSandbox":this._cookieSandbox, + "baseURI":this.location + }); this.newItems = []; } @@ -1748,7 +1828,7 @@ Zotero.Translate.Web.prototype._prepareTranslation = function() { */ Zotero.Translate.Web.prototype.translate = function(libraryID, saveAttachments, selectedItems) { this._selectedItems = selectedItems; - Zotero.Translate.Base.prototype.translate.apply(this, [libraryID, saveAttachments]); + return Zotero.Translate.Base.prototype.translate.apply(this, [libraryID, saveAttachments]); } /** @@ -2044,10 +2124,12 @@ Zotero.Translate.Import.prototype._prepareTranslation = function() { getService(Components.interfaces.nsIIOService).newFileURI(this.location); } catch(e) {} } - - this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID, - Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")], - undefined, undefined, undefined, baseURI); + + this._itemSaver = new Zotero.Translate.ItemSaver({ + "libraryID":this._libraryID, + "attachmentMode":Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")], + "baseURI":baseURI + }); this.newItems = []; this.newCollections = []; } @@ -2215,7 +2297,7 @@ Zotero.Translate.Export.prototype.translate = function() { if(!this.translator || !this.translator.length) { this.complete(false, new Error("Export translation initiated without setting a translator")); } else { - Zotero.Translate.Base.prototype.translate.apply(this, arguments); + return Zotero.Translate.Base.prototype.translate.apply(this, arguments); } }; diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -23,52 +23,42 @@ ***** END LICENSE BLOCK ***** */ -Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType, document, - cookieSandbox, baseURI) { + +/** + * Save translator items + * @constructor + * @param {Object} options + * <li>libraryID - ID of library in which items should be saved</li> + * <li>attachmentMode - One of Zotero.Translate.ItemSaver.ATTACHMENT_* specifying how attachments should be saved</li> + * <li>forceTagType - Force tags to specified tag type</li> + * <li>cookieSandbox - Cookie sandbox for attachment requests</li> + * <li>baseURI - URI to which attachment paths should be relative</li> + */ +Zotero.Translate.ItemSaver = function(options) { // initialize constants - this.newItems = []; - this.newCollections = []; this._IDMap = {}; // determine library ID - if(libraryID === false) { - this._libraryID = false; - } else if(libraryID === true || libraryID == undefined) { - this._libraryID = null; + if(!options.libraryID) { + this._libraryID = Zotero.Libraries.userLibraryID; } else { - this._libraryID = libraryID; + this._libraryID = options.libraryID; } - this.attachmentMode = attachmentMode; - // If group filesEditable==false, don't save attachments - if (typeof this._libraryID == 'number') { - var type = Zotero.Libraries.getType(this._libraryID); - switch (type) { - case 'group': - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this._libraryID); - var group = Zotero.Groups.get(groupID); - if (!group.filesEditable) { - this.attachmentMode = Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE; - } - break; - } - } - - // force tag types if requested - this._forceTagType = forceTagType; - // to set cookies on downloaded files - this._cookieSandbox = cookieSandbox; + this.attachmentMode = Zotero.Libraries.isFilesEditable(this._libraryID) ? options.attachmentMode : + Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE; + this._forceTagType = options.forceTagType; + this._cookieSandbox = options.cookieSandbox; // the URI to which other URIs are assumed to be relative if(typeof baseURI === "object" && baseURI instanceof Components.interfaces.nsIURI) { - this._baseURI = baseURI; + this._baseURI = options.baseURI; } else { // try to convert to a URI - this._baseURI = null; try { this._baseURI = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService).newURI(baseURI, null, null); + getService(Components.interfaces.nsIIOService).newURI(options.baseURI, null, null); } catch(e) {}; } }; @@ -83,130 +73,146 @@ Zotero.Translate.ItemSaver.prototype = { * @param items Items in Zotero.Item.toArray() format * @param {Function} callback A callback to be executed when saving is complete. If saving * succeeded, this callback will be passed true as the first argument and a list of items - * saved as the second. If - saving failed, the callback will be passed false as the first + * saved as the second. If saving failed, the callback will be passed false as the first * argument and an error object as the second * @param {Function} [attachmentCallback] A callback that receives information about attachment * save progress. The callback will be called as attachmentCallback(attachment, false, error) * on failure or attachmentCallback(attachment, progressPercent) periodically during saving. */ "saveItems":function(items, callback, attachmentCallback) { - Zotero.DB.executeTransaction(function* () { + Zotero.spawn(function* () { try { - var newItems = []; - for each(var item in items) { - // Get typeID, defaulting to "webpage" - var newItem; - var type = (item.itemType ? item.itemType : "webpage"); - - if(type == "note") { // handle notes differently - newItem = new Zotero.Item('note'); - newItem.libraryID = this._libraryID; - if(item.note) newItem.setNote(item.note); - var myID = newItem.save(); - newItem = Zotero.Items.get(myID); - } else { - if(type == "attachment") { // handle attachments differently - newItem = this._saveAttachment(item, null, attachmentCallback); - if(!newItem) continue; - var myID = newItem.id; + let newItems = [], standaloneAttachments = []; + yield (Zotero.DB.executeTransaction(function* () { + for (let iitem=0; iitem<items.length; iitem++) { + let item = items[iitem], newItem, myID; + // Type defaults to "webpage" + let type = (item.itemType ? item.itemType : "webpage"); + + if (type == "note") { // handle notes differently + newItem = yield this._saveNote(item); + } else if (type == "attachment") { // handle attachments differently + standaloneAttachments.push(iitem); + continue; } else { - var typeID = Zotero.ItemTypes.getID(type); - newItem = new Zotero.Item(typeID); - newItem._libraryID = this._libraryID; - - this._saveFields(item, newItem); + newItem = new Zotero.Item(type); + newItem.libraryID = this._libraryID; + if(item.tags) item.tags = this._cleanTags(item.tags); - // handle creators - if(item.creators) { - newItem.setCreators(item.creators); - } + // Need to handle these specially. Put them in a separate object to + // avoid a warning from fromJSON() + let specialFields = { + attachments:item.attachments, + notes:item.notes, + seeAlso:item.seeAlso, + id:item.itemID || item.id + }; + if (item.version) item.versionNumber = item.version; + newItem.fromJSON(this._deleteIrrelevantFields(item)); // save item - var myID = yield newItem.save(); - newItem = yield Zotero.Items.getAsync(myID); + myID = yield newItem.save(); // handle notes - if(item.notes) { - this._saveNotes(item, myID); + if (specialFields.notes) { + for (let i=0; i<specialFields.notes.length; i++) { + yield this._saveNote(specialFields.notes[i], myID); + } } // handle attachments - if(item.attachments) { - for(var i=0; i<item.attachments.length; i++) { - var newAttachment = this._saveAttachment(item.attachments[i], myID, attachmentCallback); - if(typeof newAttachment === "object") { - this._saveTags(item.attachments[i], newAttachment); - } + if (specialFields.attachments) { + for (let i=0; i<specialFields.attachments.length; i++) { + let attachment = specialFields.attachments[i]; + // Don't wait for the promise to resolve, since we want to + // signal completion as soon as the items are saved + this._saveAttachment(attachment, myID, attachmentCallback); } + // Restore the attachments field, since we use it later in + // translation + item.attachments = specialFields.attachments; } - } - } - if(item.itemID) this._IDMap[item.itemID] = myID; + // handle see also + this._handleRelated(specialFields, newItem); + } - // handle see also - this._saveTags(item, newItem); + // add to new item list + newItems.push(newItem); + } + }.bind(this))); - // add to new item list - newItem = Zotero.Items.get(myID); - newItems.push(newItem); + // Handle standalone attachments outside of the transaction + for (let iitem of standaloneAttachments) { + let newItem = yield this._saveAttachment(items[iitem], null, attachmentCallback); + if (newItem) newItems.push(newItem); } - + callback(true, newItems); } catch(e) { callback(false, e); } - }.bind(this)); + }, this); }, - "saveCollection": Zotero.Promise.coroutine(function* (collection) { - var collectionsToProcess = [collection]; + "saveCollections": Zotero.Promise.coroutine(function* (collections) { + var collectionsToProcess = collections.slice(); var parentIDs = [null]; - var topLevelCollection; - - while(collectionsToProcess.length) { - var collection = collectionsToProcess.shift(); - var parentID = parentIDs.shift(); - - var newCollection = new Zotero.Collection; - newCollection.libraryID = this._libraryID; - newCollection.name = collection.name; - if (parentID) { - newCollection.parentID = parentID; - } - yield newCollection.save(); - - if(parentID === null) topLevelCollection = newCollection; - - this.newCollections.push(newCollection.id); - - var toAdd = []; - - for(var i=0; i<collection.children.length; i++) { - var child = collection.children[i]; - if(child.type === "collection") { - // do recursive processing of collections - collectionsToProcess.push(child); - parentIDs.push(newCollection.id); - } else { - // add mapped items to collection - if(this._IDMap[child.id]) { - toAdd.push(this._IDMap[child.id]); + var topLevelCollections = []; + + yield Zotero.DB.executeTransaction(function* () { + while(collectionsToProcess.length) { + var collection = collectionsToProcess.shift(); + var parentID = parentIDs.shift(); + + var newCollection = new Zotero.Collection; + newCollection.libraryID = this._libraryID; + newCollection.name = collection.name; + if (parentID) { + newCollection.parentID = parentID; + } + yield newCollection.save(); + + if(parentID === null) topLevelCollections.push(newCollection); + + var toAdd = []; + + for(var i=0; i<collection.children.length; i++) { + var child = collection.children[i]; + if(child.type === "collection") { + // do recursive processing of collections + collectionsToProcess.push(child); + parentIDs.push(newCollection.id); } else { - Zotero.debug("Translate: Could not map "+child.id+" to an imported item", 2); + // add mapped items to collection + if(this._IDMap[child.id]) { + toAdd.push(this._IDMap[child.id]); + } else { + Zotero.debug("Translate: Could not map "+child.id+" to an imported item", 2); + } } } + + if(toAdd.length) { + Zotero.debug("Translate: Adding " + toAdd, 5); + yield newCollection.addItems(toAdd); + } } - - if(toAdd.length) { - Zotero.debug("Translate: Adding " + toAdd, 5); - yield newCollection.addItems(toAdd); - } - } - - return topLevelCollection; + }.bind(this)); + + return topLevelCollections; }), + + /** + * Deletes irrelevant fields from an item object to avoid warnings in Item#fromJSON + * Also delete some things like dateAdded, dateModified, and path that translators + * should not be able to set directly. + */ + "_deleteIrrelevantFields": function(item) { + const DELETE_FIELDS = ["attachments", "notes", "dateAdded", "dateModified", "seeAlso", "version", "id", "itemID", "path"]; + for (let i=0; i<DELETE_FIELDS.length; i++) delete item[DELETE_FIELDS[i]]; + return item; + }, /** * Saves a translator attachment to the database @@ -220,44 +226,38 @@ Zotero.Translate.ItemSaver.prototype = { * @return {Zotero.Primise<Zotero.Item|False} Flase is returned if attachment * was not saved due to error or user settings. */ - "_saveAttachment": function(attachment, parentID, attachmentCallback) { - // determine whether to save files and attachments - let attachmentPromise; - if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) { - attachmentPromise = this._saveAttachmentDownload.apply(this, arguments); - } else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) { - attachmentPromise = this._saveAttachmentFile.apply(this, arguments); - } else { - Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE'); - return Zotero.Promise.resolve(false); - } - - return attachmentPromise - .then(function(attachmentItem) { - if (!attachmentItem) return false; // attachmentCallback should not have been called in this case - - // save fields - attachment.itemType = "attachment"; - this._saveFields(attachment, attachmentItem); - - // add note if necessary - if(attachment.note) { - attachmentItem.setNote(attachment.note); + "_saveAttachment": Zotero.Promise.coroutine(function* (attachment, parentID, attachmentCallback) { + try { + let newAttachment; + + // determine whether to save files and attachments + if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) { + newAttachment = yield this._saveAttachmentDownload.apply(this, arguments); + } else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) { + newAttachment = yield this._saveAttachmentFile.apply(this, arguments); + } else { + Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE'); + return false; } - return attachmentItem.save() - .then(function() { - Zotero.debug("Translate: Created attachment; id is " + attachmentItem.id, 4); - attachmentCallback(attachment, 100); - return attachmentItem; - }) - }.bind(this)) - .catch(function(e) { + if (!newAttachment) return false; // attachmentCallback should not have been called in this case + + // save fields + if (attachment.accessDate) newAttachment.setField("accessDate", attachment.accessDate); + if (attachment.tags) newAttachment.setTags(this._cleanTags(attachment.tags)); + if (attachment.note) newAttachment.setNote(attachment.note); + this._handleRelated(attachment, newAttachment); + yield newAttachment.saveTx(); + + Zotero.debug("Translate: Created attachment; id is " + newAttachment.id, 4); + attachmentCallback(attachment, 100); + return newAttachment; + } catch(e) { Zotero.debug(e, 2); attachmentCallback(attachment, false, e); return false; - }); - }, + } + }), "_saveAttachmentFile": Zotero.Promise.coroutine(function* (attachment, parentID, attachmentCallback) { Zotero.debug("Translate: Adding attachment", 4); @@ -278,60 +278,39 @@ Zotero.Translate.ItemSaver.prototype = { } } - let done = false, - newItem; - if (attachment.path) { - var file = this._parsePath(attachment.path); - if(!file) { + let newItem; + var file = attachment.path && this._parsePath(attachment.path); + if (!file) { + if (attachment.path) { let asUrl = Zotero.Attachments.cleanAttachmentURI(attachment.path); if (!attachment.url && !asUrl) { throw new Error("Translate: Could not parse attachment path <" + attachment.path + ">"); - } else if (!attachment.url && asUrl) { + } + + if (!attachment.url && asUrl) { Zotero.debug("Translate: attachment path looks like a URI: " + attachment.path); attachment.url = asUrl; delete attachment.path; } - } else { - if (attachment.url) { - attachment.linkMode = "imported_url"; - newItem = yield Zotero.Attachments.importSnapshotFromFile({ - file: file, - url: attachment.url, - title: attachment.title, - contentType: attachment.mimeType, - charset: attachment.charset, - parentItemID: parentID - }); - } - else { - attachment.linkMode = "imported_file"; - newItem = yield Zotero.Attachments.importFromFile({ - file: file, - parentItemID: parentID - }); - } - done = true; } - } - - if(!done) { + let url = Zotero.Attachments.cleanAttachmentURI(attachment.url); if (!url) { throw new Error("Translate: Invalid attachment.url specified <" + attachment.url + ">"); } - + attachment.url = url; url = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService) .newURI(url, null, null); // This cannot fail, since we check above - + // see if this is actually a file URL if(url.scheme == "file") { throw new Error("Translate: Local file attachments cannot be specified in attachment.url"); } else if(url.scheme != "http" && url.scheme != "https") { throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators."); } - + // At this point, must be a valid HTTP/HTTPS url attachment.linkMode = "linked_file"; newItem = yield Zotero.Attachments.linkFromURL({ @@ -339,7 +318,27 @@ Zotero.Translate.ItemSaver.prototype = { parentItemID: parentID, contentType: attachment.mimeType || undefined, title: attachment.title || undefined - }) + }); + } else { + if (attachment.url) { + attachment.linkMode = "imported_url"; + newItem = yield Zotero.Attachments.importSnapshotFromFile({ + file: file, + url: attachment.url, + title: attachment.title, + contentType: attachment.mimeType, + charset: attachment.charset, + parentItemID: parentID + }); + } + else { + attachment.linkMode = "imported_file"; + newItem = yield Zotero.Attachments.importFromFile({ + file: file, + parentItemID: parentID + }); + if (attachment.title) newItem.setField("title", attachment.title); + } } return newItem; @@ -562,8 +561,11 @@ Zotero.Translate.ItemSaver.prototype = { // Import from URL let mimeType = attachment.mimeType ? attachment.mimeType : null; - let parentItem = yield Zotero.Items.getAsync(parentID); - let fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); + let fileBaseName; + if (parentID) { + let parentItem = yield Zotero.Items.getAsync(parentID); + fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); + } Zotero.debug('Importing attachment from URL'); attachment.linkMode = "imported_url"; @@ -578,125 +580,61 @@ Zotero.Translate.ItemSaver.prototype = { fileBaseName: fileBaseName, contentType: mimeType, cookieSandbox: this._cookieSandbox - }) + }); }), - "_saveFields":function(item, newItem) { - // fields that should be handled differently - const skipFields = ["note", "notes", "itemID", "attachments", "tags", "seeAlso", - "itemType", "complete", "creators"]; - - var typeID = Zotero.ItemTypes.getID(item.itemType); - var fieldID; - for(var field in item) { - // loop through item fields - if(item[field] && skipFields.indexOf(field) === -1 && (fieldID = Zotero.ItemFields.getID(field))) { - // if field is in db and shouldn't be skipped - - // try to map from base field - if(Zotero.ItemFields.isBaseField(fieldID)) { - fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(typeID, fieldID); - - // Skip mapping if item field already exists - var fieldName = Zotero.ItemFields.getName(fieldID); - if(fieldName !== field && item[fieldName]) continue; - - if(fieldID) { - Zotero.debug("Translate: Mapping "+field+" to "+fieldName, 5); - } - } - - // if field is valid for this type, set field - if(fieldID && Zotero.ItemFields.isValidForType(fieldID, typeID)) { - newItem.setField(fieldID, item[field]); - } else { - Zotero.debug("Translate: Discarded field "+field+" for item: field not valid for type "+item.itemType, 3); - } - } + "_saveNote":Zotero.Promise.coroutine(function* (note, parentID) { + var myNote = new Zotero.Item('note'); + myNote.libraryID = this._libraryID; + if(parentID) { + myNote.parentID = parentID; } - }, - - "_saveNotes":function(item, parentID) { - for(var i=0; i<item.notes.length; i++) { - var note = item.notes[i]; - if(!note) continue; - var myNote = new Zotero.Item('note'); - myNote.libraryID = this._libraryID; - myNote.setNote(typeof note == "object" ? note.note : note); - if(parentID) { - myNote.parentID = parentID; - } - var noteID = myNote.save(); - - if(typeof note == "object") { - // handle see also - myNote = Zotero.Items.get(noteID); - this._saveTags(note, myNote); - } + + if(typeof note == "object") { + myNote.setNote(note.note); + if(note.tags) myNote.setTags(this._cleanTags(note.tags)); + this._handleRelated(note, myNote); + } else { + myNote.setNote(note); + } + yield myNote.save(); + return myNote; + }), + + /** + * Remove automatic tags if automatic tags pref is on, and set type + * to automatic if forced + */ + "_cleanTags":function(tags) { + // If all tags are automatic and automatic tags pref is on, return immediately + let tagPref = Zotero.Prefs.get("automaticTags"); + if(this._forceTagType == 1 && !tagPref) return []; + + let newTags = []; + for(let i=0; i<tags.length; i++) { + let tag = tags[i]; + tag.type = this._forceTagType || tag.type || 0; + newTags.push(tag); } + return newTags; }, - "_saveTags":function(item, newItem) { + "_handleRelated":function(item, newItem) { // add to ID map - if(item.itemID) { - this._IDMap[item.itemID] = newItem.id; - } - - // add see alsos - if(item.seeAlso) { - for(var i=0; i<item.seeAlso.length; i++) { - var seeAlso = item.seeAlso[i]; - if(this._IDMap[seeAlso]) { - newItem.addRelatedItem(this._IDMap[seeAlso]); - } - } - newItem.save(); - } - - // if all tags are automatic and automatic tags pref is on, return immediately - var tagPref = Zotero.Prefs.get("automaticTags"); - if(this._forceTagType == 1 && !tagPref) return; - - // add tags - if(item.tags) { - var tagsToAdd = {}; - tagsToAdd[0] = []; // user tags - tagsToAdd[1] = []; // automatic tags - - for(var i=0; i<item.tags.length; i++) { - var tag = item.tags[i]; - - if(typeof(tag) == "string") { - // accept strings in tag array as automatic tags, or, if - // importing, as non-automatic tags - if(this._forceTagType) { - tagsToAdd[this._forceTagType].push(tag); - } else { - tagsToAdd[0].push(tag); - } - } else if(typeof(tag) == "object") { - // also accept objects - if(tag.tag || tag.name) { - if(this._forceTagType) { - var tagType = this._forceTagType; - } else if(tag.type) { - // skip automatic tags during import too (?) - if(tag.type == 1 && !tagPref) continue; - var tagType = tag.type; - } else { - var tagType = 0; - } - tagsToAdd[tagType].push(tag.tag ? tag.tag : tag.name); - } - } - } - - for (var type in [0, 1]) { - if (tagsToAdd[type].length) { - newItem.addTags(tagsToAdd[type], type); - } - } + if(item.itemID || item.id) { + this._IDMap[item.itemID || item.id] = newItem.id; } + + // // add see alsos + // if(item.seeAlso) { + // for(var i=0; i<item.seeAlso.length; i++) { + // var seeAlso = item.seeAlso[i]; + // if(this._IDMap[seeAlso]) { + // newItem.addRelatedItem(this._IDMap[seeAlso]); + // } + // } + // newItem.save(); + // } } } @@ -863,7 +801,7 @@ Zotero.Translate.ItemGetter.prototype = { // The only attachments that can have multiple supporting files are imported // attachments of mime type text/html (specified in Attachments.getNumFiles()) - if(attachment.attachmentMIMEType == "text/html" + if(attachment.attachmentContentType == "text/html" && linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE && Zotero.Attachments.getNumFiles(attachment) > 1) { // Attachment is a snapshot with supporting files. Check if any of the diff --git a/test/runtests.sh b/test/runtests.sh @@ -105,6 +105,7 @@ user_pref("extensions.zotero.debug.time", $DEBUG); user_pref("extensions.zotero.firstRunGuidance", false); user_pref("extensions.zotero.firstRun2", false); user_pref("extensions.zotero.reportTranslationFailure", false); +user_pref("extensions.zotero.httpServer.enabled", true); EOF # -v flag on Windows makes Firefox process hang diff --git a/test/tests/data/snapshot/index.html b/test/tests/data/snapshot/index.html @@ -0,0 +1,16 @@ +<html> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=windows-1252"> + + +<meta name="wpd_version" content="0.2"> +<meta name="wpd_baseurl" content="http://209.141.35.188/"> +<meta name="wpd_url" content="http://209.141.35.188/"> +<meta name="wpd_date" content="2015-05-05T00:06Z"> +</head> +<body><h1>It works!</h1> +<p>This is the default web page for this server.</p> +<p>The web server software is running but no content has been added, yet.</p> + +</body> +</html> diff --git a/test/tests/translateTest.js b/test/tests/translateTest.js @@ -1,5 +1,551 @@ +new function() { Components.utils.import("resource://gre/modules/osfile.jsm"); +/** + * Build a dummy translator that can be passed to Zotero.Translate + */ +function buildDummyTranslator(translatorType, code) { + let info = { + "translatorID":"dummy-translator", + "translatorType":1, // import + "label":"Dummy Translator", + "creator":"Simon Kornblith", + "target":"", + "priority":100, + "browserSupport":"g", + "inRepository":false, + "lastUpdated":"0000-00-00 00:00:00", + }; + let translator = new Zotero.Translator(info); + translator.code = code; + return translator; +} + +/** + * Create a new translator that saves the specified items + * @param {String} translatorType - "import" or "web" + * @param {Object} items - items as translator JSON + */ +function saveItemsThroughTranslator(translatorType, items) { + let tyname; + if (translatorType == "web") { + tyname = "Web"; + } else if (translatorType == "import") { + tyname = "Import"; + } else { + throw new Error("invalid translator type "+translatorType); + } + + let translate = new Zotero.Translate[tyname](); + let browser; + if (translatorType == "web") { + browser = Zotero.Browser.createHiddenBrowser(); + translate.setDocument(browser.contentDocument); + } else if (translatorType == "import") { + translate.setString(""); + } + translate.setTranslator(buildDummyTranslator(translatorType == "web" ? 4 : 1, + "function detectWeb() {}\n"+ + "function do"+tyname+"() {\n"+ + " var json = JSON.parse('"+JSON.stringify(items).replace(/'/g, "\'")+"');\n"+ + " for (var i=0; i<json.length; i++) {"+ + " var item = new Zotero.Item;\n"+ + " for (var field in json[i]) { item[field] = json[i][field]; }\n"+ + " item.complete();\n"+ + " }\n"+ + "}")); + return translate.translate().then(function(items) { + if (browser) Zotero.Browser.deleteHiddenBrowser(browser); + return items; + }); +} + +/** + * Convert an array of items to an object in which they are indexed by + * their display titles + */ +var itemsArrayToObject = Zotero.Promise.coroutine(function* itemsArrayToObject(items) { + var obj = {}; + for (let item of items) { + obj[yield item.loadDisplayTitle(true)] = item; + } + return obj; +}); + +const TEST_TAGS = [ + "manual tag as string", + {"tag":"manual tag as object"}, + {"tag":"manual tag as object with type", "type":0}, + {"tag":"automatic tag as object", "type":1}, + {"name":"tag in name property"} +]; + +/** + * Check that tags match expected values, if TEST_TAGS is passed as test array + */ +function checkTestTags(newItem, web) { + assert.equal(newItem.getTagType("manual tag as string"), web ? 1 : 0); + assert.equal(newItem.getTagType("manual tag as object"), web ? 1 : 0); + assert.equal(newItem.getTagType("manual tag as object with type"), web ? 1 : 0); + assert.equal(newItem.getTagType("automatic tag as object"), 1); + assert.equal(newItem.getTagType("tag in name property"), web ? 1 : 0); +} + +/** + * Get included test snapshot file + * @returns {nsIFile} + */ +function getTestSnapshot() { + let snapshot = getTestDataDirectory(); + snapshot.append("snapshot"); + snapshot.append("index.html"); + return snapshot; +} + +/** + * Get included test snapshot file + * @returns {nsIFile} + */ +function getTestPDF() { + let testPDF = getTestDataDirectory(); + testPDF.append("empty.pdf"); + return testPDF; +} + +/** + * Set up endpoints for testing attachment saving + * This must happen immediately before the test, since Zotero might get + * restarted by resetDB(), which would erase our registered endpoints. + */ +function setupAttachmentEndpoints() { + var SnapshotTest = function() {}; + Zotero.Server.Endpoints["/test/translate/test.html"] = SnapshotTest; + SnapshotTest.prototype = { + "supportedMethods":["GET"], + "init":function(data, sendResponseCallback) { + Zotero.File.getBinaryContentsAsync(getTestSnapshot()).then(function (data) { + sendResponseCallback(200, "text/html", data); + }); + } + } + var PDFTest = function() {}; + Zotero.Server.Endpoints["/test/translate/test.pdf"] = PDFTest; + PDFTest.prototype = { + "supportedMethods":["GET"], + "init":function(data, sendResponseCallback) { + Zotero.File.getBinaryContentsAsync(getTestPDF()).then(function (data) { + sendResponseCallback(200, "application/pdf", data); + }); + } + } + var NonExistentTest = function() {}; + Zotero.Server.Endpoints["/test/translate/does_not_exist.html"] = NonExistentTest; + NonExistentTest.prototype = { + "supportedMethods":["GET"], + "init":function(data, sendResponseCallback) { + sendResponseCallback(404, "text/html", "File does not exist"); + } + } +} + +describe("Zotero.Translate", function() { + let win; + before(function* () { + setupAttachmentEndpoints(); + win = yield loadBrowserWindow(); + }); + after(function () { + win.close(); + }); + + describe("Zotero.Item", function() { + it('should save ordinary fields and creators', function* () { + let data = loadSampleData('allTypesAndFields'); + let trueItems = loadSampleData('itemJSON'); + let saveItems = []; + for (let itemType in data) { + saveItems.push(data[itemType]); + let trueItem = trueItems[itemType]; + delete trueItem.dateAdded; + delete trueItem.dateModified; + delete trueItem.key; + } + + let newItems = yield saveItemsThroughTranslator("import", saveItems); + let savedItems = {}; + for (let i=0; i<newItems.length; i++) { + let savedItem = yield newItems[i].toJSON(); + savedItems[Zotero.ItemTypes.getName(newItems[i].itemTypeID)] = savedItem; + delete savedItem.dateAdded; + delete savedItem.dateModified; + delete savedItem.key; + } + assert.deepEqual(savedItems, trueItems, "saved items match inputs"); + }); + + it('should save tags', function* () { + let myItem = { + "itemType":"book", + "title":"Test Item", + "tags":TEST_TAGS + }; + checkTestTags((yield saveItemsThroughTranslator("import", [myItem]))[0]); + }); + + it('should save notes', function* () { + let myItems = [ + { + "itemType":"book", + "title":"Test Item", + "notes":[ + "1 note as string", + { + "note":"2 note as object", + "tags":TEST_TAGS + } + ] + }, + { + "itemType":"note", + "note":"standalone note", + "tags":TEST_TAGS + } + ]; + + let newItems = yield itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); + let noteIDs = newItems["Test Item"].getNotes(); + let note1 = yield Zotero.Items.getAsync(noteIDs[0]); + assert.equal(Zotero.ItemTypes.getName(note1.itemTypeID), "note"); + assert.equal(note1.getNote(), "1 note as string"); + let note2 = yield Zotero.Items.getAsync(noteIDs[1]); + assert.equal(Zotero.ItemTypes.getName(note2.itemTypeID), "note"); + assert.equal(note2.getNote(), "2 note as object"); + checkTestTags(note2); + let note3 = newItems["standalone note"]; + assert.equal(note3.getNote(), "standalone note"); + checkTestTags(note3); + }); + + it('should save collections', function* () { + let translate = new Zotero.Translate.Import(); + translate.setString(""); + translate.setTranslator(buildDummyTranslator(4, + 'function detectWeb() {}\n'+ + 'function doImport() {\n'+ + ' var item1 = new Zotero.Item("book");\n'+ + ' item1.title = "Not in Collection";\n'+ + ' item1.complete();\n'+ + ' var item2 = new Zotero.Item("book");\n'+ + ' item2.id = 1;\n'+ + ' item2.title = "In Parent Collection";\n'+ + ' item2.complete();\n'+ + ' var item3 = new Zotero.Item("book");\n'+ + ' item3.id = 2;\n'+ + ' item3.title = "In Child Collection";\n'+ + ' item3.complete();\n'+ + ' var collection = new Zotero.Collection();\n'+ + ' collection.name = "Parent Collection";\n'+ + ' collection.children = [{"id":1}, {"type":"collection", "name":"Child Collection", "children":[{"id":2}]}];\n'+ + ' collection.complete();\n'+ + '}')); + let newItems = yield translate.translate(); + assert.equal(newItems.length, 3); + newItems = yield itemsArrayToObject(newItems); + assert.equal(newItems["Not in Collection"].getCollections().length, 0); + + let parentCollection = newItems["In Parent Collection"].getCollections(); + assert.equal(parentCollection.length, 1); + parentCollection = (yield Zotero.Collections.getAsync(parentCollection))[0]; + assert.equal(parentCollection.name, "Parent Collection"); + assert.isTrue(parentCollection.hasChildCollections()); + + let childCollection = newItems["In Child Collection"].getCollections(); + assert.equal(childCollection.length, 1); + childCollection = (yield Zotero.Collections.getAsync(childCollection[0])); + assert.equal(childCollection.name, "Child Collection"); + let parentChildren = parentCollection.getChildCollections(); + assert.equal(parentChildren.length, 1); + assert.equal(parentChildren[0], childCollection); + }); + + it('import translators should save attachments', function* () { + let emptyPDF = getTestPDF().path; + let snapshot = getTestSnapshot().path; + let myItems = [ + { + "itemType":"attachment", + "path":emptyPDF, + "title":"Empty PDF", + "note":"attachment note", + "tags":TEST_TAGS + }, + { + "itemType":"attachment", + "url":"http://www.zotero.org/", + "title":"Link to zotero.org", + "note":"attachment 2 note", + "tags":TEST_TAGS + } + ]; + let childAttachments = myItems.slice(); + childAttachments.push({ + "itemType":"attachment", + "path":snapshot, + "url":"http://www.example.com/", + "title":"Snapshot", + "note":"attachment 3 note", + "tags":TEST_TAGS + }); + myItems.push({ + "itemType":"book", + "title":"Container Item", + "attachments":childAttachments + }); + + let newItems = yield itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); + let containedAttachments = yield Zotero.Items.getAsync(newItems["Container Item"].getAttachments()); + assert.equal(containedAttachments.length, 3); + + for (let savedAttachments of [[newItems["Empty PDF"], newItems["Link to zotero.org"]], + [containedAttachments[0], containedAttachments[1]]]) { + assert.equal(savedAttachments[0].getField("title"), "Empty PDF"); + assert.equal(savedAttachments[0].getNote(), "attachment note"); + assert.equal(savedAttachments[0].attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_FILE); + checkTestTags(savedAttachments[0]); + + assert.equal(savedAttachments[1].getField("title"), "Link to zotero.org"); + assert.equal(savedAttachments[1].getField("url"), "http://www.zotero.org/"); + assert.equal(savedAttachments[1].getNote(), "attachment 2 note"); + assert.equal(savedAttachments[1].attachmentLinkMode, Zotero.Attachments.LINK_MODE_LINKED_URL); + checkTestTags(savedAttachments[1]); + } + + assert.equal(containedAttachments[2].getField("title"), "Snapshot"); + assert.equal(containedAttachments[2].getField("url"), "http://www.example.com/"); + assert.equal(containedAttachments[2].getNote(), "attachment 3 note"); + assert.equal(containedAttachments[2].attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_URL); + checkTestTags(containedAttachments[2]); + }); + + it('import translators should save missing snapshots as links', function* () { + let missingFile = getTestDataDirectory(); + missingFile.append("missing"); + assert.isFalse(missingFile.exists()); + missingFile = missingFile.path; + let myItems = [ + { + "itemType":"book", + "title":"Container Item", + "attachments":[ + { + "itemType":"attachment", + "path":missingFile, + "url":"http://www.example.com/", + "title":"Snapshot with missing file", + "note":"attachment note", + "tags":TEST_TAGS + } + ] + } + ]; + + let newItems = yield saveItemsThroughTranslator("import", myItems); + assert.equal(newItems.length, 1); + assert.equal(newItems[0].getField("title"), "Container Item"); + let containedAttachments = yield Zotero.Items.getAsync(newItems[0].getAttachments()); + assert.equal(containedAttachments.length, 1); + + assert.equal(containedAttachments[0].getField("title"), "Snapshot with missing file"); + assert.equal(containedAttachments[0].getField("url"), "http://www.example.com/"); + assert.equal(containedAttachments[0].getNote(), "attachment note"); + assert.equal(containedAttachments[0].attachmentLinkMode, Zotero.Attachments.LINK_MODE_LINKED_URL); + checkTestTags(containedAttachments[0]); + }); + + it('import translators should ignore missing file attachments', function* () { + let missingFile = getTestDataDirectory(); + missingFile.append("missing"); + assert.isFalse(missingFile.exists()); + missingFile = missingFile.path; + let myItems = [ + { + "itemType":"attachment", + "path":missingFile, + "title":"Missing file" + }, + { + "itemType":"book", + "title":"Container Item", + "attachments":[ + { + "itemType":"attachment", + "path":missingFile, + "title":"Missing file" + } + ] + } + ]; + + let newItems = yield saveItemsThroughTranslator("import", myItems); + assert.equal(newItems.length, 1); + assert.equal(newItems[0].getField("title"), "Container Item"); + assert.equal(newItems[0].getAttachments().length, 0); + }); + + it('web translators should save attachments', function* () { + let myItems = [ + { + "itemType":"book", + "title":"Container Item", + "attachments":[ + { + "url":"http://www.zotero.org/", + "title":"Link to zotero.org", + "note":"attachment note", + "tags":TEST_TAGS, + "snapshot":false + }, + { + "url":"http://127.0.0.1:23119/test/translate/test.html", + "title":"Test Snapshot", + "note":"attachment 2 note", + "tags":TEST_TAGS + }, + { + "url":"http://127.0.0.1:23119/test/translate/test.pdf", + "title":"Test PDF", + "note":"attachment 3 note", + "tags":TEST_TAGS + } + ] + } + ]; + + let newItems = yield saveItemsThroughTranslator("web", myItems); + assert.equal(newItems.length, 1); + let containedAttachments = yield itemsArrayToObject(yield Zotero.Items.getAsync(newItems[0].getAttachments())); + + let link = containedAttachments["Link to zotero.org"]; + assert.equal(link.getField("url"), "http://www.zotero.org/"); + assert.equal(link.getNote(), "attachment note"); + assert.equal(link.attachmentLinkMode, Zotero.Attachments.LINK_MODE_LINKED_URL); + checkTestTags(link, true); + + let snapshot = containedAttachments["Test Snapshot"]; + assert.equal(snapshot.getField("url"), "http://127.0.0.1:23119/test/translate/test.html"); + assert.equal(snapshot.getNote(), "attachment 2 note"); + assert.equal(snapshot.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_URL); + assert.equal(snapshot.attachmentContentType, "text/html"); + checkTestTags(snapshot, true); + + let pdf = containedAttachments["Test PDF"]; + assert.equal(pdf.getField("url"), "http://127.0.0.1:23119/test/translate/test.pdf"); + assert.equal(pdf.getNote(), "attachment 3 note"); + assert.equal(pdf.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_URL); + assert.equal(pdf.attachmentContentType, "application/pdf"); + checkTestTags(pdf, true); + }); + + it('web translators should save attachment from document', function* () { + let deferred = Zotero.Promise.defer(); + let browser = Zotero.HTTP.processDocuments("http://127.0.0.1:23119/test/translate/test.html", + function (doc) { deferred.resolve(doc) }, undefined, + undefined, true); + let doc = yield deferred.promise; + + let translate = new Zotero.Translate.Web(); + translate.setDocument(doc); + translate.setTranslator(buildDummyTranslator(4, + 'function detectWeb() {}\n'+ + 'function doWeb(doc) {\n'+ + ' var item = new Zotero.Item("book");\n'+ + ' item.title = "Container Item";\n'+ + ' item.attachments = [{\n'+ + ' "document":doc,\n'+ + ' "title":"Snapshot from Document",\n'+ + ' "note":"attachment note",\n'+ + ' "tags":'+JSON.stringify(TEST_TAGS)+'\n'+ + ' }];\n'+ + ' item.complete();\n'+ + '}')); + let newItems = yield translate.translate(); + assert.equal(newItems.length, 1); + let containedAttachments = yield Zotero.Items.getAsync(newItems[0].getAttachments()); + assert.equal(containedAttachments.length, 1); + + let snapshot = containedAttachments[0]; + assert.equal(snapshot.getField("url"), "http://127.0.0.1:23119/test/translate/test.html"); + assert.equal(snapshot.getNote(), "attachment note"); + assert.equal(snapshot.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_URL); + assert.equal(snapshot.attachmentContentType, "text/html"); + checkTestTags(snapshot, true); + + Zotero.Browser.deleteHiddenBrowser(browser); + }); + + it('web translators should ignore attachments that return error codes', function* () { + this.timeout(60000); + let myItems = [ + { + "itemType":"book", + "title":"Container Item", + "attachments":[ + { + "url":"http://127.0.0.1:23119/test/translate/does_not_exist.html", + "title":"Non-Existent HTML" + }, + { + "url":"http://127.0.0.1:23119/test/translate/does_not_exist.pdf", + "title":"Non-Existent PDF" + } + ] + } + ]; + + let newItems = yield saveItemsThroughTranslator("web", myItems); + assert.equal(newItems.length, 1); + let containedAttachments = yield Zotero.Items.getAsync(newItems[0].getAttachments()); + assert.equal(containedAttachments.length, 0); + }); + + it('web translators should save PDFs only if the content type matches', function* () { + this.timeout(60000); + let myItems = [ + { + "itemType":"book", + "title":"Container Item", + "attachments":[ + { + "url":"http://127.0.0.1:23119/test/translate/test.html", + "mimeType":"application/pdf", + "title":"Test PDF with wrong mime type" + }, + { + "url":"http://127.0.0.1:23119/test/translate/test.pdf", + "mimeType":"application/pdf", + "title":"Test PDF", + "note":"attachment note", + "tags":TEST_TAGS + } + ] + } + ]; + + let newItems = yield saveItemsThroughTranslator("web", myItems); + assert.equal(newItems.length, 1); + let containedAttachments = yield Zotero.Items.getAsync(newItems[0].getAttachments()); + assert.equal(containedAttachments.length, 1); + + let pdf = containedAttachments[0]; + assert.equal(pdf.getField("title"), "Test PDF"); + assert.equal(pdf.getField("url"), "http://127.0.0.1:23119/test/translate/test.pdf"); + assert.equal(pdf.getNote(), "attachment note"); + assert.equal(pdf.attachmentLinkMode, Zotero.Attachments.LINK_MODE_IMPORTED_URL); + checkTestTags(pdf, true); + }); + }); +}); + describe("Zotero.Translate.ItemGetter", function() { describe("nextItem", function() { it('should return false for an empty database', Zotero.Promise.coroutine(function* () { @@ -380,9 +926,8 @@ describe("Zotero.Translate.ItemGetter", function() { it('should return stored/linked file and URI attachments in expected format', Zotero.Promise.coroutine(function* () { this.timeout(60000); - let file = getTestDataDirectory(); + let file = getTestPDF(); let item, relatedItem; - file.append("empty.pdf"); yield Zotero.DB.executeTransaction(function* () { item = new Zotero.Item('journalArticle'); @@ -587,4 +1132,5 @@ describe("Zotero.Translate.ItemGetter", function() { } })); }); -}); -\ No newline at end of file +}); +} +\ No newline at end of file