www

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

translate_item.js (33262B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2012 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 
     27 /**
     28  * Save translator items
     29  * @constructor
     30  * @param {Object} options
     31  *         <li>libraryID - ID of library in which items should be saved</li>
     32  *         <li>collections - New collections to create (used during Import translation</li>
     33  *         <li>attachmentMode - One of Zotero.Translate.ItemSaver.ATTACHMENT_* specifying how attachments should be saved</li>
     34  *         <li>forceTagType - Force tags to specified tag type</li>
     35  *         <li>cookieSandbox - Cookie sandbox for attachment requests</li>
     36  *         <li>proxy - A proxy to deproxify item URLs</li>
     37  *         <li>baseURI - URI to which attachment paths should be relative</li>
     38  *         <li>saveOptions - Options to pass to DataObject::save() (e.g., skipSelect)</li>
     39  */
     40 Zotero.Translate.ItemSaver = function(options) {
     41 	// initialize constants
     42 	this._IDMap = {};
     43 	
     44 	// determine library ID
     45 	if(!options.libraryID) {
     46 		this._libraryID = Zotero.Libraries.userLibraryID;
     47 	} else {
     48 		this._libraryID = options.libraryID;
     49 	}
     50 	
     51 	this._collections = options.collections || false;
     52 	
     53 	// If group filesEditable==false, don't save attachments
     54 	this.attachmentMode = Zotero.Libraries.get(this._libraryID).filesEditable ? options.attachmentMode :
     55 	                      Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE;
     56 	this._forceTagType = options.forceTagType;
     57 	this._referrer = options.referrer;
     58 	this._cookieSandbox = options.cookieSandbox;
     59 	this._proxy = options.proxy;
     60 	
     61 	// the URI to which other URIs are assumed to be relative
     62 	if(typeof options.baseURI === "object" && options.baseURI instanceof Components.interfaces.nsIURI) {
     63 		this._baseURI = options.baseURI;
     64 	} else {
     65 		// try to convert to a URI
     66 		try {
     67 			this._baseURI = Components.classes["@mozilla.org/network/io-service;1"].
     68 				getService(Components.interfaces.nsIIOService).newURI(options.baseURI, null, null);
     69 		} catch(e) {};
     70 	}
     71 	this._saveOptions = options.saveOptions || {};
     72 };
     73 
     74 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0;
     75 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
     76 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
     77 
     78 Zotero.Translate.ItemSaver.prototype = {
     79 	/**
     80 	 * Saves items to Standalone or the server
     81 	 * @param items Items in Zotero.Item.toArray() format
     82 	 * @param {Function} [attachmentCallback] A callback that receives information about attachment
     83 	 *     save progress. The callback will be called as attachmentCallback(attachment, false, error)
     84 	 *     on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
     85 	 * @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are
     86 	 * done saving with a list of items. Will include saved notes, but exclude attachments.
     87 	 */
     88 	saveItems: Zotero.Promise.coroutine(function* (items, attachmentCallback, itemsDoneCallback) {
     89 		let newItems = [], standaloneAttachments = [], childAttachments = [];
     90 		yield Zotero.DB.executeTransaction(function* () {
     91 			for (let iitem=0; iitem<items.length; iitem++) {
     92 				let item = items[iitem], newItem, myID;
     93 				// Type defaults to "webpage"
     94 				let type = (item.itemType ? item.itemType : "webpage");
     95 				
     96 				if (type == "note") {				// handle notes differently
     97 					newItem = yield this._saveNote(item);
     98 				}
     99 				// Handle standalone attachments differently
    100 				else if (type == "attachment") {
    101 					if (this._canSaveAttachment(item)) {
    102 						standaloneAttachments.push(item);
    103 						attachmentCallback(item, 0);
    104 					}
    105 					continue;
    106 				} else {
    107 					newItem = new Zotero.Item(type);
    108 					newItem.libraryID = this._libraryID;
    109 					if (item.creators) this._cleanCreators(item.creators);
    110 					if(item.tags) item.tags = this._cleanTags(item.tags);
    111 					
    112 					if (item.accessDate == 'CURRENT_TIMESTAMP') {
    113 						item.accessDate = Zotero.Date.dateToISO(new Date());
    114 					}
    115 
    116 					// Need to handle these specially. Put them in a separate object to
    117 					// avoid a warning from fromJSON()
    118 					let specialFields = {
    119 						attachments:item.attachments,
    120 						notes:item.notes,
    121 						seeAlso:item.seeAlso,
    122 						id:item.itemID || item.id
    123 					};
    124 					newItem.fromJSON(this._deleteIrrelevantFields(item));
    125 					
    126 					// deproxify url
    127 					if (this._proxy && item.url) {
    128 						let url = this._proxy.toProper(item.url);
    129 						Zotero.debug(`Deproxifying item url ${item.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
    130 						newItem.setField('url', url);
    131 					}
    132 					
    133 					if (this._collections) {
    134 						newItem.setCollections(this._collections);
    135 					}
    136 					
    137 					// save item
    138 					myID = yield newItem.save(this._saveOptions);
    139 
    140 					// handle notes
    141 					if (specialFields.notes) {
    142 						for (let i=0; i<specialFields.notes.length; i++) {
    143 							yield this._saveNote(specialFields.notes[i], myID);
    144 						}
    145 						item.notes = specialFields.notes;
    146 					}
    147 
    148 					// handle attachments
    149 					if (specialFields.attachments) {
    150 						for (let attachment of specialFields.attachments) {
    151 							if (!this._canSaveAttachment(attachment)) {
    152 								continue;
    153 							}
    154 							attachmentCallback(attachment, 0);
    155 							childAttachments.push([attachment, myID]);
    156 						}
    157 						// Restore the attachments field, since we use it later in
    158 						// translation
    159 						item.attachments = specialFields.attachments;
    160 					}
    161 
    162 					// handle see also
    163 					this._handleRelated(specialFields, newItem);
    164 				}
    165 
    166 				// add to new item list
    167 				newItems.push(newItem);
    168 			}
    169 		}.bind(this));
    170 		
    171 		if (itemsDoneCallback) {
    172 			itemsDoneCallback(newItems.splice());
    173 		}
    174 		
    175 		// Handle attachments outside of the transaction, because they can involve downloading
    176 		for (let item of standaloneAttachments) {
    177 			let newItem = yield this._saveAttachment(item, null, attachmentCallback);
    178 			if (newItem) newItems.push(newItem);
    179 		}
    180 		for (let a of childAttachments) {
    181 			// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=449811 (fixed in Fx51?)
    182 			let [item, parentItemID] = a;
    183 			yield this._saveAttachment(item, parentItemID, attachmentCallback);
    184 		}
    185 		
    186 		return newItems;
    187 	}),
    188 	
    189 	"saveCollections": Zotero.Promise.coroutine(function* (collections) {
    190 		var collectionsToProcess = collections.slice();
    191 		// Use first collection passed to translate process as the root
    192 		var rootCollectionID = (this._collections && this._collections.length)
    193 			? this._collections[0] : null;
    194 		var parentIDs = collections.map(c => null);
    195 		var topLevelCollections = [];
    196 
    197 		yield Zotero.DB.executeTransaction(function* () {
    198 			while(collectionsToProcess.length) {
    199 				var collection = collectionsToProcess.shift();
    200 				var parentID = parentIDs.shift();
    201 
    202 				var newCollection = new Zotero.Collection;
    203 				newCollection.libraryID = this._libraryID;
    204 				newCollection.name = collection.name;
    205 				if (parentID) {
    206 					newCollection.parentID = parentID;
    207 				}
    208 				else {
    209 					newCollection.parentID = rootCollectionID;
    210 					topLevelCollections.push(newCollection)
    211 				}
    212 				yield newCollection.save(this._saveOptions);
    213 
    214 				var toAdd = [];
    215 
    216 				for(var i=0; i<collection.children.length; i++) {
    217 					var child = collection.children[i];
    218 					if(child.type === "collection") {
    219 						// do recursive processing of collections
    220 						collectionsToProcess.push(child);
    221 						parentIDs.push(newCollection.id);
    222 					} else {
    223 						// add mapped items to collection
    224 						if(this._IDMap[child.id]) {
    225 							toAdd.push(this._IDMap[child.id]);
    226 						} else {
    227 							Zotero.debug("Translate: Could not map "+child.id+" to an imported item", 2);
    228 						}
    229 					}
    230 				}
    231 
    232 				if(toAdd.length) {
    233 					Zotero.debug("Translate: Adding " + toAdd, 5);
    234 					yield newCollection.addItems(toAdd);
    235 				}
    236 			}
    237 		}.bind(this));
    238 
    239 		return topLevelCollections;
    240 	}),
    241 
    242 	/**
    243 	 * Deletes irrelevant fields from an item object to avoid warnings in Item#fromJSON
    244 	 * Also delete some things like dateAdded, dateModified, and path that translators
    245 	 * should not be able to set directly.
    246 	 */
    247 	"_deleteIrrelevantFields": function(item) {
    248 		const DELETE_FIELDS = ["attachments", "notes", "dateAdded", "dateModified", "seeAlso", "version", "id", "itemID", "path"];
    249 		for (let i=0; i<DELETE_FIELDS.length; i++) delete item[DELETE_FIELDS[i]];
    250 		return item;
    251 	},
    252 	
    253 	
    254 	_canSaveAttachment: function (attachment) {
    255 		if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
    256 			if (!attachment.url && !attachment.document) {
    257 				Zotero.debug("Translate: Not adding attachment: no URL specified");
    258 				return false;
    259 			}
    260 			if (attachment.snapshot !== false) {
    261 				if (attachment.document || Zotero.MIME.isWebPageType(attachment.mimeType)) {
    262 					if (!Zotero.Prefs.get("automaticSnapshots")) {
    263 						Zotero.debug("Translate: Not adding attachment: automatic snapshots are disabled");
    264 						return false;
    265 					}
    266 				}
    267 				else {
    268 					if (!Zotero.Prefs.get("downloadAssociatedFiles")) {
    269 						Zotero.debug("Translate: Not adding attachment: automatic file attachments are disabled");
    270 						return false;
    271 					}
    272 				}
    273 			}
    274 			return true;
    275 		}
    276 		else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
    277 			return true;
    278 		}
    279 		Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
    280 		return false;
    281 	},
    282 	
    283 	
    284 	/**
    285 	 * Saves a translator attachment to the database
    286 	 *
    287 	 * @param {Translator Attachment} attachment
    288 	 * @param {Integer} parentItemID - Item to attach to
    289 	 * @param {Function} attachmentCallback Callback function that takes three
    290 	 *   parameters: translator attachment object, percent completion (integer),
    291 	 *   and an optional error object
    292 	 *
    293 	 * @return {Zotero.Primise<Zotero.Item|False} Flase is returned if attachment
    294 	 *   was not saved due to error or user settings.
    295 	 */
    296 	_saveAttachment: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
    297 		try {
    298 			let newAttachment;
    299 
    300 			// determine whether to save files and attachments
    301 			if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
    302 				newAttachment = yield this._saveAttachmentDownload.apply(this, arguments);
    303 			} else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
    304 				newAttachment = yield this._saveAttachmentFile.apply(this, arguments);
    305 			} else {
    306 				Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
    307 			}
    308 			
    309 			if (!newAttachment) return false; // attachmentCallback should not have been called in this case
    310 			
    311 			// deproxify url
    312 			let url = newAttachment.getField('url');
    313 			if (this._proxy && url) {
    314 				newAttachment.setField('url', this._proxy.toProper(url));
    315 			}
    316 
    317 			// save fields
    318 			if (attachment.accessDate) newAttachment.setField("accessDate", attachment.accessDate);
    319 			if (attachment.tags) newAttachment.setTags(this._cleanTags(attachment.tags));
    320 			if (attachment.note) newAttachment.setNote(attachment.note);
    321 			this._handleRelated(attachment, newAttachment);
    322 			yield newAttachment.saveTx(this._saveOptions);
    323 
    324 			Zotero.debug("Translate: Created attachment; id is " + newAttachment.id, 4);
    325 			attachmentCallback(attachment, 100);
    326 			return newAttachment;
    327 		} catch(e) {
    328 			Zotero.debug(e, 2);
    329 			attachmentCallback(attachment, false, e);
    330 			return false;
    331 		}
    332 	}),
    333 	
    334 	_saveAttachmentFile: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
    335 		Zotero.debug("Translate: Adding attachment", 4);
    336 		attachmentCallback(attachment, 0);
    337 		
    338 		if(!attachment.url && !attachment.path) {
    339 			throw new Error("Translate: Ignoring attachment: no path or URL specified");
    340 		}
    341 		
    342 		if (attachment.path) {
    343 			var url = Zotero.Attachments.cleanAttachmentURI(attachment.path, false);
    344 			if (url && /^(?:https?|ftp):/.test(url)) {
    345 				// A web URL. Don't bother parsing it as path below
    346 				// Some paths may look like URIs though, so don't just test for 'file'
    347 				// E.g. C:\something
    348 				if (!attachment.url) attachment.url = attachment.path;
    349 				delete attachment.path;
    350 			}
    351 		}
    352 		
    353 		let newItem;
    354 		var file = attachment.path && this._parsePath(attachment.path);
    355 		if (!file) {
    356 			if (attachment.path) {
    357 				let asUrl = Zotero.Attachments.cleanAttachmentURI(attachment.path);
    358 				if (!attachment.url && !asUrl) {
    359 					throw new Error("Translate: Could not parse attachment path <" + attachment.path + ">");
    360 				}
    361 
    362 				if (!attachment.url && asUrl) {
    363 					Zotero.debug("Translate: attachment path looks like a URI: " + attachment.path);
    364 					attachment.url = asUrl;
    365 					delete attachment.path;
    366 				}
    367 			}
    368 
    369 			let url = Zotero.Attachments.cleanAttachmentURI(attachment.url);
    370 			if (!url) {
    371 				throw new Error("Translate: Invalid attachment.url specified <" + attachment.url + ">");
    372 			}
    373 
    374 			attachment.url = url;
    375 			url = Components.classes["@mozilla.org/network/io-service;1"]
    376 				.getService(Components.interfaces.nsIIOService)
    377 				.newURI(url, null, null); // This cannot fail, since we check above
    378 
    379 			// see if this is actually a file URL
    380 			if(url.scheme == "file") {
    381 				throw new Error("Translate: Local file attachments cannot be specified in attachment.url");
    382 			} else if(url.scheme != "http" && url.scheme != "https") {
    383 				throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
    384 			}
    385 
    386 			// At this point, must be a valid HTTP/HTTPS url
    387 			attachment.linkMode = "linked_file";
    388 			newItem = yield Zotero.Attachments.linkFromURL({
    389 				url: attachment.url,
    390 				parentItemID,
    391 				contentType: attachment.mimeType || undefined,
    392 				title: attachment.title || undefined,
    393 				collections: !parentItemID ? this._collections : undefined
    394 			});
    395 		} else {
    396 			if (attachment.url) {
    397 				attachment.linkMode = "imported_url";
    398 				newItem = yield Zotero.Attachments.importSnapshotFromFile({
    399 					file: file,
    400 					url: attachment.url,
    401 					title: attachment.title,
    402 					contentType: attachment.mimeType,
    403 					charset: attachment.charset,
    404 					parentItemID,
    405 					collections: !parentItemID ? this._collections : undefined
    406 				});
    407 			}
    408 			else {
    409 				attachment.linkMode = "imported_file";
    410 				newItem = yield Zotero.Attachments.importFromFile({
    411 					file: file,
    412 					parentItemID,
    413 					collections: !parentItemID ? this._collections : undefined
    414 				});
    415 				if (attachment.title) newItem.setField("title", attachment.title);
    416 			}
    417 		}
    418 		
    419 		return newItem;
    420 	}),
    421 
    422 	"_parsePathURI":function(path) {
    423 		try {
    424 			var uri = Services.io.newURI(path, "", this._baseURI);
    425 		} catch(e) {
    426 			Zotero.debug("Translate: " + path + " is not a valid URI");
    427 			return false;
    428 		}
    429 		
    430 		try {
    431 			var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
    432 		}
    433 		catch (e) {
    434 			Zotero.debug("Translate: " + uri.spec + " is not a file URI");
    435 			return false;
    436 		}
    437 		
    438 		if(file.path == '/') {
    439 			Zotero.debug("Translate: " + path + " points to root directory");
    440 			return false;
    441 		}
    442 		
    443 		if(!file.exists()) {
    444 			Zotero.debug("Translate: File at " + file.path + " does not exist");
    445 			return false;
    446 		}
    447 		
    448 		return file;
    449 	},
    450 
    451 	"_parseAbsolutePath":function(path) {
    452 		var file = Components.classes["@mozilla.org/file/local;1"].
    453 			createInstance(Components.interfaces.nsILocalFile);
    454 		try {
    455 			file.initWithPath(path);
    456 		} catch(e) {
    457 			Zotero.debug("Translate: Invalid absolute path: " + path);
    458 			return false;
    459 		}
    460 		
    461 		if(!file.exists()) {
    462 			Zotero.debug("Translate: File at absolute path " + file.path + " does not exist");
    463 			return false;
    464 		}
    465 		
    466 		return file;
    467 	},
    468 
    469 	"_parseRelativePath":function(path) {
    470 		if (!this._baseURI) {
    471 			Zotero.debug("Translate: Cannot parse as relative path. No base URI available.");
    472 			return false;
    473 		}
    474 		
    475 		var file = this._baseURI.QueryInterface(Components.interfaces.nsIFileURL).file.parent;
    476 		var splitPath = path.split(/\//g);
    477 		for(var i=0; i<splitPath.length; i++) {
    478 			if(splitPath[i] !== "") file.append(splitPath[i]);
    479 		}
    480 		
    481 		if(!file.exists()) {
    482 			Zotero.debug("Translate: File at " + file.path + " does not exist");
    483 			return false;
    484 		}
    485 		
    486 		return file;
    487 	},
    488 
    489 	"_parsePath":function(path) {
    490 		Zotero.debug("Translate: Attempting to parse path " + path);
    491 		
    492 		var file;
    493 
    494 		// First, try to parse as absolute path
    495 		if((/^[a-zA-Z]:[\\\/]|^\\\\/.test(path) && Zotero.isWin) // Paths starting with drive letter or network shares starting with \\
    496 			|| (path[0] === "/" && !Zotero.isWin)) {
    497 			// Forward slashes on Windows are not allowed in filenames, so we can
    498 			// assume they're meant to be backslashes. Backslashes are technically
    499 			// allowed on Linux, so the reverse cannot be done reliably.
    500 			var nativePath = Zotero.isWin ? path.replace('/', '\\', 'g') : path;
    501 			if (file = this._parseAbsolutePath(nativePath)) {
    502 				Zotero.debug("Translate: Got file "+nativePath+" as absolute path");
    503 				return file;
    504 			}
    505 		}
    506 
    507 		// Next, try to parse as URI
    508 		if((file = this._parsePathURI(path))) {
    509 			Zotero.debug("Translate: Got "+path+" as URI")
    510 			return file;
    511 		} else if(path.substr(0, 7) !== "file://") {
    512 			// If it was a fully qualified file URI, we can give up now
    513 
    514 			// Next, try to parse as relative path, replacing backslashes with slashes
    515 			if((file = this._parseRelativePath(path.replace(/\\/g, "/")))) {
    516 				Zotero.debug("Translate: Got file "+path+" as relative path");
    517 				return file;
    518 			}
    519 
    520 			// Next, try to parse as relative path, without replacing backslashes with slashes
    521 			if((file = this._parseRelativePath(path))) {
    522 				Zotero.debug("Translate: Got file "+path+" as relative path");
    523 				return file;
    524 			}
    525 
    526 			if(path[0] !== "/") {
    527 				// Next, try to parse a path with no / as an absolute URI or path
    528 				if((file = this._parsePathURI("/"+path))) {
    529 					Zotero.debug("Translate: Got file "+path+" as broken URI");
    530 					return file;
    531 				}
    532 
    533 				if((file = this._parseAbsolutePath("/"+path))) {
    534 					Zotero.debug("Translate: Got file "+path+" as broken absolute path");
    535 					return file;
    536 				}
    537 
    538 			}
    539 		}
    540 
    541 		// Give up
    542 		Zotero.debug("Translate: Could not find file "+path)
    543 
    544 		return false;
    545 	},
    546 	
    547 	_saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
    548 		Zotero.debug("Translate: Adding attachment", 4);
    549 		
    550 		let doc = undefined;
    551 		if(attachment.document) {
    552 			doc = new XPCNativeWrapper(Zotero.Translate.DOMWrapper.unwrap(attachment.document));
    553 			if(!attachment.title) attachment.title = doc.title;
    554 		}
    555 		
    556 		// If no title provided, use "Attachment" as title for progress UI (but not for item)
    557 		let title = attachment.title || null;
    558 		if(!attachment.title) {
    559 			attachment.title = Zotero.getString("itemTypes.attachment");
    560 		}
    561 		
    562 		// Commit to saving
    563 		attachmentCallback(attachment, 0);
    564 		
    565 		if(attachment.snapshot === false || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
    566 			// if snapshot is explicitly set to false, attach as link
    567 			attachment.linkMode = "linked_url";
    568 			let url, mimeType;
    569 			if(attachment.document) {
    570 				url = attachment.document.location.href;
    571 				mimeType = attachment.mimeType || attachment.document.contentType;
    572 			} else {
    573 				url = attachment.url
    574 				mimeType = attachment.mimeType || undefined;
    575 			}
    576 			
    577 			if(!mimeType || !title) {
    578 				Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
    579 			}
    580 			
    581 			let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
    582 			if (!cleanURI) {
    583 				throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
    584 			}
    585 			url = Components.classes["@mozilla.org/network/io-service;1"]
    586 				.getService(Components.interfaces.nsIIOService)
    587 				.newURI(cleanURI, null, null); // This cannot fail, since we check above
    588 			
    589 			// Only HTTP/HTTPS links are allowed
    590 			if(url.scheme != "http" && url.scheme != "https") {
    591 				throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
    592 			}
    593 			
    594 			return Zotero.Attachments.linkFromURL({
    595 				url: cleanURI,
    596 				parentItemID,
    597 				contentType: mimeType,
    598 				title,
    599 				collections: !parentItemID ? this._collections : undefined
    600 			});
    601 		}
    602 		
    603 		// Snapshot is not explicitly set to false, import as file attachment
    604 		
    605 		// Import from document
    606 		if(attachment.document) {
    607 			Zotero.debug('Importing attachment from document');
    608 			attachment.linkMode = "imported_url";
    609 			
    610 			return Zotero.Attachments.importFromDocument({
    611 				libraryID: this._libraryID,
    612 				document: attachment.document,
    613 				parentItemID,
    614 				title,
    615 				collections: !parentItemID ? this._collections : undefined
    616 			});
    617 		}
    618 		
    619 		// Import from URL
    620 		let mimeType = attachment.mimeType ? attachment.mimeType : null;
    621 		let fileBaseName;
    622 		if (parentItemID) {
    623 			let parentItem = yield Zotero.Items.getAsync(parentItemID);
    624 			fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
    625 		}
    626 		
    627 		Zotero.debug('Importing attachment from URL');
    628 		attachment.linkMode = "imported_url";
    629 		
    630 		attachmentCallback(attachment, 0);
    631 		
    632 		return Zotero.Attachments.importFromURL({
    633 			libraryID: this._libraryID,
    634 			url: attachment.url,
    635 			parentItemID,
    636 			title,
    637 			fileBaseName,
    638 			contentType: mimeType,
    639 			referrer: this._referrer,
    640 			cookieSandbox: this._cookieSandbox,
    641 			collections: !parentItemID ? this._collections : undefined
    642 		});
    643 	}),
    644 	
    645 	"_saveNote":Zotero.Promise.coroutine(function* (note, parentItemID) {
    646 		var myNote = new Zotero.Item('note');
    647 		myNote.libraryID = this._libraryID;
    648 		if (parentItemID) {
    649 			myNote.parentItemID = parentItemID;
    650 		}
    651 
    652 		if(typeof note == "object") {
    653 			myNote.setNote(note.note);
    654 			if(note.tags) myNote.setTags(this._cleanTags(note.tags));
    655 			this._handleRelated(note, myNote);
    656 		} else {
    657 			myNote.setNote(note);
    658 		}
    659 		if (!parentItemID && this._collections) {
    660 			myNote.setCollections(this._collections);
    661 		}
    662 		yield myNote.save(this._saveOptions);
    663 		return myNote;
    664 	}),
    665 	
    666 	_cleanCreators: function (creators) {
    667 		creators.forEach(creator => {
    668 			if (!creator.creatorType) {
    669 				Zotero.warn(".creatorType missing in creator -- update translator code");
    670 				creator.creatorType = "author";
    671 			}
    672 		});
    673 	},
    674 	
    675 	/**
    676 	 * Remove automatic tags if automatic tags pref is on, and set type
    677 	 * to automatic if forced
    678 	 */
    679 	"_cleanTags":function(tags) {
    680 		// If all tags are automatic and automatic tags pref is on, return immediately
    681 		let tagPref = Zotero.Prefs.get("automaticTags");
    682 		if(this._forceTagType == 1 && !tagPref) return [];
    683 
    684 		let newTags = [];
    685 		for(let i=0; i<tags.length; i++) {
    686 			let tag = tags[i];
    687 			// Convert raw string to object with 'tag' property
    688 			if (typeof tag == 'string') {
    689 				tag = { tag };
    690 			}
    691 			tag.type = this._forceTagType || tag.type || 0;
    692 			newTags.push(tag);
    693 		}
    694 		return newTags;
    695 	},
    696 	
    697 	"_handleRelated":function(item, newItem) {
    698 		// add to ID map
    699 		if(item.itemID || item.id) {
    700 			this._IDMap[item.itemID || item.id] = newItem.id;
    701 		}
    702 
    703 		// // add see alsos
    704 		// if(item.seeAlso) {
    705 		// 	for(var i=0; i<item.seeAlso.length; i++) {
    706 		// 		var seeAlso = item.seeAlso[i];
    707 		// 		if(this._IDMap[seeAlso]) {
    708 		// 			newItem.addRelatedItem(this._IDMap[seeAlso]);
    709 		// 		}
    710 		// 	}
    711 		// 	newItem.save();
    712 		// }
    713 	}
    714 }
    715 
    716 Zotero.Translate.ItemGetter = function() {
    717 	this._itemsLeft = [];
    718 	this._collectionsLeft = null;
    719 	this._exportFileDirectory = null;
    720 	this.legacy = false;
    721 };
    722 
    723 Zotero.Translate.ItemGetter.prototype = {
    724 	"setItems":function(items) {
    725 		this._itemsLeft = items;
    726 		this._itemsLeft.sort(function(a, b) { return a.id - b.id; });
    727 		this.numItems = this._itemsLeft.length;
    728 	},
    729 	
    730 	"setCollection": function (collection, getChildCollections) {
    731 		// get items in this collection
    732 		var items = new Set(collection.getChildItems());
    733 		
    734 		if(getChildCollections) {
    735 			// get child collections
    736 			this._collectionsLeft = Zotero.Collections.getByParent(collection.id, true);
    737 			
    738 			// get items in child collections
    739 			for (let collection of this._collectionsLeft) {
    740 				var childItems = collection.getChildItems();
    741 				childItems.forEach(item => items.add(item));
    742 			}
    743 		}
    744 		
    745 		this._itemsLeft = Array.from(items.values());
    746 		this._itemsLeft.sort(function(a, b) { return a.id - b.id; });
    747 		this.numItems = this._itemsLeft.length;
    748 	},
    749 	
    750 	"setAll": Zotero.Promise.coroutine(function* (libraryID, getChildCollections) {
    751 		this._itemsLeft = yield Zotero.Items.getAll(libraryID, true);
    752 		
    753 		if(getChildCollections) {
    754 			this._collectionsLeft = Zotero.Collections.getByLibrary(libraryID, true);
    755 		}
    756 		
    757 		this._itemsLeft.sort(function(a, b) { return a.id - b.id; });
    758 		this.numItems = this._itemsLeft.length;
    759 	}),
    760 	
    761 	"exportFiles":function(dir, extension) {
    762 		// generate directory
    763 		this._exportFileDirectory = Components.classes["@mozilla.org/file/local;1"].
    764 		                createInstance(Components.interfaces.nsILocalFile);
    765 		this._exportFileDirectory.initWithFile(dir.parent);
    766 		
    767 		// delete this file if it exists
    768 		if(dir.exists()) {
    769 			dir.remove(true);
    770 		}
    771 		
    772 		// get name
    773 		var name = dir.leafName;
    774 		this._exportFileDirectory.append(name);
    775 		
    776 		// create directory
    777 		this._exportFileDirectory.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o700);
    778 		
    779 		// generate a new location for the exported file, with the appropriate
    780 		// extension
    781 		var location = Components.classes["@mozilla.org/file/local;1"].
    782 		                createInstance(Components.interfaces.nsILocalFile);
    783 		location.initWithFile(this._exportFileDirectory);
    784 		location.append(name+"."+extension);
    785 
    786 		return location;
    787 	},
    788 	
    789 	/**
    790 	 * Converts an attachment to array format and copies it to the export folder if desired
    791 	 */
    792 	"_attachmentToArray": function (attachment) {
    793 		var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy);
    794 		var linkMode = attachment.attachmentLinkMode;
    795 		if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
    796 			attachmentArray.localPath = attachment.getFilePath();
    797 			
    798 			if(this._exportFileDirectory) {
    799 				var exportDir = this._exportFileDirectory;
    800 				
    801 				// Add path and filename if not an internet link
    802 				let attachFile;
    803 				if (attachmentArray.localPath) {
    804 					attachFile = Zotero.File.pathToFile(attachmentArray.localPath);
    805 				}
    806 				else {
    807 					Zotero.logError(`Path doesn't exist for attachment ${attachment.libraryKey} `
    808 						+ '-- not exporting file');
    809 				}
    810 				// TODO: Make async, but that will require translator changes
    811 				if (attachFile && attachFile.exists()) {
    812 					attachmentArray.defaultPath = "files/" + attachment.id + "/" + attachFile.leafName;
    813 					attachmentArray.filename = attachFile.leafName;
    814 					
    815 					/**
    816 					 * Copies the attachment file to the specified relative path from the
    817 					 * export directory.
    818 					 * @param {String} attachPath The path to which the file should be exported 
    819 					 *    including the filename. If supporting files are included, they will be
    820 					 *    copied as well without any renaming. 
    821 					 * @param {Boolean} overwriteExisting Optional - If this is set to false, the
    822 					 *    function will throw an error when exporting a file would require an existing
    823 					 *    file to be overwritten. If true, the file will be silently overwritten.
    824 					 *    defaults to false if not provided. 
    825 					 */
    826 					attachmentArray.saveFile = function(attachPath, overwriteExisting) {
    827 						// Ensure a valid path is specified
    828 						if(attachPath === undefined || attachPath == "") {
    829 							throw new Error("ERROR_EMPTY_PATH");
    830 						}
    831 						
    832 						// Set the default value of overwriteExisting if it was not provided
    833 						if (overwriteExisting === undefined) {
    834 							overwriteExisting = false;
    835 						}
    836 						
    837 						// Separate the path into a list of subdirectories and the attachment filename,
    838 						// and initialize the required file objects
    839 						var targetFile = Components.classes["@mozilla.org/file/local;1"].
    840 								createInstance(Components.interfaces.nsILocalFile);
    841 						targetFile.initWithFile(exportDir);
    842 						for (let dir of attachPath.split("/")) targetFile.append(dir);
    843 						
    844 						// First, check that we have not gone lower than exportDir in the hierarchy
    845 						var parent = targetFile, inExportFileDirectory;
    846 						while((parent = parent.parent)) {
    847 							if(exportDir.equals(parent)) {
    848 								inExportFileDirectory = true;
    849 								break;
    850 							}
    851 						}
    852 						
    853 						if(!inExportFileDirectory) {
    854 							throw new Error("Invalid path; attachment cannot be placed above export "+
    855 								"directory in the file hirarchy");
    856 						}
    857 						
    858 						// Create intermediate directories if they don't exist
    859 						parent = targetFile;
    860 						while((parent = parent.parent) && !parent.exists()) {
    861 							parent.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o700);
    862 						}
    863 						
    864 						// Delete any existing file if overwriteExisting is set, or throw an exception
    865 						// if it is not
    866 						if(targetFile.exists()) {
    867 							if(overwriteExisting) {
    868 								targetFile.remove(false);
    869 							} else {
    870 								throw new Error("ERROR_FILE_EXISTS " + targetFile.leafName);
    871 							}
    872 						}
    873 						
    874 						var directory = targetFile.parent;
    875 						
    876 						// The only attachments that can have multiple supporting files are imported
    877 						// attachments of mime type text/html
    878 						//
    879 						// TEMP: This used to check getNumFiles() here, but that's now async.
    880 						// It could be restored (using hasMultipleFiles()) when this is made
    881 						// async, but it's probably not necessary. (The below can also be changed
    882 						// to use OS.File.DirectoryIterator.)
    883 						if(attachment.attachmentContentType == "text/html"
    884 								&& linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
    885 							// Attachment is a snapshot with supporting files. Check if any of the
    886 							// supporting files would cause a name conflict, and build a list of transfers
    887 							// that should be performed
    888 							var copySrcs = [];
    889 							var files = attachment.getFile().parent.directoryEntries;
    890 							while (files.hasMoreElements()) {
    891 								file = files.getNext();
    892 								file.QueryInterface(Components.interfaces.nsIFile);
    893 								
    894 								// Ignore the main attachment file (has already been checked for name conflict)
    895 								if(attachFile.equals(file)) {
    896 									continue;
    897 								}
    898 								
    899 								// Remove any existing files in the target destination if overwriteExisting 
    900 								// is set, or throw an exception if it is not
    901 								var targetSupportFile = targetFile.parent.clone();
    902 								targetSupportFile.append(file.leafName);
    903 								if(targetSupportFile.exists()) {
    904 									if(overwriteExisting) {
    905 										targetSupportFile.remove(false);
    906 									} else {
    907 										throw new Error("ERROR_FILE_EXISTS " + targetSupportFile.leafName);
    908 									}
    909 								}
    910 								copySrcs.push(file.clone());
    911 							}
    912 							
    913 							// No conflicts were detected or all conflicts were resolved, perform the copying
    914 							attachFile.copyTo(directory, targetFile.leafName);
    915 							for(var i = 0; i < copySrcs.length; i++) {
    916 								copySrcs[i].copyTo(directory, copySrcs[i].leafName);
    917 							}
    918 						} else {
    919 							// Attachment is a single file
    920 							// Copy the file to the specified location
    921 							attachFile.copyTo(directory, targetFile.leafName);
    922 						}
    923 						
    924 						attachmentArray.path = targetFile.path;
    925 					};
    926 				}
    927 			}
    928 		}
    929 		
    930 		return attachmentArray;
    931 	},
    932 	
    933 	/**
    934 	 * Retrieves the next available item
    935 	 */
    936 	"nextItem": function () {
    937 		while(this._itemsLeft.length != 0) {
    938 			var returnItem = this._itemsLeft.shift();
    939 			// export file data for single files
    940 			if(returnItem.isAttachment()) {		// an independent attachment
    941 				var returnItemArray = this._attachmentToArray(returnItem);
    942 				if(returnItemArray) return returnItemArray;
    943 			} else {
    944 				var returnItemArray = Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy);
    945 				
    946 				// get attachments, although only urls will be passed if exportFileData is off
    947 				returnItemArray.attachments = [];
    948 				if (returnItem.isRegularItem()) {
    949 					var attachments = returnItem.getAttachments();
    950 					for (let attachmentID of attachments) {
    951 						var attachment = Zotero.Items.get(attachmentID);
    952 						var attachmentInfo = this._attachmentToArray(attachment);
    953 						
    954 						if(attachmentInfo) {
    955 							returnItemArray.attachments.push(attachmentInfo);
    956 						}
    957 					}
    958 				}
    959 				
    960 				return returnItemArray;
    961 			}
    962 		}
    963 		return false;
    964 	},
    965 	
    966 	"nextCollection":function() {
    967 		if(!this._collectionsLeft || this._collectionsLeft.length == 0) return false;
    968 	
    969 		var returnItem = this._collectionsLeft.shift();
    970 		var obj = returnItem.serialize(true);
    971 		obj.id = obj.primary.collectionID;
    972 		obj.name = obj.fields.name;
    973 		return obj;
    974 	}
    975 }
    976 Zotero.Translate.ItemGetter.prototype.__defineGetter__("numItemsRemaining", function() { return this._itemsLeft.length });