www

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

translate.js (105453B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 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  * @class
     28  * Deprecated class for creating new Zotero.Translate instances<br/>
     29  * <br/>
     30  * New code should use Zotero.Translate.Web, Zotero.Translate.Import, Zotero.Translate.Export, or
     31  * Zotero.Translate.Search
     32  */
     33 Zotero.Translate = function(type) {
     34 	Zotero.debug("Translate: WARNING: new Zotero.Translate() is deprecated; please don't use this if you don't have to");
     35 	// hack
     36 	var translate = Zotero.Translate.newInstance(type);
     37 	for(var i in translate) {
     38 		this[i] = translate[i];
     39 	}
     40 	this.constructor = translate.constructor;
     41 	this.__proto__ = translate.__proto__;
     42 }
     43 
     44 /**
     45  * Create a new translator by a string type
     46  */
     47 Zotero.Translate.newInstance = function(type) {
     48 	return new Zotero.Translate[type.substr(0, 1).toUpperCase()+type.substr(1).toLowerCase()];
     49 }
     50 
     51 /**
     52  * Namespace for Zotero sandboxes
     53  * @namespace
     54  */
     55 Zotero.Translate.Sandbox = {
     56 	/**
     57 	 * Combines a sandbox with the base sandbox
     58 	 */
     59 	"_inheritFromBase":function(sandboxToMerge) {
     60 		var newSandbox = {};
     61 		
     62 		for(var method in Zotero.Translate.Sandbox.Base) {
     63 			newSandbox[method] = Zotero.Translate.Sandbox.Base[method];
     64 		}
     65 		
     66 		for(var method in sandboxToMerge) {
     67 			newSandbox[method] = sandboxToMerge[method];
     68 		}
     69 		
     70 		return newSandbox;
     71 	},
     72 	
     73 	/**
     74 	 * Base sandbox. These methods are available to all translators.
     75 	 * @namespace
     76 	 */
     77 	"Base": {
     78 		/**
     79 		 * Called as {@link Zotero.Item#complete} from translators to save items to the database.
     80 		 * @param {Zotero.Translate} translate
     81 		 * @param {SandboxItem} An item created using the Zotero.Item class from the sandbox
     82 		 */
     83 		_itemDone: function (translate, item) {
     84 			// https://github.com/zotero/translators/issues/1353
     85 			var asyncTranslator = !(translate instanceof Zotero.Translate.Web)
     86 				&& translate.translator[0].configOptions
     87 				&& translate.translator[0].configOptions.async;
     88 			
     89 			var run = async function (async) {
     90 				Zotero.debug("Translate: Saving item");
     91 				
     92 				// warn if itemDone called after translation completed
     93 				if(translate._complete) {
     94 					Zotero.debug("Translate: WARNING: Zotero.Item#complete() called after Zotero.done(); please fix your code", 2);
     95 				}
     96 					
     97 				const allowedObjects = [
     98 					"complete",
     99 					"attachments",
    100 					"creators",
    101 					"tags",
    102 					"notes",
    103 					"relations",
    104 					// Is this still needed?
    105 					"seeAlso"
    106 				];
    107 				
    108 				// Create a new object here, so that we strip the "complete" property
    109 				var newItem = {};
    110 				var oldItem = item;
    111 				for(var i in item) {
    112 					var val = item[i];
    113 					if(i === "complete" || (!val && val !== 0)) continue;
    114 					
    115 					var type = typeof val;
    116 					var isObject = type === "object" || type === "xml" || type === "function",
    117 						shouldBeObject = allowedObjects.indexOf(i) !== -1;
    118 					if(isObject && !shouldBeObject) {
    119 						// Convert things that shouldn't be objects to objects
    120 						translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to string");
    121 						newItem[i] = val.toString();
    122 					} else if(shouldBeObject && !isObject) {
    123 						translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to array");
    124 						newItem[i] = [val];
    125 					} else if(type === "string") {
    126 						// trim strings
    127 						newItem[i] = val.trim();
    128 					} else {
    129 						newItem[i] = val;
    130 					}
    131 				}
    132 				item = newItem;
    133 	
    134 				// Clean empty creators
    135 				if (item.creators) {
    136 					for (var i=0; i<item.creators.length; i++) {
    137 						var creator = item.creators[i];
    138 						if (!creator.firstName && !creator.lastName) {
    139 							item.creators.splice(i, 1);
    140 							i--;
    141 						}
    142 					}
    143 				}
    144 	
    145 				// If we're not in a child translator, canonicalize tags
    146 				if (!translate._parentTranslator) {
    147 					if(item.tags) item.tags = translate._cleanTags(item.tags);
    148 				}
    149 				
    150 				// if we're not supposed to save the item or we're in a child translator,
    151 				// just return the item array
    152 				if(translate._libraryID === false || translate._parentTranslator) {
    153 					translate.newItems.push(item);
    154 					if(translate._parentTranslator && Zotero.isFx && !Zotero.isBookmarklet) {
    155 						// Copy object so it is accessible to parent translator
    156 						item = translate._sandboxManager.copyObject(item);
    157 						item.complete = oldItem.complete;
    158 					}
    159 					return translate._runHandler("itemDone", item, item);
    160 				}
    161 				
    162 				// We use this within the connector to keep track of items as they are saved
    163 				if(!item.id) item.id = Zotero.Utilities.randomString();
    164 				
    165 				if(item.attachments) {
    166 					var attachments = item.attachments;
    167 					for(var j=0; j<attachments.length; j++) {
    168 						var attachment = attachments[j];
    169 	
    170 						// Don't save documents as documents in connector, since we can't pass them around
    171 						if(Zotero.isConnector && attachment.document) {
    172 							attachment.url = attachment.document.documentURI || attachment.document.URL;
    173 							attachment.mimeType = "text/html";
    174 							delete attachment.document;
    175 						}
    176 	
    177 						// If we're not in a child translator, canonicalize tags
    178 						if (!translate._parentTranslator) {
    179 							if(attachment.tags !== undefined) attachment.tags = translate._cleanTags(attachment.tags);
    180 						}
    181 					}
    182 				}
    183 	
    184 				if(item.notes) {
    185 					var notes = item.notes;
    186 					for(var j=0; j<notes.length; j++) {
    187 						var note = notes[j];
    188 						if(!note) {
    189 							notes.splice(j--, 1);
    190 						} else if(typeof(note) != "object") {
    191 							// Convert to object
    192 							notes[j] = {"note":note.toString()}
    193 						}
    194 						// If we're not in a child translator, canonicalize tags
    195 						if (!translate._parentTranslator) {
    196 							if(note.tags !== undefined) note.tags = translate._cleanTags(note.tags);
    197 						}
    198 					}
    199 				}
    200 	
    201 				if (item.version) {
    202 					translate._debug("Translate: item.version is deprecated; set item.versionNumber instead");
    203 					item.versionNumber = item.version;
    204 				}
    205 	
    206 				if (item.accessDate) {
    207 					if (Zotero.Date.isSQLDateTime(item.accessDate)) {
    208 						translate._debug("Translate: Passing accessDate as SQL is deprecated; pass an ISO 8601 date instead");
    209 						item.accessDate = Zotero.Date.sqlToISO8601(item.accessDate);
    210 					}
    211 				}
    212 			
    213 				// Fire itemSaving event
    214 				translate._runHandler("itemSaving", item);
    215 				translate._savingItems++;
    216 				
    217 				// For synchronous import (when Promise isn't available in the sandbox or the do*
    218 				// function doesn't use it) and web translators, queue saves
    219 				if (!async || !asyncTranslator) {
    220 					Zotero.debug("Translate: Saving via queue");
    221 					translate.saveQueue.push(item);
    222 				}
    223 				// For async import, save items immediately
    224 				else {
    225 					Zotero.debug("Translate: Saving now");
    226 					translate.incrementAsyncProcesses("Zotero.Translate#_saveItems()");
    227 					return translate._saveItems([item])
    228 						.then(() => translate.decrementAsyncProcesses("Zotero.Translate#_saveItems()"));
    229 				}
    230 			};
    231 			
    232 			if (!translate._sandboxManager.sandbox.Promise) {
    233 				Zotero.debug("Translate: Promise not available in sandbox in _itemDone()");
    234 				run();
    235 				return;
    236 			}
    237 			
    238 			return new translate._sandboxManager.sandbox.Promise(function (resolve, reject) {
    239 				try {
    240 					run(true).then(
    241 						resolve,
    242 						function (e) {
    243 							// Fix wrapping error from sandbox when error is thrown from _saveItems()
    244 							if (Zotero.isFx) {
    245 								reject(translate._sandboxManager.copyObject(e));
    246 							}
    247 							else {
    248 								reject(e);
    249 							}
    250 						}
    251 					);
    252 				}
    253 				catch (e) {
    254 					reject(e);
    255 				}
    256 			});
    257 		},
    258 		
    259 		/**
    260 		 * Gets translator options that were defined in displayOptions in translator header
    261 		 *
    262 		 * @param {Zotero.Translate} translate
    263 		 * @param {String} option Option to be retrieved
    264 		 */
    265 		"getOption":function(translate, option) {
    266 			if(typeof option !== "string") {
    267 				throw(new Error("getOption: option must be a string"));
    268 				return;
    269 			}
    270 			
    271 			return translate._displayOptions[option];
    272 		},
    273 		
    274 		/**
    275 		 * Gets a hidden preference that can be defined by hiddenPrefs in translator header
    276 		 *
    277 		 * @param {Zotero.Translate} translate
    278 		 * @param {String} pref Prefernce to be retrieved
    279 		 */
    280 		"getHiddenPref":function(translate, pref) {
    281 			if(typeof(pref) != "string") {
    282 				throw(new Error("getPref: preference must be a string"));
    283 			}
    284 
    285 			var hp = translate._translatorInfo.hiddenPrefs || {};
    286 
    287 			var value;
    288 			try {
    289 				value = Zotero.Prefs.get('translators.' + pref);
    290 			} catch(e) {}
    291 
    292 			return (value !== undefined ? value : hp[pref]);
    293 		},
    294 		
    295 		/**
    296 		 * For loading other translators and accessing their methods
    297 		 * 
    298 		 * @param {Zotero.Translate} translate
    299 		 * @param {String} type Translator type ("web", "import", "export", or "search")
    300 		 * @returns {Object} A safeTranslator object, which operates mostly like Zotero.Translate
    301 		 */	 
    302 		"loadTranslator":function(translate, type) {
    303 			const setDefaultHandlers = function(translate, translation) {
    304 				if(type !== "export"
    305 					&& (!translation._handlers['itemDone'] || !translation._handlers['itemDone'].length)) {
    306 					translation.setHandler("itemDone", function(obj, item) {
    307 						translate.Sandbox._itemDone(translate, item);
    308 					});
    309 				}
    310 				if(!translation._handlers['selectItems'] || !translation._handlers['selectItems'].length) {
    311 					translation.setHandler("selectItems", translate._handlers["selectItems"]);
    312 				}
    313 			}
    314 			
    315 			if(typeof type !== "string") {
    316 				throw(new Error("loadTranslator: type must be a string"));
    317 				return;
    318 			}
    319 			
    320 			Zotero.debug("Translate: Creating translate instance of type "+type+" in sandbox");
    321 			var translation = Zotero.Translate.newInstance(type);
    322 			translation._parentTranslator = translate;
    323 			
    324 			if(translation instanceof Zotero.Translate.Export && !(translation instanceof Zotero.Translate.Export)) {
    325 				throw(new Error("Only export translators may call other export translators"));
    326 			}
    327 			
    328 			/**
    329 			 * @class Wrapper for {@link Zotero.Translate} for safely calling another translator 
    330 			 * from inside an existing translator
    331 			 * @inner
    332 			 */
    333 			var safeTranslator = {};
    334 			safeTranslator.__exposedProps__ = {
    335 				"setSearch":"r",
    336 				"setDocument":"r",
    337 				"setHandler":"r",
    338 				"setString":"r",
    339 				"setTranslator":"r",
    340 				"getTranslators":"r",
    341 				"translate":"r",
    342 				"getTranslatorObject":"r"
    343 			};
    344 			safeTranslator.setSearch = function(arg) {
    345 				if(!Zotero.isBookmarklet) arg = JSON.parse(JSON.stringify(arg));
    346 				return translation.setSearch(arg);
    347 			};
    348 			safeTranslator.setDocument = function(arg) {
    349 				if (Zotero.isFx && !Zotero.isBookmarklet) {
    350 					return translation.setDocument(
    351 						Zotero.Translate.DOMWrapper.wrap(arg, arg.SpecialPowers_wrapperOverrides)
    352 					);
    353 				} else {
    354 					return translation.setDocument(arg);
    355 				}
    356 			};
    357 			var errorHandlerSet = false;
    358 			safeTranslator.setHandler = function(arg1, arg2) {
    359 				if(arg1 === "error") errorHandlerSet = true;
    360 				translation.setHandler(arg1, 
    361 					function(obj, item) {
    362 						try {
    363 							item = item.wrappedJSObject ? item.wrappedJSObject : item;
    364 							if(arg1 == "itemDone") {
    365 								item.complete = translate._sandboxZotero.Item.prototype.complete;
    366 							} else if(arg1 == "translators" && Zotero.isFx && !Zotero.isBookmarklet) {
    367 								var translators = new translate._sandboxManager.sandbox.Array();
    368 								translators = translators.wrappedJSObject || translators;
    369 								for (var i=0; i<item.length; i++) {
    370 									translators.push(item[i]);
    371 								}
    372 								item = translators;
    373 							}
    374 							arg2(obj, item);
    375 						} catch(e) {
    376 							translate.complete(false, e);
    377 						}
    378 					}
    379 				);
    380 			};
    381 			safeTranslator.setString = function(arg) { translation.setString(arg) };
    382 			safeTranslator.setTranslator = function(arg) {
    383 				var success = translation.setTranslator(arg);
    384 				if(!success) {
    385 					throw new Error("Translator "+translate.translator[0].translatorID+" attempted to call invalid translatorID "+arg);
    386 				}
    387 			};
    388 			
    389 			var translatorsHandlerSet = false;
    390 			safeTranslator.getTranslators = function() {
    391 				if(!translation._handlers["translators"] || !translation._handlers["translators"].length) {
    392 					throw new Error('Translator must register a "translators" handler to '+
    393 						'call getTranslators() in this translation environment.');
    394 				}
    395 				if(!translatorsHandlerSet) {
    396 					translation.setHandler("translators", function() {
    397 						translate.decrementAsyncProcesses("safeTranslator#getTranslators()");
    398 					});
    399 				}
    400 				translate.incrementAsyncProcesses("safeTranslator#getTranslators()");
    401 				return translation.getTranslators();
    402 			};
    403 			
    404 			var doneHandlerSet = false;
    405 			safeTranslator.translate = function() {
    406 				translate.incrementAsyncProcesses("safeTranslator#translate()");
    407 				setDefaultHandlers(translate, translation);
    408 				if(!doneHandlerSet) {
    409 					doneHandlerSet = true;
    410 					translation.setHandler("done", function() { translate.decrementAsyncProcesses("safeTranslator#translate()") });
    411 				}
    412 				if(!errorHandlerSet) {
    413 					errorHandlerSet = true;
    414 					translation.setHandler("error", function(obj, error) { translate.complete(false, error) });
    415 				}
    416 				translation.translate(false);
    417 			};
    418 			
    419 			safeTranslator.getTranslatorObject = function(callback) {
    420 				if(callback) {
    421 					translate.incrementAsyncProcesses("safeTranslator#getTranslatorObject()");
    422 				} else {
    423 					throw new Error("Translator must pass a callback to getTranslatorObject() to "+
    424 						"operate in this translation environment.");
    425 				}
    426 				
    427 				var translator = translation.translator[0];
    428 				translator = typeof translator === "object" ? translator : Zotero.Translators.get(translator);
    429 				// Zotero.Translators.get returns a value in the client and a promise in connectors
    430 				// so we normalize the value to a promise here
    431 				Zotero.Promise.resolve(translator)
    432 				.then(function(translator) {
    433 					return translation._loadTranslator(translator)
    434 				})
    435 				.then(function() {
    436 					if(Zotero.isFx && !Zotero.isBookmarklet) {
    437 						// do same origin check
    438 						var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
    439 							.getService(Components.interfaces.nsIScriptSecurityManager);
    440 						var ioService = Components.classes["@mozilla.org/network/io-service;1"] 
    441 							.getService(Components.interfaces.nsIIOService);
    442 						
    443 						var outerSandboxURI = ioService.newURI(typeof translate._sandboxLocation === "object" ?
    444 							translate._sandboxLocation.location : translate._sandboxLocation, null, null);
    445 						var innerSandboxURI = ioService.newURI(typeof translation._sandboxLocation === "object" ?
    446 							translation._sandboxLocation.location : translation._sandboxLocation, null, null);
    447 						
    448 						try {
    449 							secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false);
    450 						} catch(e) {
    451 							throw new Error("getTranslatorObject() may not be called from web or search "+
    452 								"translators to web or search translators from different origins.");
    453 							return;
    454 						}
    455 					}
    456 					
    457 					return translation._prepareTranslation();
    458 				})
    459 				.then(function () {
    460 					setDefaultHandlers(translate, translation);
    461 					var sandbox = translation._sandboxManager.sandbox;
    462 					if(!Zotero.Utilities.isEmpty(sandbox.exports)) {
    463 						sandbox.exports.Zotero = sandbox.Zotero;
    464 						sandbox = sandbox.exports;
    465 					} else {
    466 						translate._debug("COMPAT WARNING: "+translation.translator[0].label+" does "+
    467 							"not export any properties. Only detect"+translation._entryFunctionSuffix+
    468 							" and do"+translation._entryFunctionSuffix+" will be available in "+
    469 							"connectors.");
    470 					}
    471 					
    472 					callback(sandbox);
    473 					translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()");
    474 				}).catch(function(e) {
    475 					translate.complete(false, e);
    476 					return;
    477 				});
    478 			};
    479 
    480 			if (Zotero.isFx) {
    481 				for(var i in safeTranslator) {
    482 					if (typeof(safeTranslator[i]) === "function") {
    483 						safeTranslator[i] = translate._sandboxManager._makeContentForwarder(function(func) {
    484 							return function() {
    485 								func.apply(safeTranslator, this.args.wrappedJSObject || this.args);
    486 							}
    487 						}(safeTranslator[i]));
    488 					}
    489 				}
    490 			}
    491 			
    492 			return safeTranslator;
    493 		},
    494 		
    495 		/**
    496 		 * Enables asynchronous detection or translation
    497 		 * @param {Zotero.Translate} translate
    498 		 * @deprecated
    499 		 */
    500 		"wait":function(translate) {},
    501 		
    502 		/**
    503 		 * Sets the return value for detection
    504 		 *
    505 		 * @param {Zotero.Translate} translate
    506 		 */
    507 		"done":function(translate, returnValue) {
    508 			if(translate._currentState === "detect") {
    509 				translate._returnValue = returnValue;
    510 			}
    511 		},
    512 		
    513 		/**
    514 		 * Proxy for translator _debug function
    515 		 * 
    516 		 * @param {Zotero.Translate} translate
    517 		 * @param {String} string String to write to console
    518 		 * @param {String} [level] Level to log as (1 to 5)
    519 		 */
    520 		"debug":function(translate, string, level) {
    521 			translate._debug(string, level);
    522 		}
    523 	},
    524 	
    525 	/**
    526 	 * Web functions exposed to sandbox
    527 	 * @namespace
    528 	 */
    529 	"Web":{
    530 		/**
    531 		 * Lets user pick which items s/he wants to put in his/her library
    532 		 * @param {Zotero.Translate} translate
    533 		 * @param {Object} items An set of id => name pairs in object format
    534 		 */
    535 		"selectItems":function(translate, items, callback) {
    536 			function transferObject(obj) {
    537 				return Zotero.isFx && !Zotero.isBookmarklet ? translate._sandboxManager.copyObject(obj) : obj;
    538 			}
    539 			
    540 			if(Zotero.Utilities.isEmpty(items)) {
    541 				throw new Error("Translator called select items with no items");
    542 			}
    543 			
    544 			// Some translators pass an array rather than an object to Zotero.selectItems.
    545 			// This will break messaging outside of Firefox, so we need to fix it.
    546 			if(Object.prototype.toString.call(items) === "[object Array]") {
    547 				translate._debug("WARNING: Zotero.selectItems should be called with an object, not an array");
    548 				var itemsObj = {};
    549 				for(var i in items) itemsObj[i] = items[i];
    550 				items = itemsObj;
    551 			}
    552 			
    553 			if(translate._selectedItems) {
    554 				// if we have a set of selected items for this translation, use them
    555 				return transferObject(translate._selectedItems);
    556 			} else if(translate._handlers.select) {
    557 					// whether the translator supports asynchronous selectItems
    558 					var haveAsyncCallback = !!callback;
    559 					// whether the handler operates asynchronously
    560 					var haveAsyncHandler = false;
    561 					var returnedItems = null;
    562 					
    563 					var callbackExecuted = false;
    564 					if(haveAsyncCallback) {
    565 						// if this translator provides an async callback for selectItems, rig things
    566 						// up to pop off the async process
    567 						var newCallback = function(selectedItems) {
    568 							callbackExecuted = true;
    569 							callback(transferObject(selectedItems));
    570 							if(haveAsyncHandler) translate.decrementAsyncProcesses("Zotero.selectItems()");
    571 						};
    572 					} else {
    573 						// if this translator doesn't provide an async callback for selectItems, set things
    574 						// up so that we can wait to see if the select handler returns synchronously. If it
    575 						// doesn't, we will need to restart translation.
    576 						var newCallback = function(selectedItems) {
    577 							callbackExecuted = true;
    578 							if(haveAsyncHandler) {
    579 								translate.translate({
    580 									libraryID: translate._libraryID,
    581 									saveAttachments: translate._saveAttachments,
    582 									selectedItems
    583 								});
    584 							} else {
    585 								returnedItems = transferObject(selectedItems);
    586 							}
    587 						};
    588 					}
    589 					
    590 					if(Zotero.isFx && !Zotero.isBookmarklet) {
    591 						items = Components.utils.cloneInto(items, {});
    592 					}
    593 
    594 					var returnValue = translate._runHandler("select", items, newCallback);
    595 					if(returnValue !== undefined) {
    596 						// handler may have returned a value, which makes callback unnecessary
    597 						Zotero.debug("WARNING: Returning items from a select handler is deprecated. "+
    598 							"Please pass items as to the callback provided as the third argument to "+
    599 							"the handler.");
    600 						
    601 						returnedItems = transferObject(returnValue);
    602 						haveAsyncHandler = false;
    603 					} else {
    604 						// if we don't have returnedItems set already, the handler is asynchronous
    605 						haveAsyncHandler = !callbackExecuted;
    606 					}
    607 					
    608 					if(haveAsyncCallback) {
    609 						if(haveAsyncHandler) {
    610 							// we are running asynchronously, so increment async processes
    611 							translate.incrementAsyncProcesses("Zotero.selectItems()");
    612 						} else if(!callbackExecuted) {
    613 							// callback didn't get called from handler, so call it here
    614 							callback(returnedItems);
    615 						}
    616 						return false;
    617 					} else {
    618 						translate._debug("COMPAT WARNING: No callback was provided for "+
    619 							"Zotero.selectItems(). When executed outside of Firefox, a selectItems() call "+
    620 							"will require this translator to be called multiple times.", 1);
    621 						
    622 						if(haveAsyncHandler) {
    623 							// The select handler is asynchronous, but this translator doesn't support
    624 							// asynchronous select. We return false to abort translation in this
    625 							// instance, and we will restart it later when the selectItems call is
    626 							// complete.
    627 							translate._aborted = true;
    628 							return false;
    629 						} else {
    630 							return returnedItems;
    631 						}
    632 					}
    633 			} else { // no handler defined; assume they want all of them
    634 				if(callback) callback(items);
    635 				return items;
    636 			}
    637 		},
    638 		
    639 		/**
    640 		 * Overloads {@link Zotero.Translate.Sandbox.Base._itemDone} to ensure that no standalone
    641 		 * items are saved, that an item type is specified, and to add a libraryCatalog and 
    642 		 * shortTitle if relevant.
    643 		 * @param {Zotero.Translate} translate
    644 		 * @param {SandboxItem} An item created using the Zotero.Item class from the sandbox
    645 		 */
    646 		 "_itemDone":function(translate, item) {
    647 		 	// Only apply checks if there is no parent translator
    648 		 	if(!translate._parentTranslator) {
    649 				if(!item.itemType) {
    650 					item.itemType = "webpage";
    651 					translate._debug("WARNING: No item type specified");
    652 				}
    653 				
    654 				if(item.type == "attachment" || item.type == "note") {
    655 					Zotero.debug("Translate: Discarding standalone "+item.type+" in non-import translator", 2);
    656 					return;
    657 				}
    658 				
    659 				// store library catalog if this item was captured from a website, and
    660 				// libraryCatalog is truly undefined (not false or "")
    661 				if(item.repository !== undefined) {
    662 					Zotero.debug("Translate: 'repository' field is now 'libraryCatalog'; please fix your code", 2);
    663 					item.libraryCatalog = item.repository;
    664 					delete item.repository;
    665 				}
    666 				
    667 				// automatically set library catalog
    668 				if(item.libraryCatalog === undefined && item.itemType != "webpage") {
    669 					item.libraryCatalog = translate.translator[0].label;
    670 				}
    671 							
    672 				// automatically set access date if URL is set
    673 				if(item.url && typeof item.accessDate == 'undefined') {
    674 					item.accessDate = Zotero.Date.dateToISO(new Date());
    675 				}
    676 				
    677 				//consider type-specific "title" alternatives
    678 				var altTitle = Zotero.ItemFields.getName(Zotero.ItemFields.getFieldIDFromTypeAndBase(item.itemType, 'title'));
    679 				if(altTitle && item[altTitle]) item.title = item[altTitle];
    680 				
    681 				if(!item.title) {
    682 					translate.complete(false, new Error("No title specified for item"));
    683 					return;
    684 				}
    685 				
    686 				// create short title
    687 				if(item.shortTitle === undefined && Zotero.Utilities.fieldIsValidForType("shortTitle", item.itemType)) {		
    688 					// only set if changes have been made
    689 					var setShortTitle = false;
    690 					var title = item.title;
    691 					
    692 					// shorten to before first colon
    693 					var index = title.indexOf(":");
    694 					if(index !== -1) {
    695 						title = title.substr(0, index);
    696 						setShortTitle = true;
    697 					}
    698 					// shorten to after first question mark
    699 					index = title.indexOf("?");
    700 					if(index !== -1) {
    701 						index++;
    702 						if(index != title.length) {
    703 							title = title.substr(0, index);
    704 							setShortTitle = true;
    705 						}
    706 					}
    707 					
    708 					if(setShortTitle) item.shortTitle = title;
    709 				}
    710 				
    711 				/* Clean up ISBNs
    712 				 * Allow multiple ISBNs, but...
    713 				 * (1) validate all ISBNs
    714 				 * (2) convert all ISBNs to ISBN-13
    715 				 * (3) remove any duplicates
    716 				 * (4) separate them with space
    717 				 */
    718 				if (item.ISBN) {
    719 					// Match ISBNs with groups separated by various dashes or even spaces
    720 					var isbnRe = /\b(?:97[89][\s\x2D\xAD\u2010-\u2015\u2043\u2212]*)?(?:\d[\s\x2D\xAD\u2010-\u2015\u2043\u2212]*){9}[\dx](?![\x2D\xAD\u2010-\u2015\u2043\u2212])\b/gi,
    721 						validISBNs = [],
    722 						isbn;
    723 					while (isbn = isbnRe.exec(item.ISBN)) {
    724 						var validISBN = Zotero.Utilities.cleanISBN(isbn[0]);
    725 						if (!validISBN) {
    726 							// Back up and move up one character
    727 							isbnRe.lastIndex = isbn.index + 1;
    728 							continue;
    729 						}
    730 						
    731 						var isbn13 = Zotero.Utilities.toISBN13(validISBN);
    732 						if (validISBNs.indexOf(isbn13) == -1) validISBNs.push(isbn13);
    733 					}
    734 					item.ISBN = validISBNs.join(' ');
    735 				}
    736 				
    737 				// refuse to save very long tags
    738 				if(item.tags) {
    739 					for(var i=0; i<item.tags.length; i++) {
    740 						var tag = item.tags[i],
    741 							tagString = typeof tag === "string" ? tag :
    742 								typeof tag === "object" ? (tag.tag || tag.name) : null;
    743 						if(tagString && tagString.length > 255) {
    744 							translate._debug("WARNING: Skipping unsynchable tag "+JSON.stringify(tagString));
    745 							item.tags.splice(i--, 1);
    746 						}
    747 					}
    748 				}
    749 				
    750 				for(var i=0; i<item.attachments.length; i++) {
    751 					var attachment = item.attachments[i];
    752 					
    753 					// Web translators are not allowed to use attachment.path
    754 					if (attachment.path) {
    755 						if (!attachment.url) attachment.url = attachment.path;
    756 						delete attachment.path;
    757 					}
    758 					
    759 					if(attachment.url) {
    760 						// Remap attachment (but not link) URLs
    761 						// TODO: provide both proxied and un-proxied URLs (also for documents)
    762 						//   because whether the attachment is attached as link or file
    763 						//   depends on Zotero preferences as well.
    764 						attachment.url = translate.resolveURL(attachment.url, attachment.snapshot === false);
    765 					}
    766 				}
    767 			}
    768 			
    769 			// call super
    770 			Zotero.Translate.Sandbox.Base._itemDone(translate, item);
    771 		},
    772 		
    773 		/**
    774 		 * Tells Zotero to monitor changes to the DOM and re-trigger detectWeb
    775 		 * Can only be set during the detectWeb call
    776 		 * @param {DOMNode} target Document node to monitor for changes
    777 		 * @param {MutationObserverInit} [config] specifies which DOM mutations should be reported
    778 		 */
    779 		"monitorDOMChanges":function(translate, target, config) {
    780 			if(translate._currentState != "detect") {
    781 				Zotero.debug("Translate: monitorDOMChanges can only be called during the 'detect' stage");
    782 				return;
    783 			}
    784 
    785 			var window = translate.document.defaultView
    786 			var mutationObserver = window && ( window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver );
    787 			if(!mutationObserver) {
    788 				Zotero.debug("Translate: This browser does not support mutation observers.");
    789 				return;
    790 			}
    791 
    792 			var translator = translate._potentialTranslators[0];
    793 			if(!translate._registeredDOMObservers[translator.translatorID])
    794 				translate._registeredDOMObservers[translator.translatorID] = [];
    795 			var obs = translate._registeredDOMObservers[translator.translatorID];
    796 
    797 			//do not re-register observer by the same translator for the same node
    798 			if(obs.indexOf(target) != -1) {
    799 				Zotero.debug("Translate: Already monitoring this node");
    800 				return;
    801 			}
    802 
    803 			obs.push(target);
    804 
    805 			var observer = new mutationObserver(function(mutations, observer) {
    806 				obs.splice(obs.indexOf(target),1);
    807 				observer.disconnect();
    808 				
    809 				Zotero.debug("Translate: Page modified.");
    810 				//we don't really care what got updated
    811 				var doc = mutations[0].target.ownerDocument;
    812 				translate._runHandler("pageModified", doc);
    813 			});
    814 
    815 			observer.observe(target, config || {childList: true, subtree: true});
    816 			Zotero.debug("Translate: Mutation observer registered on <" + target.nodeName + "> node");
    817 		}
    818 	},
    819 
    820 	/**
    821 	 * Import functions exposed to sandbox
    822 	 * @namespace
    823 	 */
    824 	"Import":{
    825 		/**
    826 		 * Saves a collection to the DB
    827 		 * Called as {@link Zotero.Collection#complete} from the sandbox
    828 		 * @param {Zotero.Translate} translate
    829 		 * @param {SandboxCollection} collection
    830 		 */
    831 		"_collectionDone":function(translate, collection) {
    832 			translate.newCollections.push(collection);
    833 			if(translate._libraryID == false) {
    834 				translate._runHandler("collectionDone", collection);
    835 			}
    836 		},
    837 		
    838 		/**
    839 		 * Sets the value of the progress indicator associated with export as a percentage
    840 		 * @param {Zotero.Translate} translate
    841 		 * @param {Number} value
    842 		 */
    843 		"setProgress":function(translate, value) {
    844 			if(typeof value !== "number") {
    845 				translate._progress = null;
    846 			} else {
    847 				translate._progress = value;
    848 			}
    849 		}
    850 	},
    851 
    852 	/**
    853 	 * Export functions exposed to sandbox
    854 	 * @namespace
    855 	 */
    856 	"Export":{
    857 		/**
    858 		 * Retrieves the next item to be exported
    859 		 * @param {Zotero.Translate} translate
    860 		 * @return {SandboxItem}
    861 		 */
    862 		"nextItem":function(translate) {
    863 			var item = translate._itemGetter.nextItem();
    864 			
    865 			if(translate._displayOptions.hasOwnProperty("exportTags") && !translate._displayOptions["exportTags"]) {
    866 				item.tags = [];
    867 			}
    868 			
    869 			translate._runHandler("itemDone", item);
    870 			
    871 			return item;
    872 		},
    873 		
    874 		/**
    875 		 * Retrieves the next collection to be exported
    876 		 * @param {Zotero.Translate} translate
    877 		 * @return {SandboxCollection}
    878 		 */
    879 		"nextCollection":function(translate) {
    880 			if(!translate._translatorInfo.configOptions || !translate._translatorInfo.configOptions.getCollections) {
    881 				throw(new Error("getCollections configure option not set; cannot retrieve collection"));
    882 			}
    883 			
    884 			return translate._itemGetter.nextCollection();
    885 		},
    886 		
    887 		/**
    888 		 * @borrows Zotero.Translate.Sandbox.Import.setProgress as this.setProgress
    889 		 */
    890 		"setProgress":function(translate, value) {
    891 			Zotero.Translate.Sandbox.Import.setProgress(translate, value);
    892 		}
    893 	},
    894 	
    895 	/**
    896 	 * Search functions exposed to sandbox
    897 	 * @namespace
    898 	 */
    899 	"Search":{
    900 		/**
    901 		 * @borrows Zotero.Translate.Sandbox.Web._itemDone as this._itemDone
    902 		 */
    903 		"_itemDone":function(translate, item) {
    904 			// Always set library catalog, even if we have a parent translator
    905 			if(item.libraryCatalog === undefined) {
    906 				item.libraryCatalog = translate.translator[0].label;
    907 			}
    908 			
    909 			Zotero.Translate.Sandbox.Web._itemDone(translate, item);
    910 		}
    911 	}
    912 }
    913 
    914 /**
    915  * @class Base class for all translation types
    916  *
    917  * @property {String} type The type of translator. This is deprecated; use instanceof instead.
    918  * @property {Zotero.Translator[]} translator The translator currently in use. Usually, only the
    919  *     first entry of the Zotero.Translator array is populated; subsequent entries represent
    920  *     translators to be used if the first fails.
    921  * @property {String} path The path or URI string of the target
    922  * @property {String} newItems Items created when translate() was called
    923  * @property {String} newCollections Collections created when translate() was called
    924  * @property {Number} runningAsyncProcesses The number of async processes that are running. These
    925  *                                          need to terminate before Zotero.done() is called.
    926  */
    927 Zotero.Translate.Base = function() {}
    928 Zotero.Translate.Base.prototype = {
    929 	/**
    930 	 * Initializes a Zotero.Translate instance
    931 	 */
    932 	"init":function() {
    933 		this._handlers = [];
    934 		this._currentState = null;
    935 		this._translatorInfo = null;
    936 		this.document = null;
    937 		this.location = null;
    938 	},
    939 	
    940 	/**
    941 	 * Sets the location to operate upon
    942 	 *
    943 	 * @param {String|nsIFile} location The URL to which the sandbox should be bound or path to local file
    944 	 */
    945 	"setLocation":function(location) {
    946 		this.location = location;
    947 		if(typeof this.location == "object") {	// if a file
    948 			this.path = location.path;
    949 		} else {								// if a url
    950 			this.path = location;
    951 		}
    952 	},
    953 	
    954 	/**
    955 	 * Sets the translator to be used for import/export
    956 	 *
    957 	 * @param {Zotero.Translator|string} Translator object or ID
    958 	 */
    959 	"setTranslator":function(translator) {
    960 		if(!translator) {
    961 			throw new Error("No translator specified");
    962 		}
    963 		
    964 		this.translator = null;
    965 		
    966 		if(typeof(translator) == "object") {	// passed an object and not an ID
    967 			if(translator.translatorID) {
    968 				this.translator = [translator];
    969 			} else {
    970 				throw(new Error("No translatorID specified"));
    971 			}
    972 		} else {
    973 			this.translator = [translator];
    974 		}
    975 		
    976 		return !!this.translator;
    977 	},
    978 	
    979 	/**
    980 	 * Registers a handler function to be called when translation is complete
    981 	 *
    982 	 * @param {String} type Type of handler to register. Legal values are:
    983 	 * select
    984 	 *   valid: web
    985 	 *   called: when the user needs to select from a list of available items
    986 	 *   passed: an associative array in the form id => text
    987 	 *   returns: a numerically indexed array of ids, as extracted from the passed
    988 	 *            string
    989 	 * itemDone
    990 	 *   valid: import, web, search
    991 	 *   called: when an item has been processed; may be called asynchronously
    992 	 *   passed: an item object (see Zotero.Item)
    993 	 *   returns: N/A
    994 	 * collectionDone
    995 	 *   valid: import
    996 	 *   called: when a collection has been processed, after all items have been
    997 	 *           added; may be called asynchronously
    998 	 *   passed: a collection object (see Zotero.Collection)
    999 	 *   returns: N/A
   1000 	 * done
   1001 	 *   valid: all
   1002 	 *   called: when all processing is finished
   1003 	 *   passed: true if successful, false if an error occurred
   1004 	 *   returns: N/A
   1005 	 * debug
   1006 	 *   valid: all
   1007 	 *   called: when Zotero.debug() is called
   1008 	 *   passed: string debug message
   1009 	 *   returns: true if message should be logged to the console, false if not
   1010 	 * error
   1011 	 *   valid: all
   1012 	 *   called: when a fatal error occurs
   1013 	 *   passed: error object (or string)
   1014 	 *   returns: N/A
   1015 	 * translators
   1016 	 *   valid: all
   1017 	 *   called: when a translator search initiated with Zotero.Translate.getTranslators() is
   1018 	 *           complete
   1019 	 *   passed: an array of appropriate translators
   1020 	 *   returns: N/A
   1021 	 * pageModified
   1022 	 *   valid: web
   1023 	 *   called: when a web page has been modified
   1024 	 *   passed: the document object for the modified page
   1025 	 *   returns: N/A
   1026 	 * @param {Function} handler Callback function. All handlers will be passed the current
   1027 	 * translate instance as the first argument. The second argument is dependent on the handler.
   1028 	 */
   1029 	"setHandler":function(type, handler) {
   1030 		if(!this._handlers[type]) {
   1031 			this._handlers[type] = new Array();
   1032 		}
   1033 		this._handlers[type].push(handler);
   1034 	},
   1035 
   1036 	/**
   1037 	 * Clears all handlers for a given function
   1038 	 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
   1039 	 */
   1040 	"clearHandlers":function(type) {
   1041 		this._handlers[type] = new Array();
   1042 	},
   1043 
   1044 	/**
   1045 	 * Clears a single handler for a given function
   1046 	 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
   1047 	 * @param {Function} handler Callback function to remove
   1048 	 */
   1049 	"removeHandler":function(type, handler) {
   1050 		var handlerIndex = this._handlers[type].indexOf(handler);
   1051 		if(handlerIndex !== -1) this._handlers[type].splice(handlerIndex, 1);
   1052 	},
   1053 	
   1054 	/**
   1055 	 * Indicates that a new async process is running
   1056 	 */
   1057 	"incrementAsyncProcesses":function(f) {
   1058 		this._runningAsyncProcesses++;
   1059 		if(this._parentTranslator) {
   1060 			this._parentTranslator.incrementAsyncProcesses(f+" from child translator");
   1061 		} else {
   1062 			//Zotero.debug("Translate: Incremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4);
   1063 			//Zotero.debug((new Error()).stack);
   1064 		}
   1065 	},
   1066 	
   1067 	/**
   1068 	 * Indicates that a new async process is finished
   1069 	 */
   1070 	"decrementAsyncProcesses":function(f, by) {
   1071 		this._runningAsyncProcesses -= (by ? by : 1);
   1072 		if(!this._parentTranslator) {
   1073 			//Zotero.debug("Translate: Decremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4);
   1074 			//Zotero.debug((new Error()).stack);
   1075 		}
   1076 		if(this._runningAsyncProcesses === 0) {
   1077 			this.complete();
   1078 		}
   1079 		if(this._parentTranslator) this._parentTranslator.decrementAsyncProcesses(f+" from child translator", by);
   1080 	},
   1081 
   1082 	/**
   1083 	 * Clears all handlers for a given function
   1084 	 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
   1085 	 * @param {Any} argument Argument to be passed to handler
   1086 	 */
   1087 	"_runHandler":function(type) {
   1088 		var returnValue = undefined;
   1089 		if(this._handlers[type]) {
   1090 			// compile list of arguments
   1091 			if(this._parentTranslator) {
   1092 				// if there is a parent translator, make sure we don't pass the Zotero.Translate
   1093 				// object, since it could open a security hole
   1094 				var args = [null];
   1095 			} else {
   1096 				var args = [this];
   1097 			}
   1098 			for(var i=1; i<arguments.length; i++) {
   1099 				args.push(arguments[i]);
   1100 			}
   1101 			
   1102 			var handlers = this._handlers[type].slice();
   1103 			for(var i=0, n=handlers.length; i<n; i++) {
   1104 				if (type != 'debug') {
   1105 					Zotero.debug(`Translate: Running handler ${i} for ${type}`, 5);
   1106 				}
   1107 				try {
   1108 					returnValue = handlers[i].apply(null, args);
   1109 				} catch(e) {
   1110 					if(this._parentTranslator) {
   1111 						// throw handler errors if they occur when a translator is
   1112 						// called from another translator, so that the
   1113 						// "Could Not Translate" dialog will appear if necessary
   1114 						throw(e);
   1115 					} else {
   1116 						// otherwise, fail silently, so as not to interfere with
   1117 						// interface cleanup
   1118 						Zotero.debug("Translate: "+e+' in handler '+i+' for '+type, 5);
   1119 						Zotero.logError(e);
   1120 					}
   1121 				}
   1122 			}
   1123 		}
   1124 		return returnValue;
   1125 	},
   1126 
   1127 	/**
   1128 	 * Gets all applicable translators of a given type
   1129 	 *
   1130 	 * For import, you should call this after setLocation; otherwise, you'll just get a list of all
   1131 	 * import filters, not filters equipped to handle a specific file
   1132 	 *
   1133 	 * @param {Boolean} [getAllTranslators] Whether all applicable translators should be returned,
   1134 	 *     rather than just the first available.
   1135 	 * @param {Boolean} [checkSetTranslator] If true, the appropriate detect function is run on the
   1136 	 *     set document/text/etc. using the translator set by setTranslator.
   1137 	 *     getAllTranslators parameter is meaningless in this context.
   1138 	 * @return {Promise} Promise for an array of {@link Zotero.Translator} objects
   1139 	 */
   1140 	getTranslators: Zotero.Promise.method(function (getAllTranslators, checkSetTranslator) {
   1141 		var potentialTranslators;
   1142 
   1143 		// do not allow simultaneous instances of getTranslators
   1144 		if(this._currentState === "detect") throw new Error("getTranslators: detection is already running");
   1145 		this._currentState = "detect";
   1146 		this._getAllTranslators = getAllTranslators;
   1147 		this._potentialTranslators = [];
   1148 		this._foundTranslators = [];
   1149 
   1150 		if(checkSetTranslator) {
   1151 			// setTranslator must be called beforehand if checkSetTranslator is set
   1152 			if( !this.translator || !this.translator[0] ) {
   1153 				return Zotero.Promise.reject(new Error("getTranslators: translator must be set via setTranslator before calling" +
   1154 										  " getTranslators with the checkSetTranslator flag"));
   1155 			}
   1156 			var promises = new Array();
   1157 			var t;
   1158 			for(var i=0, n=this.translator.length; i<n; i++) {
   1159 				if(typeof(this.translator[i]) == 'string') {
   1160 					t = Zotero.Translators.get(this.translator[i]);
   1161 					if(!t) Zotero.debug("getTranslators: could not retrieve translator '" + this.translator[i] + "'");
   1162 				} else {
   1163 					t = this.translator[i];
   1164 				}
   1165 				/**TODO: check that the translator is of appropriate type?*/
   1166 				if(t) promises.push(t);
   1167 			}
   1168 			if(!promises.length) return Zotero.Promise.reject(new Error("getTranslators: no valid translators were set"));
   1169 			potentialTranslators = Zotero.Promise.all(promises);
   1170 		} else {
   1171 			potentialTranslators = this._getTranslatorsGetPotentialTranslators();
   1172 		}
   1173 
   1174 		// if detection returns immediately, return found translators
   1175 		return potentialTranslators.then(function(result) {
   1176 			var allPotentialTranslators = result[0];
   1177 			var proxies = result[1];
   1178 			
   1179 			// this gets passed out by Zotero.Translators.getWebTranslatorsForLocation() because it is
   1180 			// specific for each translator, but we want to avoid making a copy of a translator whenever
   1181 			// possible.
   1182 			this._proxies = proxies ? [] : null;
   1183 			this._waitingForRPC = false;
   1184 			
   1185 			for(var i=0, n=allPotentialTranslators.length; i<n; i++) {
   1186 				var translator = allPotentialTranslators[i];
   1187 				if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) {
   1188 					this._potentialTranslators.push(translator);
   1189 					if (proxies) {
   1190 						this._proxies.push(proxies[i]);
   1191 					}
   1192 				} else if (this instanceof Zotero.Translate.Web && Zotero.Connector) {
   1193 					this._waitingForRPC = true;
   1194 				}
   1195 			}
   1196 			
   1197 			// Attach handler for translators, so that we can return a
   1198 			// promise that provides them.
   1199 			// TODO make this._detect() return a promise
   1200 			var deferred = Zotero.Promise.defer();
   1201 			var translatorsHandler = function(obj, translators) {
   1202 				this.removeHandler("translators", translatorsHandler);
   1203 				deferred.resolve(translators);
   1204 			}.bind(this);
   1205 			this.setHandler("translators", translatorsHandler);
   1206 			this._detect();
   1207 
   1208 			if(this._waitingForRPC) {
   1209 				// Try detect in Zotero Standalone. If this fails, it fails; we shouldn't
   1210 				// get hung up about it.
   1211 				Zotero.Connector.callMethod(
   1212 					"detect",
   1213 					{
   1214 						uri: this.location.toString(),
   1215 						cookie: this.document.cookie,
   1216 						html: this.document.documentElement.innerHTML
   1217 					}).catch(() => false).then(function (rpcTranslators) {
   1218 						this._waitingForRPC = false;
   1219 						
   1220 						// if there are translators, add them to the list of found translators
   1221 						if (rpcTranslators) {
   1222 							for(var i=0, n=rpcTranslators.length; i<n; i++) {
   1223 								rpcTranslators[i] = new Zotero.Translator(rpcTranslators[i]);
   1224 								rpcTranslators[i].runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE;
   1225 								rpcTranslators[i].proxy = rpcTranslators[i].proxy ? new Zotero.Proxy(rpcTranslators[i].proxy) : null;
   1226 							}
   1227 							this._foundTranslators = this._foundTranslators.concat(rpcTranslators);
   1228 						}
   1229 						
   1230 						// call _detectTranslatorsCollected to return detected translators
   1231 						if (this._currentState === null) {
   1232 							this._detectTranslatorsCollected();
   1233 						}
   1234 					}.bind(this));
   1235 			}
   1236 
   1237 			return deferred.promise;
   1238 		}.bind(this))
   1239 		.catch(function(e) {
   1240 			Zotero.logError(e);
   1241 			this.complete(false, e);
   1242 		}.bind(this));
   1243 	}),
   1244 
   1245 	/**
   1246 	 * Get all potential translators (without running detect)
   1247 	 * @return {Promise} Promise for an array of {@link Zotero.Translator} objects
   1248 	 */
   1249 	 "_getTranslatorsGetPotentialTranslators":function() {
   1250 		return Zotero.Translators.getAllForType(this.type).
   1251 		then(function(translators) { return [translators] });
   1252 	 },
   1253 
   1254 	/**
   1255 	 * Begins the actual translation. At present, this returns immediately for import/export
   1256 	 * translators, but new code should use {@link Zotero.Translate.Base#setHandler} to register a 
   1257 	 * "done" handler to determine when execution of web/search translators is complete.
   1258 	 *
   1259 	 * @param 	{Integer|FALSE}	[libraryID]		Library in which to save items,
   1260 	 *																or NULL for default library;
   1261 	 *																if FALSE, don't save items
   1262 	 * @param 	{Boolean}				[saveAttachments=true]	Exclude attachments (e.g., snapshots) on import
   1263 	 * @returns {Promise}                                       Promise resolved with saved items
   1264 	 *                                                          when translation complete
   1265 	 */
   1266 	translate: Zotero.Promise.method(function (options = {}, ...args) {		// initialize properties specific to each translation
   1267 		if (typeof options == 'number') {
   1268 			Zotero.debug("Translate: translate() now takes an object -- update your code", 2);
   1269 			options = {
   1270 				libraryID: options,
   1271 				saveAttachments: args[0],
   1272 				selectedItems: args[1]
   1273 			};
   1274 		}
   1275 		
   1276 		var me = this;
   1277 		var deferred = Zotero.Promise.defer()
   1278 		
   1279 		if(!this.translator || !this.translator.length) {
   1280 			Zotero.debug("Translate: translate called without specifying a translator. Running detection first.");
   1281 			this.setHandler('translators', function(me, translators) {
   1282 				if(!translators.length) {
   1283 					me.complete(false, "Could not find an appropriate translator");
   1284 				} else {
   1285 					me.setTranslator(translators);
   1286 					deferred.resolve(Zotero.Translate.Base.prototype.translate.call(me, options));
   1287 				}
   1288 			});
   1289 			this.getTranslators();
   1290 			return deferred.promise;
   1291 		}
   1292 		
   1293 		this._currentState = "translate";
   1294 		
   1295 		this._sessionID = options.sessionID;
   1296 		this._libraryID = options.libraryID;
   1297 		if (options.collections && !Array.isArray(options.collections)) {
   1298 			throw new Error("'collections' must be an array");
   1299 		}
   1300 		this._collections = options.collections;
   1301 		this._saveAttachments = options.saveAttachments === undefined || options.saveAttachments;
   1302 		this._forceTagType = options.forceTagType;
   1303 		this._saveOptions = options.saveOptions;
   1304 		
   1305 		this._savingAttachments = [];
   1306 		this._savingItems = 0;
   1307 		this._waitingForSave = false;
   1308 
   1309 		// Attach handlers for promise
   1310 		var me = this;
   1311 		var doneHandler = function (obj, returnValue) {
   1312 			if (returnValue) deferred.resolve(me.newItems);
   1313 			me.removeHandler("done", doneHandler);
   1314 			me.removeHandler("error", errorHandler);
   1315 		};
   1316 		var errorHandler = function (obj, error) {
   1317 			deferred.reject(error);
   1318 			me.removeHandler("done", doneHandler);
   1319 			me.removeHandler("error", errorHandler);
   1320 		};
   1321 		this.setHandler("done", doneHandler);
   1322 		this.setHandler("error", errorHandler);
   1323 		
   1324 		// need to get translator first
   1325 		if (typeof this.translator[0] !== "object") {
   1326 			this.translator[0] = Zotero.Translators.get(this.translator[0]);
   1327 		}
   1328 		
   1329 		// Zotero.Translators.get() returns a promise in the connectors, but we don't expect it to
   1330 		// otherwise
   1331 		if (!Zotero.isConnector && this.translator[0].then) {
   1332 			throw new Error("Translator should not be a promise in non-connector mode");
   1333 		}
   1334 		
   1335 		if (this.noWait) {
   1336 			var loadPromise = this._loadTranslator(this.translator[0]);
   1337 			if (!loadPromise.isResolved()) {
   1338 				return Zotero.Promise.reject(new Error("Load promise is not resolved in noWait mode"));
   1339 			}
   1340 			this._translateTranslatorLoaded();
   1341 		}
   1342 		else if (this.translator[0].then) {
   1343 			Zotero.Promise.resolve(this.translator[0])
   1344 			.then(function (translator) {
   1345 				this.translator[0] = translator;
   1346 				this._loadTranslator(translator)
   1347 					.then(() => this._translateTranslatorLoaded())
   1348 					.catch(e => deferred.reject(e));
   1349 			}.bind(this));
   1350 		}
   1351 		else {
   1352 			this._loadTranslator(this.translator[0])
   1353 				.then(() => this._translateTranslatorLoaded())
   1354 				.catch(e => deferred.reject(e));
   1355 		}
   1356 		
   1357 		return deferred.promise;
   1358 	}),
   1359 	
   1360 	/**
   1361 	 * Called when translator has been retrieved and loaded
   1362 	 */
   1363 	"_translateTranslatorLoaded": Zotero.Promise.method(function() {
   1364 		// set display options to default if they don't exist
   1365 		if(!this._displayOptions) this._displayOptions = this._translatorInfo.displayOptions || {};
   1366 		
   1367 		var loadPromise = this._prepareTranslation();
   1368 		if (this.noWait) {
   1369 			if (!loadPromise.isResolved()) {
   1370 				throw new Error("Load promise is not resolved in noWait mode");
   1371 			}
   1372 			rest.apply(this, arguments);
   1373 		} else {
   1374 			return loadPromise.then(() => rest.apply(this, arguments))
   1375 		}
   1376 		
   1377 		function rest() {
   1378 			Zotero.debug("Translate: Beginning translation with " + this.translator[0].label);
   1379 
   1380 			this.incrementAsyncProcesses("Zotero.Translate#translate()");
   1381 
   1382 			// translate
   1383 			try {
   1384 				let maybePromise = Function.prototype.apply.call(
   1385 					this._sandboxManager.sandbox["do" + this._entryFunctionSuffix],
   1386 					null,
   1387 					this._getParameters()
   1388 				);
   1389 				// doImport can return a promise to allow for incremental saves (via promise-returning
   1390 				// item.complete() calls)
   1391 				if (maybePromise) {
   1392 					maybePromise
   1393 						.then(() => this.decrementAsyncProcesses("Zotero.Translate#translate()"))
   1394 					return;
   1395 				}
   1396 			} catch (e) {
   1397 				this.complete(false, e);
   1398 				return false;
   1399 			}
   1400 
   1401 			this.decrementAsyncProcesses("Zotero.Translate#translate()");
   1402 		}
   1403 	}),
   1404 	
   1405 	/**
   1406 	 * Return the progress of the import operation, or null if progress cannot be determined
   1407 	 */
   1408 	"getProgress":function() { return null },
   1409 
   1410 	/**
   1411 	 * Translate a URL to a form that goes through the appropriate proxy, or
   1412 	 * convert a relative URL to an absolute one
   1413 	 *
   1414 	 * @param {String} url
   1415 	 * @param {Boolean} dontUseProxy If true, don't convert URLs to variants
   1416 	 *     that use the proxy
   1417 	 * @type String
   1418 	 * @private
   1419 	 */
   1420 	"resolveURL":function(url, dontUseProxy) {
   1421 		Zotero.debug("Translate: resolving URL " + url);
   1422 		
   1423 		const hostPortRe = /^([A-Z][-A-Z0-9+.]*):\/\/[^\/]+/i;
   1424 		const allowedSchemes = ['http', 'https', 'ftp'];
   1425 		
   1426 		var m = url.match(hostPortRe),
   1427 			resolved;
   1428 		if (!m) {
   1429 			// Convert relative URLs to absolute
   1430 			if(Zotero.isFx && this.location) {
   1431 				resolved = Components.classes["@mozilla.org/network/io-service;1"].
   1432 					getService(Components.interfaces.nsIIOService).
   1433 					newURI(this.location, "", null).resolve(url);
   1434 			} else if(Zotero.isNode && this.location) {
   1435 				resolved = require('url').resolve(this.location, url);
   1436 			} else if (this.document) {
   1437 				var a = this.document.createElement('a');
   1438 				a.href = url;
   1439 				resolved = a.href;
   1440 			} else if (url.indexOf('//') == 0) {
   1441 				// Protocol-relative URL with no associated web page
   1442 				// Use HTTP by default
   1443 				resolved = 'http:' + url;
   1444 			} else {
   1445 				throw new Error('Cannot resolve relative URL without an associated web page: ' + url);
   1446 			}
   1447 		} else if (allowedSchemes.indexOf(m[1].toLowerCase()) == -1) {
   1448 			Zotero.debug("Translate: unsupported scheme " + m[1]);
   1449 			return url;
   1450 		} else {
   1451 			resolved = url;
   1452 		}
   1453 		
   1454 		Zotero.debug("Translate: resolved to " + resolved);
   1455 		
   1456 		// convert proxy to proper if applicable
   1457 		if(!dontUseProxy && this.translator && this.translator[0]
   1458 				&& this._proxy) {
   1459 			var proxiedURL = this._proxy.toProxy(resolved);
   1460 			if (proxiedURL != resolved) {
   1461 				Zotero.debug("Translate: proxified to " + proxiedURL);
   1462 			}
   1463 			resolved = proxiedURL;
   1464 		}
   1465 		
   1466 		/*var m = hostPortRe.exec(resolved);
   1467 		if(!m) {
   1468 			throw new Error("Invalid URL supplied for HTTP request: "+url);
   1469 		} else if(this._translate.document && this._translate.document.location) {
   1470 			var loc = this._translate.document.location;
   1471 			if(this._translate._currentState !== "translate" && loc
   1472 					&& (m[1].toLowerCase() !== loc.protocol.toLowerCase()
   1473 					|| m[2].toLowerCase() !== loc.host.toLowerCase())) {
   1474 				throw new Error("Attempt to access "+m[1]+"//"+m[2]+" from "+loc.protocol+"//"+loc.host
   1475 					+" blocked: Cross-site requests are only allowed during translation");
   1476 			}
   1477 		}*/
   1478 		
   1479 		return resolved;
   1480 	},
   1481 	
   1482 	/**
   1483 	 * Executed on translator completion, either automatically from a synchronous scraper or as
   1484 	 * done() from an asynchronous scraper. Finishes things up and calls callback function(s).
   1485 	 * @param {Boolean|String} returnValue An item type or a boolean true or false
   1486 	 * @param {String|Exception} [error] An error that occurred during translation.
   1487 	 * @returm {String|NULL} The exception serialized to a string, or null if translation
   1488 	 *     completed successfully.
   1489 	 */
   1490 	"complete":function(returnValue, error) {
   1491 		// allow translation to be aborted for re-running after selecting items
   1492 		if(this._aborted) return;
   1493 		
   1494 		// Make sure this isn't called twice
   1495 		if(this._currentState === null) {
   1496 			if(!returnValue) {
   1497 				Zotero.debug("Translate: WARNING: Zotero.done() called after translator completion with error");
   1498 				Zotero.debug(error);
   1499 			} else {
   1500 				var e = new Error();
   1501 				Zotero.debug("Translate: WARNING: Zotero.done() called after translation completion. This should never happen. Please examine the stack below.");
   1502 				Zotero.debug(e.stack);
   1503 			}
   1504 			return;
   1505 		}
   1506 		
   1507 		// reset async processes and propagate them to parent
   1508 		if(this._parentTranslator && this._runningAsyncProcesses) {
   1509 			this._parentTranslator.decrementAsyncProcesses("Zotero.Translate#complete", this._runningAsyncProcesses);
   1510 		}
   1511 		this._runningAsyncProcesses = 0;
   1512 		
   1513 		if(!returnValue && this._returnValue) returnValue = this._returnValue;
   1514 		
   1515 		var errorString = null;
   1516 		if(!returnValue && error) errorString = this._generateErrorString(error);
   1517 		if(this._currentState === "detect") {
   1518 			if(this._potentialTranslators.length) {
   1519 				var lastTranslator = this._potentialTranslators.shift();
   1520 				var lastProxy = this._proxies ? this._proxies.shift() : null;
   1521 				
   1522 				if (returnValue) {
   1523 					var dupeTranslator = {proxy: lastProxy ? new Zotero.Proxy(lastProxy) : null};
   1524 					
   1525 					for (var i in lastTranslator) dupeTranslator[i] = lastTranslator[i];
   1526 					if (Zotero.isBookmarklet && returnValue === "server") {
   1527 						// In the bookmarklet, the return value from detectWeb can be "server" to
   1528 						// indicate the translator should be run on the Zotero server
   1529 						dupeTranslator.runMode = Zotero.Translator.RUN_MODE_ZOTERO_SERVER;
   1530 					} else {
   1531 						// Usually the return value from detectWeb will be either an item type or
   1532 						// the string "multiple"
   1533 						dupeTranslator.itemType = returnValue;
   1534 					}
   1535 					
   1536 					this._foundTranslators.push(dupeTranslator);
   1537 				} else if(error) {
   1538 					this._debug("Detect using "+lastTranslator.label+" failed: \n"+errorString, 2);
   1539 				}
   1540 			}
   1541 				
   1542 			if(this._potentialTranslators.length && (this._getAllTranslators || !returnValue)) {
   1543 				// more translators to try; proceed to next translator
   1544 				this._detect();
   1545 			} else {
   1546 				this._currentState = null;
   1547 				if(!this._waitingForRPC) this._detectTranslatorsCollected();
   1548 			}
   1549 		} else {
   1550 			// unset return value is equivalent to true
   1551 			if(returnValue === undefined) returnValue = true;
   1552 			
   1553 			if(returnValue) {
   1554 				if(this.saveQueue.length) {
   1555 					this._waitingForSave = true;
   1556 					this._saveItems(this.saveQueue)
   1557 						.catch(e => this._runHandler("error", e))
   1558 						.then(() => this.saveQueue = []);
   1559 					return;
   1560 				}
   1561 				this._debug("Translation successful");
   1562 			} else {
   1563 				if(error) {
   1564 					// report error to console
   1565 					Zotero.logError(error);
   1566 					
   1567 					// report error to debug log
   1568 					this._debug("Translation using "+(this.translator && this.translator[0] && this.translator[0].label ? this.translator[0].label : "no translator")+" failed: \n"+errorString, 2);
   1569 				}
   1570 				
   1571 				this._runHandler("error", error);
   1572 			}
   1573 			
   1574 			this._currentState = null;
   1575 			
   1576 			// call handlers
   1577 			this._runHandler("itemsDone", returnValue);
   1578 			if(returnValue) {
   1579 				this._checkIfDone();
   1580 			} else {
   1581 				this._runHandler("done", returnValue);
   1582 			}
   1583 		}
   1584 		
   1585 		return errorString;
   1586 	},
   1587 
   1588 	/**
   1589 	 * Canonicalize an array of tags such that they are all objects with the tag stored in the
   1590 	 * "tag" property and a type (if specified) is stored in the "type" property
   1591 	 * @returns {Object[]} Array of new tag objects
   1592 	 */
   1593 	"_cleanTags":function(tags) {
   1594 		var newTags = [];
   1595 		if(!tags) return newTags;
   1596 		for(var i=0; i<tags.length; i++) {
   1597 			var tag = tags[i];
   1598 			if(!tag) continue;
   1599 			if(typeof(tag) == "object") {
   1600 				var tagString = tag.tag || tag.name;
   1601 				if(tagString) {
   1602 					var newTag = {"tag":tagString};
   1603 					if(tag.type) newTag.type = tag.type;
   1604 					newTags.push(newTag);
   1605 				}
   1606 			} else {
   1607 				newTags.push({"tag":tag.toString()});
   1608 			}
   1609 		}
   1610 		return newTags;
   1611 	},
   1612 	
   1613 	/**
   1614 	 * Saves items to the database, taking care to defer attachmentProgress notifications
   1615 	 * until after save
   1616 	 */
   1617 	_saveItems: Zotero.Promise.method(function (items) {
   1618 		var itemDoneEventsDispatched = false;
   1619 		var deferredProgress = [];
   1620 		var attachmentsWithProgress = [];
   1621 		
   1622 		function attachmentCallback(attachment, progress, error) {
   1623 			var attachmentIndex = this._savingAttachments.indexOf(attachment);
   1624 			if(progress === false || progress === 100) {
   1625 				if(attachmentIndex !== -1) {
   1626 					this._savingAttachments.splice(attachmentIndex, 1);
   1627 				}
   1628 			} else if(attachmentIndex === -1) {
   1629 				this._savingAttachments.push(attachment);
   1630 			}
   1631 			
   1632 			if(itemDoneEventsDispatched) {
   1633 				// itemDone event has already fired, so we can fire attachmentProgress
   1634 				// notifications
   1635 				this._runHandler("attachmentProgress", attachment, progress, error);
   1636 				this._checkIfDone();
   1637 			} else {
   1638 				// Defer until after we fire the itemDone event
   1639 				deferredProgress.push([attachment, progress, error]);
   1640 				attachmentsWithProgress.push(attachment);
   1641 			}
   1642 		}
   1643 		
   1644 		return this._itemSaver.saveItems(items.slice(), attachmentCallback.bind(this))
   1645 		.then(function(newItems) {
   1646 			// Remove attachments not being saved from item.attachments
   1647 			for(var i=0; i<items.length; i++) {
   1648 				var item = items[i];
   1649 				for(var j=0; j<item.attachments.length; j++) {
   1650 					if(attachmentsWithProgress.indexOf(item.attachments[j]) === -1) {
   1651 						item.attachments.splice(j--, 1);
   1652 					}
   1653 				}
   1654 			}
   1655 			
   1656 			// Trigger itemDone events, waiting for them if they return promises
   1657 			var maybePromises = [];
   1658 			for(var i=0, nItems = items.length; i<nItems; i++) {
   1659 				maybePromises.push(this._runHandler("itemDone", newItems[i], items[i]));
   1660 			}
   1661 			return Zotero.Promise.all(maybePromises).then(() => newItems);
   1662 		}.bind(this))
   1663 		.then(function (newItems) {
   1664 			// Specify that itemDone event was dispatched, so that we don't defer
   1665 			// attachmentProgress notifications anymore
   1666 			itemDoneEventsDispatched = true;
   1667 			
   1668 			// Run deferred attachmentProgress notifications
   1669 			for(var i=0; i<deferredProgress.length; i++) {
   1670 				this._runHandler("attachmentProgress", deferredProgress[i][0],
   1671 					deferredProgress[i][1], deferredProgress[i][2]);
   1672 			}
   1673 			
   1674 			this._savingItems -= items.length;
   1675 			this.newItems = this.newItems.concat(newItems);
   1676 			this._checkIfDone();
   1677 		}.bind(this))
   1678 		.catch((e) => {
   1679 			this._savingItems -= items.length;
   1680 			this.complete(false, e);
   1681 			throw e;
   1682 		});
   1683 	}),
   1684 	
   1685 	/**
   1686 	 * Checks if saving done, and if so, fires done event
   1687 	 */
   1688 	"_checkIfDone":function() {
   1689 		if(!this._savingItems && !this._savingAttachments.length && (!this._currentState || this._waitingForSave)) {
   1690 			if(this.newCollections && this._itemSaver.saveCollections) {
   1691 				var me = this;
   1692 				this._itemSaver.saveCollections(this.newCollections)
   1693 				.then(function (newCollections) {
   1694 					me.newCollections = newCollections;
   1695 					me._runHandler("done", true);
   1696 				})
   1697 				.catch(function (err) {
   1698 					me._runHandler("error", err);
   1699 					me._runHandler("done", false);
   1700 				});
   1701 			} else {
   1702 				this._runHandler("done", true);
   1703 			}
   1704 		}
   1705 	},
   1706 	
   1707 	/**
   1708 	 * Begins running detect code for a translator, first loading it
   1709 	 */
   1710 	"_detect":function() {
   1711 		// there won't be any translators if we need an RPC call
   1712 		if(!this._potentialTranslators.length) {
   1713 			this.complete(true);
   1714 			return;
   1715 		}
   1716 		
   1717 		let lab = this._potentialTranslators[0].label;
   1718 		this._loadTranslator(this._potentialTranslators[0])
   1719 		.then(function() {
   1720 			return this._detectTranslatorLoaded();
   1721 		}.bind(this))
   1722 		.catch(function (e) {
   1723 			this.complete(false, e);
   1724 		}.bind(this));
   1725 	},
   1726 	
   1727 	/**
   1728 	 * Runs detect code for a translator
   1729 	 */
   1730 	"_detectTranslatorLoaded":function() {
   1731 		this._prepareDetection();
   1732 		
   1733 		this.incrementAsyncProcesses("Zotero.Translate#getTranslators");
   1734 		
   1735 		try {
   1736 			var returnValue = Function.prototype.apply.call(this._sandboxManager.sandbox["detect"+this._entryFunctionSuffix], null, this._getParameters());
   1737 		} catch(e) {
   1738 			this.complete(false, e);
   1739 			return;
   1740 		}
   1741 		
   1742 		if(returnValue !== undefined) this._returnValue = returnValue;
   1743 		this.decrementAsyncProcesses("Zotero.Translate#getTranslators");
   1744 	},
   1745 	
   1746 	/**
   1747 	 * Called when all translators have been collected for detection
   1748 	 */
   1749 	"_detectTranslatorsCollected":function() {
   1750 		Zotero.debug("Translate: All translator detect calls and RPC calls complete:");
   1751 		this._foundTranslators.sort(function(a, b) { return a.priority-b.priority });
   1752 		if (this._foundTranslators.length) {
   1753 			this._foundTranslators.forEach(function(t) {
   1754 				Zotero.debug("\t" + t.label + ": " + t.priority);
   1755 			});
   1756 		} else {
   1757 			Zotero.debug("\tNo suitable translators found");
   1758 		}
   1759 		this._runHandler("translators", this._foundTranslators);
   1760 	},
   1761 	
   1762 	/**
   1763 	 * Loads the translator into its sandbox
   1764 	 * @param {Zotero.Translator} translator
   1765 	 * @return {Promise<Boolean>} Whether the translator could be successfully loaded
   1766 	 */
   1767 	"_loadTranslator": Zotero.Promise.method(function (translator) {
   1768 		var sandboxLocation = this._getSandboxLocation();
   1769 		if(!this._sandboxLocation || sandboxLocation !== this._sandboxLocation) {
   1770 			this._sandboxLocation = sandboxLocation;
   1771 			this._generateSandbox();
   1772 		}
   1773 		
   1774 		this._currentTranslator = translator;
   1775 		
   1776 		// Pass on the proxy of the parent translate
   1777 		if (this._parentTranslator) {
   1778 			this._proxy = this._parentTranslator._proxy;
   1779 		} else {
   1780 			this._proxy = translator.proxy;
   1781 		}
   1782 		this._runningAsyncProcesses = 0;
   1783 		this._returnValue = undefined;
   1784 		this._aborted = false;
   1785 		this.saveQueue = [];
   1786 		
   1787 		var parse = function(code) {
   1788 			Zotero.debug("Translate: Parsing code for " + translator.label + " "
   1789 				+ "(" + translator.translatorID + ", " + translator.lastUpdated + ")", 4);
   1790 			this._sandboxManager.eval(
   1791 				"var exports = {}, ZOTERO_TRANSLATOR_INFO = " + code,
   1792 				[
   1793 					"detect" + this._entryFunctionSuffix,
   1794 					"do" + this._entryFunctionSuffix,
   1795 					"exports",
   1796 					"ZOTERO_TRANSLATOR_INFO"
   1797 				],
   1798 				(translator.file ? translator.file.path : translator.label)
   1799 			);
   1800 			this._translatorInfo = this._sandboxManager.sandbox.ZOTERO_TRANSLATOR_INFO;
   1801 		}.bind(this);
   1802 		
   1803 		if (this.noWait) {
   1804 			try {
   1805 				let codePromise = translator.getCode();
   1806 				if (!codePromise.isResolved()) {
   1807 					throw new Error("Code promise is not resolved in noWait mode");
   1808 				}
   1809 				parse(codePromise.value());
   1810 			}
   1811 			catch (e) {
   1812 				this.complete(false, e);
   1813 			}
   1814 		}
   1815 		else {
   1816 			return translator.getCode()
   1817 			.then(parse)
   1818 			.catch(function(e) {
   1819 				this.complete(false, e);
   1820 			}.bind(this));
   1821 		}
   1822 	}),
   1823 	
   1824 	/**
   1825 	 * Generates a sandbox for scraping/scraper detection
   1826 	 */
   1827 	"_generateSandbox":function() {
   1828 		Zotero.debug("Translate: Binding sandbox to "+(typeof this._sandboxLocation == "object" ? this._sandboxLocation.document.location : this._sandboxLocation), 4);
   1829 		if (this._parentTranslator && this._parentTranslator._sandboxManager.newChild) {
   1830 			this._sandboxManager = this._parentTranslator._sandboxManager.newChild();
   1831 		} else {
   1832 			this._sandboxManager = new Zotero.Translate.SandboxManager(this._sandboxLocation);
   1833 		}
   1834 		const createArrays = "['creators', 'notes', 'tags', 'seeAlso', 'attachments']";
   1835 		var src = "";
   1836 		if (Zotero.isFx && !Zotero.isBookmarklet) {
   1837 			src = "var Zotero = {};";
   1838 		}
   1839 		src += "Zotero.Item = function (itemType) {"+
   1840 				"var createArrays = "+createArrays+";"+
   1841 				"this.itemType = itemType;"+
   1842 				"for(var i=0, n=createArrays.length; i<n; i++) {"+
   1843 					"this[createArrays[i]] = [];"+
   1844 				"}"+
   1845 		"};";
   1846 		
   1847 		if(this instanceof Zotero.Translate.Export || this instanceof Zotero.Translate.Import) {
   1848 			src += "Zotero.Collection = function () {};"+
   1849 			"Zotero.Collection.prototype.complete = function() { return Zotero._collectionDone(this); };";
   1850 		}
   1851 		
   1852 		src += "Zotero.Item.prototype.complete = function() { return Zotero._itemDone(this); }";
   1853 
   1854 		this._sandboxManager.eval(src);
   1855 		this._sandboxManager.importObject(this.Sandbox, this);
   1856 		this._sandboxManager.importObject({"Utilities":new Zotero.Utilities.Translate(this)});
   1857 
   1858 		this._sandboxZotero = this._sandboxManager.sandbox.Zotero;
   1859 
   1860 		if(Zotero.isFx) {
   1861 			if(this._sandboxZotero.wrappedJSObject) this._sandboxZotero = this._sandboxZotero.wrappedJSObject;
   1862 		}
   1863 		this._sandboxZotero.Utilities.HTTP = this._sandboxZotero.Utilities;
   1864 		
   1865 		this._sandboxZotero.isBookmarklet = Zotero.isBookmarklet || false;
   1866 		this._sandboxZotero.isConnector = Zotero.isConnector || false;
   1867 		this._sandboxZotero.isServer = Zotero.isServer || false;
   1868 		this._sandboxZotero.parentTranslator = this._parentTranslator
   1869 			&& this._parentTranslator._currentTranslator ? 
   1870 			this._parentTranslator._currentTranslator.translatorID : null;
   1871 		
   1872 		// create shortcuts
   1873 		this._sandboxManager.sandbox.Z = this._sandboxZotero;
   1874 		this._sandboxManager.sandbox.ZU = this._sandboxZotero.Utilities;
   1875 		this._transferItem = this._sandboxZotero._transferItem;
   1876 		
   1877 		// Add web helper functions
   1878 		if (this.type == 'web') {
   1879 			this._sandboxManager.sandbox.attr = this._attr.bind(this);
   1880 			this._sandboxManager.sandbox.text = this._text.bind(this);
   1881 		}
   1882 	},
   1883 	
   1884 	/**
   1885 	 * Helper function to extract HTML attribute text
   1886 	 */
   1887 	_attr: function (selector, attr, index) {
   1888 		if (typeof arguments[0] == 'string') {
   1889 			var docOrElem = this.document;
   1890 		}
   1891 		// Document or element passed as first argument
   1892 		else {
   1893 			// TODO: Warn if Document rather than Element is passed once we drop 4.0 translator
   1894 			// support
   1895 			[docOrElem, selector, attr, index] = arguments;
   1896 		}
   1897 		var elem = index
   1898 			? docOrElem.querySelectorAll(selector).item(index)
   1899 			: docOrElem.querySelector(selector);
   1900 		return elem ? elem.getAttribute(attr) : null;
   1901 	},
   1902 	
   1903 	/**
   1904 	 * Helper function to extract HTML element text
   1905 	 */
   1906 	_text: function (selector, index) {
   1907 		if (typeof arguments[0] == 'string') {
   1908 			var docOrElem = this.document;
   1909 		}
   1910 		// Document or element passed as first argument
   1911 		else {
   1912 			// TODO: Warn if Document rather than Element is passed once we drop 4.0 translator
   1913 			// support
   1914 			[docOrElem, selector, index] = arguments;
   1915 		}
   1916 		var elem = index
   1917 			? docOrElem.querySelectorAll(selector).item(index)
   1918 			: docOrElem.querySelector(selector);
   1919 		return elem ? elem.textContent : null;
   1920 	},
   1921 	
   1922 	/**
   1923 	 * Logs a debugging message
   1924 	 * @param {String} string Debug string to log
   1925 	 * @param {Integer} level Log level (1-5, higher numbers are higher priority)
   1926 	 */
   1927 	"_debug":function(string, level) {
   1928 		if(level !== undefined && typeof level !== "number") {
   1929 			Zotero.debug("debug: level must be an integer");
   1930 			return;
   1931 		}
   1932 		
   1933 		// if handler does not return anything explicitly false, show debug
   1934 		// message in console
   1935 		if(this._runHandler("debug", string) !== false) {
   1936 			if(typeof string == "string") string = "Translate: "+string;
   1937 			Zotero.debug(string, level);
   1938 		}
   1939 	},
   1940 	/**
   1941 	 * Generates a string from an exception
   1942 	 * @param {String|Exception} error
   1943 	 */
   1944 	_generateErrorString: function (error) {
   1945 		var errorString = error;
   1946 		if (error.stack && error) {
   1947 			errorString += "\n\n" + error.stack;
   1948 		}
   1949 		if (this.path) {
   1950 			errorString += `\nurl => ${this.path}`;
   1951 		}
   1952 		if (Zotero.Prefs.get("downloadAssociatedFiles")) {
   1953 			errorString += "\ndownloadAssociatedFiles => true";
   1954 		}
   1955 		if (Zotero.Prefs.get("automaticSnapshots")) {
   1956 			errorString += "\nautomaticSnapshots => true";
   1957 		}
   1958 		return errorString;
   1959 	},
   1960 	
   1961 	/**
   1962 	 * Determines the location where the sandbox should be bound
   1963 	 * @return {String|document} The location to which to bind the sandbox
   1964 	 */
   1965 	"_getSandboxLocation":function() {
   1966 		return (this._parentTranslator ? this._parentTranslator._sandboxLocation : "http://www.example.com/");
   1967 	},
   1968 	
   1969 	/**
   1970 	 * Gets parameters to be passed to detect* and do* functions
   1971 	 * @return {Array} A list of parameters
   1972 	 */
   1973 	"_getParameters":function() { return []; },
   1974 	
   1975 	/**
   1976 	 * No-op for preparing detection
   1977 	 */
   1978 	"_prepareDetection":function() {},
   1979 	
   1980 	/**
   1981 	 * No-op for preparing translation
   1982 	 */
   1983 	"_prepareTranslation": function () { return Zotero.Promise.resolve(); }
   1984 }
   1985 
   1986 /**
   1987  * @class Web translation
   1988  *
   1989  * @property {Document} document The document object to be used for web scraping (set with setDocument)
   1990  * @property {Zotero.CookieSandbox} cookieSandbox A CookieSandbox to manage cookies for
   1991  *     this Translate instance.
   1992  */
   1993 Zotero.Translate.Web = function() {
   1994 	this._registeredDOMObservers = {}
   1995 	this.init();
   1996 }
   1997 Zotero.Translate.Web.prototype = new Zotero.Translate.Base();
   1998 Zotero.Translate.Web.prototype.type = "web";
   1999 Zotero.Translate.Web.prototype._entryFunctionSuffix = "Web";
   2000 Zotero.Translate.Web.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Web);
   2001 
   2002 /**
   2003  * Sets the browser to be used for web translation
   2004  * @param {Document} doc An HTML document
   2005  */
   2006 Zotero.Translate.Web.prototype.setDocument = function(doc) {
   2007 	this.document = doc;
   2008 	try {
   2009 		this.rootDocument = doc.defaultView.top.document;
   2010 	} catch (e) {
   2011 		// Cross-origin frames won't be able to access top.document and will throw an error
   2012 	}
   2013 	if (!this.rootDocument) {
   2014 		this.rootDocument = doc;
   2015 	}
   2016 	this.setLocation(doc.location.href, this.rootDocument.location.href);
   2017 }
   2018 
   2019 /**
   2020  * Sets a Zotero.CookieSandbox to handle cookie management for XHRs initiated from this
   2021  * translate instance
   2022  *
   2023  * @param {Zotero.CookieSandbox} cookieSandbox
   2024  */
   2025 Zotero.Translate.Web.prototype.setCookieSandbox = function(cookieSandbox) {
   2026 	this.cookieSandbox = cookieSandbox;
   2027 }
   2028 
   2029 /**
   2030  * Sets the location to operate upon
   2031  *
   2032  * @param {String} location The URL of the page to translate
   2033  * @param {String} rootLocation The URL of the root page, within which `location` is embedded
   2034  */
   2035 Zotero.Translate.Web.prototype.setLocation = function(location, rootLocation) {
   2036 	this.location = location;
   2037 	this.rootLocation = rootLocation || location;
   2038 	this.path = this.location;
   2039 }
   2040 
   2041 /**
   2042  * Get potential web translators
   2043  */
   2044 Zotero.Translate.Web.prototype._getTranslatorsGetPotentialTranslators = function() {
   2045 	return Zotero.Translators.getWebTranslatorsForLocation(this.location, this.rootLocation);
   2046 }
   2047 
   2048 /**
   2049  * Bind sandbox to document being translated
   2050  */
   2051 Zotero.Translate.Web.prototype._getSandboxLocation = function() {
   2052 	if(this._parentTranslator) {
   2053 		return this._parentTranslator._sandboxLocation;
   2054 	} else if(this.document.defaultView
   2055 			&& (this.document.defaultView.toString().indexOf("Window") !== -1
   2056 				|| this.document.defaultView.toString().indexOf("XrayWrapper") !== -1)) {
   2057 		return this.document.defaultView;
   2058 	} else {
   2059 		return this.document.location.toString();
   2060 	}
   2061 }
   2062 
   2063 /**
   2064  * Pass document and location to detect* and do* functions
   2065  */
   2066 Zotero.Translate.Web.prototype._getParameters = function() {
   2067 	if (Zotero.Translate.DOMWrapper && Zotero.Translate.DOMWrapper.isWrapped(this.document)) {
   2068 		return [
   2069 			this._sandboxManager.wrap(
   2070 				Zotero.Translate.DOMWrapper.unwrap(this.document),
   2071 				null,
   2072 				this.document.SpecialPowers_wrapperOverrides
   2073 			),
   2074 			this.location
   2075 		];
   2076 	} else {
   2077 		return [this.document, this.location];
   2078 	}
   2079 };
   2080 
   2081 /**
   2082  * Prepare translation
   2083  */
   2084 Zotero.Translate.Web.prototype._prepareTranslation = Zotero.Promise.method(function () {
   2085 	this._itemSaver = new Zotero.Translate.ItemSaver({
   2086 		libraryID: this._libraryID,
   2087 		collections: this._collections,
   2088 		attachmentMode: Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")],
   2089 		forceTagType: 1,
   2090 		sessionID: this._sessionID,
   2091 		cookieSandbox: this._cookieSandbox,
   2092 		proxy: this._proxy,
   2093 		baseURI: this.location
   2094 	});
   2095 	this.newItems = [];
   2096 });
   2097 
   2098 /**
   2099  * Overload translate to set selectedItems
   2100  */
   2101 Zotero.Translate.Web.prototype.translate = function (options = {}, ...args) {
   2102 	if (typeof options == 'number' || options === false) {
   2103 		Zotero.debug("Translate: translate() now takes an object -- update your code", 2);
   2104 		options = {
   2105 			libraryID: options,
   2106 			saveAttachments: args[0],
   2107 			selectedItems: args[1]
   2108 		};
   2109 	}
   2110 	this._selectedItems = options.selectedItems;
   2111 	return Zotero.Translate.Base.prototype.translate.call(this, options);
   2112 }
   2113 
   2114 /**
   2115  * Overload _translateTranslatorLoaded to send an RPC call if necessary
   2116  */
   2117 Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() {
   2118 	var runMode = this.translator[0].runMode;
   2119 	if(runMode === Zotero.Translator.RUN_MODE_IN_BROWSER || this._parentTranslator) {
   2120 		Zotero.Translate.Base.prototype._translateTranslatorLoaded.apply(this);
   2121 	} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE ||
   2122 			(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && Zotero.Connector.isOnline)) {
   2123 		var me = this;
   2124 		Zotero.Connector.callMethod("savePage", {
   2125 				uri: this.location.toString(),
   2126 				translatorID: (typeof this.translator[0] === "object"
   2127 				                ? this.translator[0].translatorID : this.translator[0]),
   2128 				cookie: this.document.cookie,
   2129 				proxy: this._proxy ? this._proxy.toJSON() : null,
   2130 				html: this.document.documentElement.innerHTML
   2131 			}).then(obj => me._translateRPCComplete(obj));
   2132 	} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER) {
   2133 		var me = this;
   2134 		Zotero.API.createItem({"url":this.document.location.href.toString()},
   2135 			function(statusCode, response) {
   2136 				me._translateServerComplete(statusCode, response);
   2137 			});
   2138 	}
   2139 }
   2140 	
   2141 /**
   2142  * Called when an call to Zotero Standalone for translation completes
   2143  */
   2144 Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) {
   2145 	if(!obj) this.complete(false, failureCode);
   2146 	
   2147 	if(obj.selectItems) {
   2148 		// if we have to select items, call the selectItems handler and do it
   2149 		var me = this;
   2150 		this._runHandler("select", obj.selectItems,
   2151 			function(selectedItems) {
   2152 				Zotero.Connector.callMethod("selectItems",
   2153 					{"instanceID":obj.instanceID, "selectedItems":selectedItems})
   2154 					.then((obj) => me._translateRPCComplete(obj))
   2155 			}
   2156 		);
   2157 	} else {
   2158 		// if we don't have to select items, continue
   2159 		for(var i=0, n=obj.items.length; i<n; i++) {
   2160 			this._runHandler("itemDone", null, obj.items[i]);
   2161 		}
   2162 		this.newItems = obj.items;
   2163 		this.complete(true);
   2164 	}
   2165 }
   2166 	
   2167 /**
   2168  * Called when an call to the Zotero Translator Server for translation completes
   2169  */
   2170 Zotero.Translate.Web.prototype._translateServerComplete = function(statusCode, response) {
   2171 	if(statusCode === 300) {
   2172 		// Multiple Choices
   2173 		try {
   2174 			response = JSON.parse(response);
   2175 		} catch(e) {
   2176 			Zotero.logError(e);
   2177 			this.complete(false, "Invalid JSON response received from server");
   2178 			return;
   2179 		}
   2180 		var me = this;
   2181 		this._runHandler("select", response,
   2182 			function(selectedItems) {
   2183 				Zotero.API.createItem({
   2184 						"url":me.document.location.href.toString(),
   2185 						"items":selectedItems
   2186 					},
   2187 					function(statusCode, response) {
   2188 							me._translateServerComplete(statusCode, response);
   2189 					});
   2190 			}
   2191 		);
   2192 	} else if(statusCode === 201) {
   2193 		// Created
   2194 		try {
   2195 			response = (new DOMParser()).parseFromString(response, "application/xml");
   2196 		} catch(e) {
   2197 			Zotero.logError(e);
   2198 			this.complete(false, "Invalid XML response received from server");
   2199 			return;
   2200 		}
   2201 		
   2202 		// Extract items from ATOM/JSON response
   2203 		var items = [], contents;
   2204 		if("getElementsByTagNameNS" in response) {
   2205 			contents = response.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "content");
   2206 		} else { // IE...
   2207 			contents = response.getElementsByTagName("content");
   2208 		}
   2209 		for(var i=0, n=contents.length; i<n; i++) {
   2210 			var content = contents[i];
   2211 			if("getAttributeNS" in content) {
   2212 				if(content.getAttributeNS("http://zotero.org/ns/api", "type") != "json") continue;
   2213 			} else if(content.getAttribute("zapi:type") != "json") { // IE...
   2214 				continue;
   2215 			}
   2216 			
   2217 			try {
   2218 				var item = JSON.parse("textContent" in content ?
   2219 					content.textContent : content.text);
   2220 			} catch(e) {
   2221 				Zotero.logError(e);
   2222 				this.complete(false, "Invalid JSON response received from server");
   2223 				return;
   2224 			}
   2225 			
   2226 			if(!("attachments" in item)) item.attachments = [];
   2227 			this._runHandler("itemDone", null, item);
   2228 			items.push(item);
   2229 		}
   2230 		this.newItems = items;
   2231 		this.complete(true);
   2232 	} else {
   2233 		this.complete(false, response);
   2234 	}
   2235 }
   2236 
   2237 /**
   2238  * Overload complete to report translation failure
   2239  */
   2240 Zotero.Translate.Web.prototype.complete = async function(returnValue, error) {
   2241 	// call super
   2242 	var oldState = this._currentState;
   2243 	var errorString = Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
   2244 	
   2245 	var promise;
   2246 	if (Zotero.Prefs.getAsync) {
   2247 		promise = Zotero.Prefs.getAsync('reportTranslationFailure');
   2248 	} else {
   2249 		promise = Zotero.Promise.resolve(Zotero.Prefs.get("reportTranslationFailure"));
   2250 	}
   2251 	var reportTranslationFailure = await promise;
   2252 	// Report translation failure if we failed
   2253 	if(oldState == "translate" && errorString && !this._parentTranslator && this.translator.length
   2254 		&& this.translator[0].inRepository && reportTranslationFailure) {
   2255 		// Don't report failure if in private browsing mode
   2256 		if (Zotero.isConnector && await Zotero.Connector_Browser.isIncognito()) {
   2257 			return
   2258 		}
   2259 		
   2260 		var translator = this.translator[0];
   2261 		var info = await Zotero.getSystemInfo();
   2262 		
   2263 		var postBody = "id=" + encodeURIComponent(translator.translatorID) +
   2264 					   "&lastUpdated=" + encodeURIComponent(translator.lastUpdated) +
   2265 					   "&diagnostic=" + encodeURIComponent(info) +
   2266 					   "&errorData=" + encodeURIComponent(errorString);
   2267 		return Zotero.HTTP.doPost(ZOTERO_CONFIG.REPOSITORY_URL + "report", postBody);
   2268 	}
   2269 }
   2270 
   2271 /**
   2272  * @class Import translation
   2273  */
   2274 Zotero.Translate.Import = function() {
   2275 	this.init();
   2276 }
   2277 Zotero.Translate.Import.prototype = new Zotero.Translate.Base();
   2278 Zotero.Translate.Import.prototype.type = "import";
   2279 Zotero.Translate.Import.prototype._entryFunctionSuffix = "Import";
   2280 Zotero.Translate.Import.prototype._io = false;
   2281 
   2282 Zotero.Translate.Import.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Import);
   2283 
   2284 /**
   2285  * Sets string for translation and initializes string IO
   2286  */
   2287 Zotero.Translate.Import.prototype.setString = function(string) {
   2288 	this._string = string;
   2289 	this._io = false;
   2290 }
   2291 
   2292 /**
   2293  * Overload {@link Zotero.Translate.Base#complete} to close file
   2294  */
   2295 Zotero.Translate.Import.prototype.complete = function(returnValue, error) {
   2296 	if(this._io) {
   2297 		this._progress = null;
   2298 		this._io.close(false);
   2299 	}
   2300 	
   2301 	// call super
   2302 	Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
   2303 }
   2304 
   2305 /**
   2306  * Get all potential import translators, ordering translators with the right file extension first
   2307  */
   2308 Zotero.Translate.Import.prototype._getTranslatorsGetPotentialTranslators = function() {
   2309 	return (this.location ?
   2310 	        Zotero.Translators.getImportTranslatorsForLocation(this.location) :
   2311 	        Zotero.Translators.getAllForType(this.type)).
   2312 	then(function(translators) { return [translators] });;
   2313 }
   2314 
   2315 /**
   2316  * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately only
   2317  * if no string or location is set
   2318  */
   2319 Zotero.Translate.Import.prototype.getTranslators = function() {
   2320 	if(!this._string && !this.location) {
   2321 		if(this._currentState === "detect") throw new Error("getTranslators: detection is already running");
   2322 		this._currentState = "detect";
   2323 		var me = this;
   2324 		return Zotero.Translators.getAllForType(this.type).
   2325 		then(function(translators) {
   2326 			me._potentialTranslators = [];
   2327 			me._foundTranslators = translators;
   2328 			me.complete(true);
   2329 			return me._foundTranslators;
   2330 		});
   2331 	} else {
   2332 		return Zotero.Translate.Base.prototype.getTranslators.call(this);
   2333 	}
   2334 }
   2335 	
   2336 /**
   2337  * Overload {@link Zotero.Translate.Base#_loadTranslator} to prepare translator IO
   2338  */
   2339 Zotero.Translate.Import.prototype._loadTranslator = function(translator) {
   2340 	return Zotero.Translate.Base.prototype._loadTranslator.call(this, translator)
   2341 	.then(function() {
   2342 		return this._loadTranslatorPrepareIO(translator);
   2343 	}.bind(this));
   2344 }
   2345 	
   2346 /**
   2347  * Prepare translator IO
   2348  */
   2349 Zotero.Translate.Import.prototype._loadTranslatorPrepareIO = Zotero.Promise.method(function (translator) {
   2350 	var configOptions = this._translatorInfo.configOptions;
   2351 	var dataMode = configOptions ? configOptions["dataMode"] : "";
   2352 	
   2353 	if(!this._io) {
   2354 		if(Zotero.Translate.IO.Read && this.location && this.location instanceof Components.interfaces.nsIFile) {
   2355 			this._io = new Zotero.Translate.IO.Read(this.location, this._sandboxManager);
   2356 		} else {
   2357 			this._io = new Zotero.Translate.IO.String(this._string, this.path ? this.path : "", this._sandboxManager);
   2358 		}
   2359 	}
   2360 	
   2361 	this._io.init(dataMode);
   2362 	this._sandboxManager.importObject(this._io);
   2363 });
   2364 
   2365 /**
   2366  * Prepare translation
   2367  */
   2368 Zotero.Translate.Import.prototype._prepareTranslation = Zotero.Promise.method(function () {
   2369 	this._progress = undefined;
   2370 	
   2371 	var baseURI = null;
   2372 	if(this.location) {
   2373 		try {
   2374 			baseURI = Components.classes["@mozilla.org/network/io-service;1"].
   2375 				getService(Components.interfaces.nsIIOService).newFileURI(this.location);
   2376 		} catch(e) {}
   2377 	}
   2378 
   2379 	this._itemSaver = new Zotero.Translate.ItemSaver({
   2380 		libraryID: this._libraryID,
   2381 		collections: this._collections,
   2382 		forceTagType: this._forceTagType,
   2383 		attachmentMode: Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")],
   2384 		baseURI,
   2385 		saveOptions: Object.assign(
   2386 			{
   2387 				skipSelect: true
   2388 			},
   2389 			this._saveOptions || {}
   2390 		)
   2391 	});
   2392 	this.newItems = [];
   2393 	this.newCollections = [];
   2394 });
   2395 
   2396 /**
   2397  * Return the progress of the import operation, or null if progress cannot be determined
   2398  */
   2399 Zotero.Translate.Import.prototype.getProgress = function() {
   2400 	if(this._progress !== undefined) return this._progress;
   2401 	if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 || this._mode === "xml/e4x" || this._mode == "xml/dom" || !this._io) {
   2402 		return null;
   2403 	}
   2404 	return this._io.bytesRead/this._io.contentLength*100;
   2405 };
   2406 	
   2407 
   2408 /**
   2409  * @class Export translation
   2410  */
   2411 Zotero.Translate.Export = function() {
   2412 	this.init();
   2413 }
   2414 Zotero.Translate.Export.prototype = new Zotero.Translate.Base();
   2415 Zotero.Translate.Export.prototype.type = "export";
   2416 Zotero.Translate.Export.prototype._entryFunctionSuffix = "Export";
   2417 Zotero.Translate.Export.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Export);
   2418 
   2419 /**
   2420  * Sets the items to be exported
   2421  * @param {Zotero.Item[]} items
   2422  */
   2423 Zotero.Translate.Export.prototype.setItems = function(items) {
   2424 	this._export = {type: 'items', items: items};
   2425 }
   2426 
   2427 /**
   2428  * Sets the group to be exported (overrides setItems/setCollection)
   2429  * @param {Zotero.Group[]} group
   2430  */
   2431 Zotero.Translate.Export.prototype.setLibraryID = function(libraryID) {
   2432 	this._export = {type: 'library', id: libraryID};
   2433 }
   2434 
   2435 /**
   2436  * Sets the collection to be exported (overrides setItems/setGroup)
   2437  * @param {Zotero.Collection[]} collection
   2438  */
   2439 Zotero.Translate.Export.prototype.setCollection = function(collection) {
   2440 	this._export = {type: 'collection', collection: collection};
   2441 }
   2442 
   2443 /**
   2444  * Sets the translator to be used for export
   2445  *
   2446  * @param {Zotero.Translator|string} Translator object or ID. If this contains a displayOptions
   2447  *    attribute, setDisplayOptions is automatically called with the specified value.
   2448  */
   2449 Zotero.Translate.Export.prototype.setTranslator = function(translator) {
   2450 	if(typeof translator == "object" && translator.displayOptions) {
   2451 		this._displayOptions = translator.displayOptions;
   2452 	}
   2453 	return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]);
   2454 }
   2455 
   2456 /**
   2457  * Sets translator display options. you can also pass a translator (not ID) to
   2458  * setTranslator that includes a displayOptions argument
   2459  */
   2460 Zotero.Translate.Export.prototype.setDisplayOptions = function(displayOptions) {
   2461 	this._displayOptions = displayOptions;
   2462 }
   2463 
   2464 /**
   2465  * Overload {@link Zotero.Translate.Base#complete} to close file and set complete
   2466  */
   2467 Zotero.Translate.Export.prototype.complete = function(returnValue, error) {
   2468 	if(this._io) {
   2469 		this._progress = null;
   2470 		this._io.close(true);
   2471 		if(this._io instanceof Zotero.Translate.IO.String) {
   2472 			this.string = this._io.string;
   2473 		}
   2474 	}
   2475 	
   2476 	// call super
   2477 	Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
   2478 }
   2479 
   2480 /**
   2481  * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately
   2482  */
   2483 Zotero.Translate.Export.prototype.getTranslators = function() {
   2484 	if(this._currentState === "detect") {
   2485 		return Zotero.Promise.reject(new Error("getTranslators: detection is already running"));
   2486 	}
   2487 	var me = this;
   2488 	return Zotero.Translators.getAllForType(this.type).then(function(translators) {
   2489 		me._currentState = "detect";
   2490 		me._foundTranslators = translators;
   2491 		me._potentialTranslators = [];
   2492 		me.complete(true);
   2493 		return me._foundTranslators;
   2494 	});
   2495 }
   2496 
   2497 /**
   2498  * Does the actual export, after code has been loaded and parsed
   2499  */
   2500 Zotero.Translate.Export.prototype._prepareTranslation = Zotero.Promise.method(function () {
   2501 	this._progress = undefined;
   2502 	
   2503 	// initialize ItemGetter
   2504 	this._itemGetter = new Zotero.Translate.ItemGetter();
   2505 	
   2506 	// Toggle legacy mode for translators pre-4.0.27
   2507 	this._itemGetter.legacy = Services.vc.compare('4.0.27', this._translatorInfo.minVersion) > 0;
   2508 	
   2509 	var configOptions = this._translatorInfo.configOptions || {},
   2510 		getCollections = configOptions.getCollections || false;
   2511 	var loadPromise = Zotero.Promise.resolve();
   2512 	switch (this._export.type) {
   2513 		case 'collection':
   2514 			this._itemGetter.setCollection(this._export.collection, getCollections);
   2515 			break;
   2516 		case 'items':
   2517 			this._itemGetter.setItems(this._export.items);
   2518 			break;
   2519 		case 'library':
   2520 			loadPromise = this._itemGetter.setAll(this._export.id, getCollections);
   2521 			break;
   2522 		default:
   2523 			throw new Error('No export set up');
   2524 			break;
   2525 	}
   2526 	delete this._export;
   2527 	
   2528 	if (this.noWait) {
   2529 		if (!loadPromise.isResolved()) {
   2530 			throw new Error("Load promise is not resolved in noWait mode");
   2531 		}
   2532 		rest.apply(this, arguments);
   2533 	} else {
   2534 		return loadPromise.then(() => rest.apply(this, arguments))
   2535 	}
   2536 	
   2537 	function rest() {
   2538 		// export file data, if requested
   2539 		if(this._displayOptions["exportFileData"]) {
   2540 			this.location = this._itemGetter.exportFiles(this.location, this.translator[0].target);
   2541 		}
   2542 
   2543 		// initialize IO
   2544 		// this is currently hackish since we pass null callbacks to the init function (they have
   2545 		// callbacks to be consistent with import, but they are synchronous, so we ignore them)
   2546 		if(!this.location) {
   2547 			this._io = new Zotero.Translate.IO.String(null, this.path ? this.path : "", this._sandboxManager);
   2548 			this._io.init(configOptions["dataMode"], function() {});
   2549 		} else if(!Zotero.Translate.IO.Write) {
   2550 			throw new Error("Writing to files is not supported in this build of Zotero.");
   2551 		} else {
   2552 			this._io = new Zotero.Translate.IO.Write(this.location);
   2553 			this._io.init(configOptions["dataMode"],
   2554 				this._displayOptions["exportCharset"] ? this._displayOptions["exportCharset"] : null,
   2555 				function() {});
   2556 		}
   2557 
   2558 		this._sandboxManager.importObject(this._io);
   2559 	}
   2560 });
   2561 
   2562 /**
   2563  * Overload Zotero.Translate.Base#translate to make sure that
   2564  *   Zotero.Translate.Export#translate is not called without setting a
   2565  *   translator first. Doesn't make sense to run detection for export.
   2566  */
   2567 Zotero.Translate.Export.prototype.translate = function() {
   2568 	if(!this.translator || !this.translator.length) {
   2569 		this.complete(false, new Error("Export translation initiated without setting a translator"));
   2570 	} else {
   2571 		return Zotero.Translate.Base.prototype.translate.apply(this, arguments);
   2572 	}
   2573 };
   2574 
   2575 /**
   2576  * Return the progress of the import operation, or null if progress cannot be determined
   2577  */
   2578 Zotero.Translate.Export.prototype.getProgress = function() {
   2579 	if(this._progress !== undefined) return this._progress;
   2580 	if(!this._itemGetter) {
   2581 		return null;
   2582 	}
   2583 	return (1-this._itemGetter.numItemsRemaining/this._itemGetter.numItems)*100;
   2584 };
   2585 
   2586 /**
   2587  * @class Search translation
   2588  * @property {Array[]} search Item (in {@link Zotero.Item#serialize} format) to extrapolate data
   2589  *    (set with setSearch)
   2590  */
   2591 Zotero.Translate.Search = function() {
   2592 	this.init();
   2593 };
   2594 Zotero.Translate.Search.prototype = new Zotero.Translate.Base();
   2595 Zotero.Translate.Search.prototype.type = "search";
   2596 Zotero.Translate.Search.prototype._entryFunctionSuffix = "Search";
   2597 Zotero.Translate.Search.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Search);
   2598 Zotero.Translate.Search.prototype.ERROR_NO_RESULTS = "No items returned from any translator";
   2599 
   2600 /**
   2601  * @borrows Zotero.Translate.Web#setCookieSandbox
   2602  */
   2603 Zotero.Translate.Search.prototype.setCookieSandbox = Zotero.Translate.Web.prototype.setCookieSandbox;
   2604 
   2605 /**
   2606  * Sets the item to be used for searching
   2607  * @param {Object} item An item, with as many fields as desired, in the format returned by
   2608  *     {@link Zotero.Item#serialize}
   2609  */
   2610 Zotero.Translate.Search.prototype.setSearch = function(search) {
   2611 	this.search = search;
   2612 }
   2613 
   2614 /**
   2615  * Set an identifier to use for searching
   2616  *
   2617  * @param {Object} identifier - An object with 'DOI', 'ISBN', or 'PMID'
   2618  */
   2619 Zotero.Translate.Search.prototype.setIdentifier = function (identifier) {
   2620 	var search;
   2621 	if (identifier.DOI) {
   2622 		search = {
   2623 			itemType: "journalArticle",
   2624 			DOI: identifier.DOI
   2625 		};
   2626 	}
   2627 	else if (identifier.ISBN) {
   2628 		search = {
   2629 			itemType: "book",
   2630 			ISBN: identifier.ISBN
   2631 		};
   2632 	}
   2633 	else if (identifier.PMID) {
   2634 		search = {
   2635 			itemType: "journalArticle",
   2636 			contextObject: "rft_id=info:pmid/" + identifier.PMID
   2637 		};
   2638 	}
   2639 	else if (identifier.arXiv) {
   2640 		search = {
   2641 			itemType: "journalArticle",
   2642 			arXiv: identifier.arXiv
   2643 		};
   2644 	}
   2645 	else {
   2646 		throw new Error("Unrecognized identifier");
   2647 	}
   2648 	this.setSearch(search);
   2649 }
   2650 
   2651 /**
   2652  * Overloads {@link Zotero.Translate.Base#getTranslators} to always return all potential translators
   2653  */
   2654 Zotero.Translate.Search.prototype.getTranslators = function() {
   2655 	return Zotero.Translate.Base.prototype.getTranslators.call(this, true);
   2656 }
   2657 
   2658 /**
   2659  * Sets the translator or translators to be used for search
   2660  *
   2661  * @param {Zotero.Translator|string} Translator object or ID
   2662  */
   2663 Zotero.Translate.Search.prototype.setTranslator = function(translator) {
   2664 	// Accept an array of translators
   2665 	if (Array.isArray(translator)) {
   2666 		this.translator = translator;
   2667 		return true;
   2668 	}
   2669 	return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]);
   2670 }
   2671 
   2672 /**
   2673  * Overload Zotero.Translate.Base#complete to move onto the next translator if
   2674  * translation fails
   2675  */
   2676 Zotero.Translate.Search.prototype.complete = function(returnValue, error) {
   2677 	if(this._currentState == "translate"
   2678 			&& (!this.newItems || !this.newItems.length)
   2679 			&& !this._savingItems
   2680 			//length is 0 only when translate was called without translators
   2681 			&& this.translator.length) {
   2682 		Zotero.debug("Translate: Could not find a result using " + this.translator[0].label
   2683 			+ (this.translator.length > 1 ? " -- trying next translator" : ""), 3);
   2684 		if(error) Zotero.debug(this._generateErrorString(error), 3);
   2685 		if(this.translator.length > 1) {
   2686 			this.translator.shift();
   2687 			this.translate({
   2688 				libraryID: this._libraryID,
   2689 				saveAttachments: this._saveAttachments,
   2690 				collections: this._collections
   2691 			});
   2692 			return;
   2693 		} else {
   2694 			Zotero.debug("No more translators to try");
   2695 			error = this.ERROR_NO_RESULTS;
   2696 			returnValue = false;
   2697 		}
   2698 	}
   2699 	
   2700 	// call super
   2701 	Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
   2702 }
   2703 
   2704 /**
   2705  * Pass search item to detect* and do* functions
   2706  */
   2707 Zotero.Translate.Search.prototype._getParameters = function() {
   2708 	if(Zotero.isFx) {
   2709 		return [this._sandboxManager.copyObject(this.search)];
   2710 	}
   2711 	return [this.search];
   2712 };
   2713 
   2714 /**
   2715  * Extract sandbox location from translator target
   2716  */
   2717 Zotero.Translate.Search.prototype._getSandboxLocation = function() {
   2718 	// generate sandbox for search by extracting domain from translator target
   2719 	if(this.translator && this.translator[0] && this.translator[0].target) {
   2720 		// so that web translators work too
   2721 		const searchSandboxRe = /^http:\/\/[\w.]+\//;
   2722 		var tempURL = this.translator[0].target.replace(/\\/g, "").replace(/\^/g, "");
   2723 		var m = searchSandboxRe.exec(tempURL);
   2724 		if(m) return m[0];
   2725 	}
   2726 	return Zotero.Translate.Base.prototype._getSandboxLocation.call(this);
   2727 }
   2728 
   2729 Zotero.Translate.Search.prototype._prepareTranslation = Zotero.Translate.Web.prototype._prepareTranslation;
   2730 
   2731 /**
   2732  * IO-related functions
   2733  * @namespace
   2734  */
   2735 Zotero.Translate.IO = {
   2736 	/**
   2737 	 * Parses XML using DOMParser
   2738 	 */
   2739 	"parseDOMXML":function(input, charset, size) {
   2740 		try {
   2741 			var dp = new DOMParser();
   2742 		} catch(e) {
   2743 			try {
   2744 				var dp = Components.classes["@mozilla.org/xmlextras/domparser;1"]
   2745 				   .createInstance(Components.interfaces.nsIDOMParser);
   2746 			} catch(e) {
   2747 				throw new Error("DOMParser not supported");
   2748 			}
   2749 		}
   2750 		
   2751 		if(typeof input == "string") {
   2752 			var nodes = dp.parseFromString(input, "text/xml");
   2753 		} else {
   2754 			var nodes = dp.parseFromStream(input, charset, size, "text/xml");
   2755 		}
   2756 		
   2757 		if(nodes.getElementsByTagName("parsererror").length) {
   2758 			throw "DOMParser error: loading data into data store failed";
   2759 		}
   2760 		
   2761 		if("normalize" in nodes) nodes.normalize();
   2762 		
   2763 		return nodes;
   2764 	},
   2765 	
   2766 	/**
   2767 	 * Names of RDF data modes
   2768 	 */
   2769 	"rdfDataModes":["rdf", "rdf/xml", "rdf/n3"]
   2770 };
   2771 
   2772 /******* String support *******/
   2773 
   2774 /**
   2775  * @class Translate backend for translating from a string
   2776  */
   2777 Zotero.Translate.IO.String = function(string, uri, sandboxManager) {
   2778 	if(string && typeof string === "string") {
   2779 		this.string = string;
   2780 	} else {
   2781 		this.string = "";
   2782 	}
   2783 	this.contentLength = this.string.length;
   2784 	this.bytesRead = 0;
   2785 	this._uri = uri;
   2786 	this._sandboxManager = sandboxManager;
   2787 }
   2788 
   2789 Zotero.Translate.IO.String.prototype = {
   2790 	"__exposedProps__":{
   2791 		"RDF":"r",
   2792 		"read":"r",
   2793 		"write":"r",
   2794 		"setCharacterSet":"r",
   2795 		"getXML":"r"
   2796 	},
   2797 	
   2798 	"_initRDF": function () {
   2799 		Zotero.debug("Translate: Initializing RDF data store");
   2800 		this._dataStore = new Zotero.RDF.AJAW.IndexedFormula();
   2801 		this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore);
   2802 		
   2803 		if(this.contentLength) {
   2804 			try {
   2805 				var xml = Zotero.Translate.IO.parseDOMXML(this.string);
   2806 			} catch(e) {
   2807 				this._xmlInvalid = true;
   2808 				throw e;
   2809 			}
   2810 			var parser = new Zotero.RDF.AJAW.RDFParser(this._dataStore);
   2811 			parser.parse(xml, this._uri);
   2812 		}
   2813 	},
   2814 	
   2815 	"setCharacterSet":function(charset) {},
   2816 	
   2817 	"read":function(bytes) {
   2818 		// if we are reading in RDF data mode and no string is set, serialize current RDF to the
   2819 		// string
   2820 		if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") {
   2821 			this.string = this.RDF.serialize();
   2822 		}
   2823 		
   2824 		// return false if string has been read
   2825 		if(this.bytesRead >= this.contentLength) {
   2826 			return false;
   2827 		}
   2828 		
   2829 		if(bytes !== undefined) {
   2830 			if(this.bytesRead >= this.contentLength) return false;
   2831 			var oldPointer = this.bytesRead;
   2832 			this.bytesRead += bytes;
   2833 			return this.string.substr(oldPointer, bytes);
   2834 		} else {
   2835 			// bytes not specified; read a line
   2836 			var oldPointer = this.bytesRead;
   2837 			var lfIndex = this.string.indexOf("\n", this.bytesRead);
   2838 			
   2839 			if(lfIndex !== -1) {
   2840 				// in case we have a CRLF
   2841 				this.bytesRead = lfIndex+1;
   2842 				if(this.contentLength > lfIndex && this.string.substr(lfIndex-1, 1) === "\r") {
   2843 					lfIndex--;
   2844 				}
   2845 				return this.string.substr(oldPointer, lfIndex-oldPointer);					
   2846 			}
   2847 			
   2848 			if(!this._noCR) {
   2849 				var crIndex = this.string.indexOf("\r", this.bytesRead);
   2850 				if(crIndex === -1) {
   2851 					this._noCR = true;
   2852 				} else {
   2853 					this.bytesRead = crIndex+1;
   2854 					return this.string.substr(oldPointer, crIndex-oldPointer-1);
   2855 				}
   2856 			}
   2857 			
   2858 			this.bytesRead = this.contentLength;
   2859 			return this.string.substr(oldPointer);
   2860 		}
   2861 	},
   2862 	
   2863 	"write":function(data) {
   2864 		this.string += data;
   2865 		this.contentLength = this.string.length;
   2866 	},
   2867 	
   2868 	"getXML":function() {
   2869 		try {
   2870 			var xml = Zotero.Translate.IO.parseDOMXML(this.string);
   2871 		} catch(e) {
   2872 			this._xmlInvalid = true;
   2873 			throw e;
   2874 		}
   2875 		return (Zotero.isFx && !Zotero.isBookmarklet ? this._sandboxManager.wrap(xml) : xml);
   2876 	},
   2877 	
   2878 	init: function (newMode) {
   2879 		this.bytesRead = 0;
   2880 		this._noCR = undefined;
   2881 		
   2882 		this._mode = newMode;
   2883 		if(newMode === "xml/e4x") {
   2884 			throw new Error("E4X is not supported");
   2885 		} else if(newMode && (Zotero.Translate.IO.rdfDataModes.indexOf(newMode) !== -1
   2886 				|| newMode.substr(0, 3) === "xml/dom") && this._xmlInvalid) {
   2887 			throw new Error("XML known invalid");
   2888 		} else if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) {
   2889 			this._initRDF();
   2890 		}
   2891 	},
   2892 	
   2893 	"close":function(serialize) {
   2894 		// if we are writing in RDF data mode and no string is set, serialize current RDF to the
   2895 		// string
   2896 		if(serialize && Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") {
   2897 			this.string = this.RDF.serialize();
   2898 		}
   2899 	}
   2900 }
   2901 
   2902 /****** RDF DATA MODE ******/
   2903 
   2904 /**
   2905  * @class An API for handling RDF from the sandbox. This is exposed to translators as Zotero.RDF.
   2906  *
   2907  * @property {Zotero.RDF.AJAW.IndexedFormula} _dataStore
   2908  * @property {Integer[]} _containerCounts
   2909  * @param {Zotero.RDF.AJAW.IndexedFormula} dataStore
   2910  */
   2911 Zotero.Translate.IO._RDFSandbox = function(dataStore) {
   2912 	this._dataStore = dataStore;
   2913 }
   2914 
   2915 Zotero.Translate.IO._RDFSandbox.prototype = {
   2916 	"_containerCounts":[],
   2917 	"__exposedProps__":{
   2918 		"addStatement":"r",
   2919 		"newResource":"r",
   2920 		"newContainer":"r",
   2921 		"addContainerElement":"r",
   2922 		"getContainerElements":"r",
   2923 		"addNamespace":"r",
   2924 		"getAllResources":"r",
   2925 		"getResourceURI":"r",
   2926 		"getArcsIn":"r",
   2927 		"getArcsOut":"r",
   2928 		"getSources":"r",
   2929 		"getTargets":"r",
   2930 		"getStatementsMatching":"r",
   2931 		"serialize":"r"
   2932 	},
   2933 	
   2934 	/**
   2935 	 * Gets a resource as a Zotero.RDF.AJAW.Symbol, rather than a string
   2936 	 * @param {String|Zotero.RDF.AJAW.Symbol} about
   2937 	 * @return {Zotero.RDF.AJAW.Symbol}
   2938 	 */
   2939 	"_getResource":function(about) {
   2940 		return (typeof about == "object" ? about : new Zotero.RDF.AJAW.Symbol(about));
   2941 	},
   2942 	
   2943 	/**
   2944 	 * Runs a callback to initialize this RDF store
   2945 	 */
   2946 	"_init":function() {
   2947 		if(this._prepFunction) {
   2948 			this._dataStore = this._prepFunction();
   2949 			delete this._prepFunction;
   2950 		}
   2951 	},
   2952 	
   2953 	/**
   2954 	 * Serializes the current RDF to a string
   2955 	 */
   2956 	"serialize":function(dataMode) {
   2957 		var serializer = Zotero.RDF.AJAW.Serializer(this._dataStore);
   2958 		
   2959 		for(var prefix in this._dataStore.namespaces) {
   2960 			serializer.suggestPrefix(prefix, this._dataStore.namespaces[prefix]);
   2961 		}
   2962 		
   2963 		// serialize in appropriate format
   2964 		if(dataMode == "rdf/n3") {
   2965 			return serializer.statementsToN3(this._dataStore.statements);
   2966 		}
   2967 		
   2968 		return serializer.statementsToXML(this._dataStore.statements);
   2969 	},
   2970 	
   2971 	/**
   2972 	 * Adds an RDF triple
   2973 	 * @param {String|Zotero.RDF.AJAW.Symbol} about
   2974 	 * @param {String|Zotero.RDF.AJAW.Symbol} relation
   2975 	 * @param {String|Zotero.RDF.AJAW.Symbol} value
   2976 	 * @param {Boolean} literal Whether value should be treated as a literal (true) or a resource
   2977 	 *     (false)
   2978 	 */
   2979 	"addStatement":function(about, relation, value, literal) {
   2980 		if(about === null || about === undefined) {
   2981 			throw new Error("about must be defined in Zotero.RDF.addStatement");
   2982 		}
   2983 		if(relation === null || relation === undefined) {
   2984 			throw new Error("relation must be defined in Zotero.RDF.addStatement");
   2985 		}
   2986 		if(value === null || value === undefined) {
   2987 			throw new Error("value must be defined in Zotero.RDF.addStatement");
   2988 		}
   2989 		
   2990 		if(literal) {
   2991 			// zap chars that Mozilla will mangle
   2992 			value = value.toString().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
   2993 		} else {
   2994 			value = this._getResource(value);
   2995 		}
   2996 		
   2997 		this._dataStore.add(this._getResource(about), this._getResource(relation), value);
   2998 	},
   2999 	
   3000 	/**
   3001 	 * Creates a new anonymous resource
   3002 	 * @return {Zotero.RDF.AJAW.Symbol}
   3003 	 */
   3004 	"newResource":function() {
   3005 		return new Zotero.RDF.AJAW.BlankNode();
   3006 	},
   3007 	
   3008 	/**
   3009 	 * Creates a new container resource
   3010 	 * @param {String} type The type of the container ("bag", "seq", or "alt")
   3011 	 * @param {String|Zotero.RDF.AJAW.Symbol} about The URI of the resource
   3012 	 * @return {Zotero.Translate.RDF.prototype.newContainer
   3013 	 */
   3014 	"newContainer":function(type, about) {
   3015 		const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
   3016 		const containerTypes = {"bag":"Bag", "seq":"Seq", "alt":"Alt"};
   3017 		
   3018 		type = type.toLowerCase();
   3019 		if(!containerTypes[type]) {
   3020 			throw new Error("Invalid container type in Zotero.RDF.newContainer");
   3021 		}
   3022 		
   3023 		var about = this._getResource(about);
   3024 		this.addStatement(about, rdf+"type", rdf+containerTypes[type], false);
   3025 		this._containerCounts[about.toNT()] = 1;
   3026 		
   3027 		return about;
   3028 	},
   3029 	
   3030 	/**
   3031 	 * Adds a new element to a container
   3032 	 * @param {String|Zotero.RDF.AJAW.Symbol} about The container
   3033 	 * @param {String|Zotero.RDF.AJAW.Symbol} element The element to add to the container
   3034 	 * @param {Boolean} literal Whether element should be treated as a literal (true) or a resource
   3035 	 *     (false)
   3036 	 */
   3037 	"addContainerElement":function(about, element, literal) {
   3038 		const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
   3039 	
   3040 		var about = this._getResource(about);
   3041 		this._dataStore.add(about, new Zotero.RDF.AJAW.Symbol(rdf+"_"+(this._containerCounts[about.toNT()]++)), element, literal);
   3042 	},
   3043 	
   3044 	/**
   3045 	 * Gets all elements within a container
   3046 	 * @param {String|Zotero.RDF.AJAW.Symbol} about The container
   3047 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3048 	 */
   3049 	"getContainerElements":function(about) {
   3050 		const liPrefix = "http://www.w3.org/1999/02/22-rdf-syntax-ns#_";
   3051 		
   3052 		var about = this._getResource(about);
   3053 		var statements = this._dataStore.statementsMatching(about);
   3054 		var containerElements = [];
   3055 		
   3056 		// loop over arcs out looking for list items
   3057 		for(var i=0; i<statements.length; i++) {
   3058 			var statement = statements[i];
   3059 			if(statement.predicate.uri.substr(0, liPrefix.length) == liPrefix) {
   3060 				var number = statement.predicate.uri.substr(liPrefix.length);
   3061 				
   3062 				// make sure these are actually numeric list items
   3063 				var intNumber = parseInt(number);
   3064 				if(number == intNumber.toString()) {
   3065 					// add to element array
   3066 					containerElements[intNumber-1] = (statement.object.termType == "literal" ? statement.object.toString() : statement.object);
   3067 				}
   3068 			}
   3069 		}
   3070 		
   3071 		return containerElements;
   3072 	},
   3073 	
   3074 	/**
   3075 	 * Adds a namespace for a specific URI
   3076 	 * @param {String} prefix Namespace prefix
   3077 	 * @param {String} uri Namespace URI
   3078 	 */
   3079 	"addNamespace":function(prefix, uri) {
   3080 		this._dataStore.setPrefixForURI(prefix, uri);
   3081 	},
   3082 	
   3083 	/**
   3084 	 * Gets the URI a specific resource
   3085 	 * @param {String|Zotero.RDF.AJAW.Symbol} resource
   3086 	 * @return {String}
   3087 	 */
   3088 	"getResourceURI":function(resource) {
   3089 		if(typeof(resource) == "string") return resource;
   3090 		
   3091 		const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
   3092 		var values = this.getTargets(resource, rdf + 'value');
   3093 		if(values && values.length) return this.getResourceURI(values[0]);
   3094 		
   3095 		if(resource.uri) return resource.uri;
   3096 		if(resource.toNT == undefined) throw new Error("Zotero.RDF: getResourceURI called on invalid resource");
   3097 		return resource.toNT();
   3098 	},
   3099 	
   3100 	/**
   3101 	 * Gets all resources in the RDF data store
   3102 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3103 	 */
   3104 	"getAllResources":function() {
   3105 		var returnArray = [];
   3106 		for(var i in this._dataStore.subjectIndex) {
   3107 			returnArray.push(this._dataStore.subjectIndex[i][0].subject);
   3108 		}
   3109 		return returnArray;
   3110 	},
   3111 	
   3112 	/**
   3113 	 * Gets all arcs (predicates) into a resource
   3114 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3115 	 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
   3116 	 */
   3117 	"getArcsIn":function(resource) {
   3118 		var statements = this._dataStore.objectIndex[this._dataStore.canon(this._getResource(resource))];
   3119 		if(!statements) return false;
   3120 		
   3121 		var returnArray = [];
   3122 		for(var i=0; i<statements.length; i++) {
   3123 			returnArray.push(statements[i].predicate.uri);
   3124 		}
   3125 		return returnArray;
   3126 	},
   3127 	
   3128 	/**
   3129 	 * Gets all arcs (predicates) out of a resource
   3130 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3131 	 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
   3132 	 */
   3133 	"getArcsOut":function(resource) {
   3134 		var statements = this._dataStore.subjectIndex[this._dataStore.canon(this._getResource(resource))];
   3135 		if(!statements) return false;
   3136 		
   3137 		var returnArray = [];
   3138 		for(var i=0; i<statements.length; i++) {
   3139 			returnArray.push(statements[i].predicate.uri);
   3140 		}
   3141 		return returnArray;
   3142 	},
   3143 	
   3144 	/**
   3145 	 * Gets all subjects whose predicates point to a resource
   3146 	 * @param {String|Zotero.RDF.AJAW.Symbol} resource Subject that predicates should point to
   3147 	 * @param {String|Zotero.RDF.AJAW.Symbol} property Predicate
   3148 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3149 	 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
   3150 	 */
   3151 	"getSources":function(resource, property) {
   3152 		var statements = this._dataStore.statementsMatching(undefined, this._getResource(property), this._getResource(resource));
   3153 		if(!statements.length) return false;
   3154 		
   3155 		var returnArray = [];
   3156 		for(var i=0; i<statements.length; i++) {
   3157 			returnArray.push(statements[i].subject);
   3158 		}
   3159 		return returnArray;
   3160 	},
   3161 	
   3162 	/**
   3163 	 * Gets all objects of a given subject with a given predicate
   3164 	 * @param {String|Zotero.RDF.AJAW.Symbol} resource Subject
   3165 	 * @param {String|Zotero.RDF.AJAW.Symbol} property Predicate
   3166 	 * @return {Zotero.RDF.AJAW.Symbol[]}
   3167 	 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
   3168 	 */
   3169 	"getTargets":function(resource, property) {
   3170 		var statements = this._dataStore.statementsMatching(this._getResource(resource), this._getResource(property));
   3171 		if(!statements.length) return false;
   3172 		
   3173 		var returnArray = [];
   3174 		for(var i=0; i<statements.length; i++) {
   3175 			returnArray.push(statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object);
   3176 		}
   3177 		return returnArray;
   3178 	},
   3179 	
   3180 	/**
   3181 	 * Gets statements matching a certain pattern
   3182 	 *
   3183 	 * @param	{String|Zotero.RDF.AJAW.Symbol}	subj 		Subject
   3184 	 * @param	{String|Zotero.RDF.AJAW.Symbol}	predicate	Predicate
   3185 	 * @param	{String|Zotero.RDF.AJAW.Symbol}	obj			Object
   3186 	 * @param	{Boolean}							objLiteral	Whether the object is a literal (as
   3187 	 *															opposed to a URI)
   3188 	 * @param	{Boolean}							justOne		Whether to stop when a single result is
   3189 	 *															retrieved
   3190 	 */
   3191 	"getStatementsMatching":function(subj, pred, obj, objLiteral, justOne) {
   3192 		var statements = this._dataStore.statementsMatching(
   3193 			(subj ? this._getResource(subj) : undefined),
   3194 			(pred ? this._getResource(pred) : undefined),
   3195 			(obj ? (objLiteral ? objLiteral : this._getResource(obj)) : undefined),
   3196 			undefined, justOne);
   3197 		if(!statements.length) return false;
   3198 		
   3199 		
   3200 		var returnArray = [];
   3201 		for(var i=0; i<statements.length; i++) {
   3202 			returnArray.push([statements[i].subject, statements[i].predicate, (statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object)]);
   3203 		}
   3204 		return returnArray;
   3205 	}
   3206 };