www

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

zotero.js (82793B)


      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 // Commonly used imports accessible anywhere
     27 Components.utils.import("resource://zotero/config.js");
     28 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
     29 Components.utils.import("resource://gre/modules/Services.jsm");
     30 Components.utils.import("resource://gre/modules/osfile.jsm");
     31 Components.utils.import("resource://gre/modules/PluralForm.jsm");
     32 Components.classes["@mozilla.org/net/osfileconstantsservice;1"]
     33 	.getService(Components.interfaces.nsIOSFileConstantsService)
     34 	.init();
     35 
     36 Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
     37 
     38 /*
     39  * Core functions
     40  */
     41  (function(){
     42 	// Privileged (public) methods
     43 	this.getStorageDirectory = getStorageDirectory;
     44 	this.debug = debug;
     45 	this.log = log;
     46 	this.logError = logError;
     47 	this.localeJoin = localeJoin;
     48 	this.setFontSize = setFontSize;
     49 	this.flattenArguments = flattenArguments;
     50 	this.getAncestorByTagName = getAncestorByTagName;
     51 	this.randomString = randomString;
     52 	this.moveToUnique = moveToUnique;
     53 	this.reinit = reinit; // defined in zotero-service.js
     54 	
     55 	// Public properties
     56 	this.initialized = false;
     57 	this.skipLoading = false;
     58 	this.startupError;
     59 	this.__defineGetter__("startupErrorHandler", function() { return _startupErrorHandler; });
     60 	this.version;
     61 	this.platform;
     62 	this.locale;
     63 	this.dir; // locale direction: 'ltr' or 'rtl'
     64 	this.isMac;
     65 	this.isWin;
     66 	this.initialURL; // used by Schema to show the changelog on upgrades
     67 	this.Promise = require('resource://zotero/bluebird.js');
     68 	
     69 	this.getActiveZoteroPane = function() {
     70 		var win = Services.wm.getMostRecentWindow("navigator:browser");
     71 		return win ? win.ZoteroPane : null;
     72 	};
     73 	
     74 	/**
     75 	 * @property	{Boolean}	locked		Whether all Zotero panes are locked
     76 	 *										with an overlay
     77 	 */
     78 	this.__defineGetter__('locked', function () { return _locked; });
     79 	this.__defineSetter__('locked', function (lock) {
     80 		var wasLocked = _locked;
     81 		_locked = lock;
     82 		
     83 		if (!wasLocked && lock) {
     84 			this.unlockDeferred = Zotero.Promise.defer();
     85 			this.unlockPromise = this.unlockDeferred.promise;
     86 		}
     87 		else if (wasLocked && !lock) {
     88 			Zotero.debug("Running unlock callbacks");
     89 			this.unlockDeferred.resolve();
     90 		}
     91 	});
     92 	
     93 	/**
     94 	 * @property	{Boolean}	closing		True if the application is closing.
     95 	 */
     96 	this.closing = false;
     97 	
     98 	
     99 	this.unlockDeferred;
    100 	this.unlockPromise;
    101 	this.initializationDeferred;
    102 	this.initializationPromise;
    103 	
    104 	this.hiDPISuffix = "";
    105 	
    106 	var _startupErrorHandler;
    107 	var _localizedStringBundle;
    108 	
    109 	var _locked = false;
    110 	var _shutdownListeners = [];
    111 	var _progressMessage;
    112 	var _progressMeters;
    113 	var _progressPopup;
    114 	var _lastPercentage;
    115 	
    116 	// whether we are waiting for another Zotero process to release its DB lock
    117 	var _waitingForDBLock = false;
    118 	
    119 	/**
    120 	 * Maintains nsITimers to be used when Zotero.wait() completes (to reduce performance penalty
    121 	 * of initializing new objects)
    122 	 */
    123 	var _waitTimers = [];
    124 	
    125 	/**
    126 	 * Maintains nsITimerCallbacks to be used when Zotero.wait() completes
    127 	 */
    128 	var _waitTimerCallbacks = [];
    129 	
    130 	/**
    131 	 * Maintains running nsITimers in global scope, so that they don't disappear randomly
    132 	 */
    133 	var _runningTimers = new Map();
    134 	
    135 	var _startupTime = new Date();
    136 	// Errors that were in the console at startup
    137 	var _startupErrors = [];
    138 	// Number of errors to maintain in the recent errors buffer
    139 	const ERROR_BUFFER_SIZE = 25;
    140 	// A rolling buffer of the last ERROR_BUFFER_SIZE errors
    141 	var _recentErrors = [];
    142 	
    143 	/**
    144 	 * Initialize the extension
    145 	 *
    146 	 * @return {Promise<Boolean>}
    147 	 */
    148 	this.init = Zotero.Promise.coroutine(function* (options) {
    149 		if (this.initialized || this.skipLoading) {
    150 			return false;
    151 		}
    152 		
    153 		this.locked = true;
    154 		this.initializationDeferred = Zotero.Promise.defer();
    155 		this.initializationPromise = this.initializationDeferred.promise;
    156 		this.uiReadyDeferred = Zotero.Promise.defer();
    157 		this.uiReadyPromise = this.uiReadyDeferred.promise;
    158 		
    159 		// Add a function to Zotero.Promise to check whether a value is still defined, and if not
    160 		// to throw a specific error that's ignored by the unhandled rejection handler in
    161 		// bluebird.js. This allows for easily cancelling promises when they're no longer
    162 		// needed, for example after a binding is destroyed.
    163 		//
    164 		// Example usage:
    165 		//
    166 		// getAsync.tap(() => Zotero.Promise.check(this.mode))
    167 		//
    168 		// If the binding is destroyed while getAsync() is being resolved and this.mode no longer
    169 		// exists, subsequent lines won't be run, and nothing will be logged to the console.
    170 		this.Promise.check = function (val) {
    171 			if (!val && val !== 0) {
    172 				let e = new Error;
    173 				e.name = "ZoteroPromiseInterrupt";
    174 				throw e;
    175 			}
    176 		};
    177 		
    178 		if (options) {
    179 			let opts = [
    180 				'openPane',
    181 				'test',
    182 				'automatedTest',
    183 				'skipBundledFiles'
    184 			];
    185 			opts.filter(opt => options[opt]).forEach(opt => this[opt] = true);
    186 			
    187 			this.forceDataDir = options.forceDataDir;
    188 		}
    189 		
    190 		this.mainThread = Services.tm.mainThread;
    191 		
    192 		this.clientName = ZOTERO_CONFIG.CLIENT_NAME;
    193 		
    194 		var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
    195 			.getService(Components.interfaces.nsIXULAppInfo);
    196 		this.platformVersion = appInfo.platformVersion;
    197 		this.platformMajorVersion = parseInt(appInfo.platformVersion.match(/^[0-9]+/)[0]);
    198 		this.isFx = true;
    199 		this.isClient = true;
    200 		this.isStandalone = Services.appinfo.ID == ZOTERO_CONFIG['GUID'];
    201 		
    202 		if (Zotero.isStandalone) {
    203 			var version = Services.appinfo.version;
    204 		}
    205 		else {
    206 			let deferred = Zotero.Promise.defer();
    207 			Components.utils.import("resource://gre/modules/AddonManager.jsm");
    208 			AddonManager.getAddonByID(
    209 				ZOTERO_CONFIG.GUID,
    210 				function (addon) {
    211 					deferred.resolve(addon.version);
    212 				}
    213 			);
    214 			var version = yield deferred.promise;
    215 		}
    216 		Zotero.version = version;
    217 		
    218 		// OS platform
    219 		var win = Components.classes["@mozilla.org/appshell/appShellService;1"]
    220 			   .getService(Components.interfaces.nsIAppShellService)
    221 			   .hiddenDOMWindow;
    222 		this.platform = win.navigator.platform;
    223 		this.isMac = (this.platform.substr(0, 3) == "Mac");
    224 		this.isWin = (this.platform.substr(0, 3) == "Win");
    225 		this.isLinux = (this.platform.substr(0, 5) == "Linux");
    226 		this.oscpu = win.navigator.oscpu;
    227 		
    228 		// Browser
    229 		Zotero.browser = "g";
    230 		
    231 		//
    232 		// Get settings from language pack (extracted by zotero-build/locale/merge_mozilla_files)
    233 		//
    234 		function getIntlProp(name, fallback = null) {
    235 			try {
    236 				return intlProps.GetStringFromName(name);
    237 			}
    238 			catch (e) {
    239 				Zotero.logError(`Couldn't load ${name} from intl.properties`);
    240 				return fallback;
    241 			}
    242 		}
    243 		function setOrClearIntlPref(name, type) {
    244 			var val = getIntlProp(name);
    245 			if (val !== null) {
    246 				if (type == 'boolean') {
    247 					val = val == 'true';
    248 				}
    249 				Zotero.Prefs.set(name, val, 1);
    250 			}
    251 			else {
    252 				Zotero.Prefs.clear(name, 1);
    253 			}
    254 		}
    255 		var intlProps = Services.strings.createBundle("chrome://zotero/locale/mozilla/intl.properties");
    256 		this.locale = getIntlProp('general.useragent.locale', 'en-US');
    257 		let [get, numForms] = PluralForm.makeGetter(parseInt(getIntlProp('pluralRule', 1)));
    258 		this.pluralFormGet = get;
    259 		this.pluralFormNumForms = numForms;
    260 		setOrClearIntlPref('intl.accept_languages', 'string');
    261 		
    262 		// Also load the brand as appName
    263 		var brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
    264 		this.appName = brandBundle.GetStringFromName("brandShortName");
    265 		
    266 		_localizedStringBundle = Services.strings.createBundle("chrome://zotero/locale/zotero.properties");
    267 		
    268 		// Set the locale direction to Zotero.dir
    269 		// DEBUG: is there a better way to get the entity from JS?
    270 		var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
    271 						.createInstance();
    272 		xmlhttp.open('GET', 'chrome://global/locale/global.dtd', false);
    273 		xmlhttp.overrideMimeType('text/plain');
    274 		xmlhttp.send(null);
    275 		var matches = xmlhttp.responseText.match(/(ltr|rtl)/);
    276 		if (matches && matches[0] == 'rtl') {
    277 			Zotero.dir = 'rtl';
    278 		}
    279 		else {
    280 			Zotero.dir = 'ltr';
    281 		}
    282 		Zotero.rtl = Zotero.dir == 'rtl';
    283 		
    284 		Zotero.Prefs.init();
    285 		Zotero.Debug.init(options && options.forceDebugLog);
    286 		
    287 		// Make sure that Zotero Standalone is not running as root
    288 		if(Zotero.isStandalone && !Zotero.isWin) _checkRoot();
    289 		
    290 		_addToolbarIcon();
    291 		
    292 		try {
    293 			yield Zotero.DataDirectory.init();
    294 			if (this.restarting) {
    295 				return;
    296 			}
    297 			var dataDir = Zotero.DataDirectory.dir;
    298 		}
    299 		catch (e) {
    300 			// Zotero dir not found
    301 			if ((e instanceof OS.File.Error && e.becauseNoSuchFile) || e.name == 'NS_ERROR_FILE_NOT_FOUND') {
    302 				let foundInDefault = false;
    303 				try {
    304 					foundInDefault = (yield OS.File.exists(Zotero.DataDirectory.defaultDir))
    305 						&& (yield OS.File.exists(
    306 							OS.Path.join(
    307 								Zotero.DataDirectory.defaultDir,
    308 								Zotero.DataDirectory.getDatabaseFilename()
    309 							)
    310 						));
    311 				}
    312 				catch (e) {
    313 					Zotero.logError(e);
    314 				}
    315 				
    316 				let previousDir = Zotero.Prefs.get('lastDataDir')
    317 					|| Zotero.Prefs.get('dataDir')
    318 					|| e.dataDir;
    319 				Zotero.startupError = foundInDefault
    320 					? Zotero.getString(
    321 						'dataDir.notFound.defaultFound',
    322 						[
    323 							Zotero.clientName,
    324 							previousDir,
    325 							Zotero.DataDirectory.defaultDir
    326 						]
    327 					)
    328 					: Zotero.getString('dataDir.notFound', Zotero.clientName);
    329 				_startupErrorHandler = function() {
    330 					var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
    331 							createInstance(Components.interfaces.nsIPromptService);
    332 					var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
    333 						+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING
    334 						+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
    335 					// TEMP: lastDataDir can be removed once old persistent descriptors have been
    336 					// converted, which they are in getZoteroDirectory() in 5.0
    337 					if (foundInDefault) {
    338 						let index = ps.confirmEx(null,
    339 							Zotero.getString('general.error'),
    340 							Zotero.startupError,
    341 							buttonFlags,
    342 							Zotero.getString('dataDir.useNewLocation'),
    343 							Zotero.getString('general.quit'),
    344 							Zotero.getString('general.locate'),
    345 							null, {}
    346 						);
    347 						// Revert to home directory
    348 						if (index == 0) {
    349 							Zotero.DataDirectory.set(Zotero.DataDirectory.defaultDir);
    350 							Zotero.Utilities.Internal.quit(true);
    351 							return;
    352 						}
    353 						// Locate data directory
    354 						else if (index == 2) {
    355 							Zotero.DataDirectory.choose(true);
    356 						}
    357 
    358 					}
    359 					else {
    360 						let index = ps.confirmEx(null,
    361 							Zotero.getString('general.error'),
    362 							Zotero.startupError
    363 								+ (previousDir
    364 									? '\n\n' + Zotero.getString('dataDir.previousDir') + ' ' + previousDir
    365 									: ''),
    366 							buttonFlags,
    367 							Zotero.getString('general.quit'),
    368 							Zotero.getString('dataDir.useDefaultLocation'),
    369 							Zotero.getString('general.locate'),
    370 							null, {}
    371 						);
    372 						// Revert to home directory
    373 						if (index == 1) {
    374 							Zotero.DataDirectory.set(Zotero.DataDirectory.defaultDir);
    375 							Zotero.Utilities.Internal.quit(true);
    376 							return;
    377 						}
    378 						// Locate data directory
    379 						else if (index == 2) {
    380 							Zotero.DataDirectory.choose(true);
    381 						}
    382 					}
    383 				}
    384 				return;
    385 			}
    386 			// DEBUG: handle more startup errors
    387 			else {
    388 				throw e;
    389 			}
    390 		}
    391 		
    392 		if (!Zotero.isConnector) {
    393 			if (!this.forceDataDir) {
    394 				yield Zotero.DataDirectory.checkForMigration(
    395 					dataDir, Zotero.DataDirectory.defaultDir
    396 				);
    397 				if (this.skipLoading) {
    398 					return;
    399 				}
    400 				
    401 				yield Zotero.DataDirectory.checkForLostLegacy();
    402 				if (this.restarting) {
    403 					return;
    404 				}
    405 			}
    406 			
    407 			// Make sure data directory isn't in Dropbox, etc.
    408 			if (Zotero.isStandalone) {
    409 				yield Zotero.DataDirectory.checkForUnsafeLocation(dataDir);
    410 			}
    411 		}
    412 		
    413 		// Register shutdown handler to call Zotero.shutdown()
    414 		var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }};
    415 		Services.obs.addObserver(_shutdownObserver, "quit-application", false);
    416 		
    417 		try {
    418 			Zotero.IPC.init();
    419 		}
    420 		catch (e) {
    421 			if (_checkDataDirAccessError(e)) {
    422 				return false;
    423 			}
    424 			throw (e);
    425 		}
    426 		
    427 		// Get startup errors
    428 		try {
    429 			var messages = {};
    430 			Services.console.getMessageArray(messages, {});
    431 			_startupErrors = Object.keys(messages.value).map(i => messages[i])
    432 				.filter(msg => _shouldKeepError(msg));
    433 		} catch(e) {
    434 			Zotero.logError(e);
    435 		}
    436 		// Register error observer
    437 		Services.console.registerListener(ConsoleListener);
    438 		
    439 		// Add shutdown listener to remove quit-application observer and console listener
    440 		this.addShutdownListener(function() {
    441 			Services.obs.removeObserver(_shutdownObserver, "quit-application", false);
    442 			Services.console.unregisterListener(ConsoleListener);
    443 		});
    444 		
    445 		// Load additional info for connector or not
    446 		if(Zotero.isConnector) {
    447 			Zotero.debug("Loading in connector mode");
    448 			Zotero.Connector_Types.init();
    449 			
    450 			// Store a startupError until we get information from Zotero Standalone
    451 			Zotero.startupError = Zotero.getString("connector.loadInProgress")
    452 			
    453 			if(!Zotero.isFirstLoadThisSession) {
    454 				// We want to get a checkInitComplete message before initializing if we switched to
    455 				// connector mode because Standalone was launched
    456 				Zotero.IPC.broadcast("checkInitComplete");
    457 			} else {
    458 				Zotero.initComplete();
    459 			}
    460 		} else {
    461 			Zotero.debug("Loading in full mode");
    462 			return _initFull()
    463 			.then(function (success) {
    464 				if (!success) {
    465 					return false;
    466 				}
    467 				
    468 				if(Zotero.isStandalone) Zotero.Standalone.init();
    469 				Zotero.initComplete();
    470 			})
    471 		}
    472 		
    473 		return true;
    474 	});
    475 	
    476 	/**
    477 	 * Triggers events when initialization finishes
    478 	 */
    479 	this.initComplete = function() {
    480 		if(Zotero.initialized) return;
    481 		
    482 		Zotero.debug("Running initialization callbacks");
    483 		delete this.startupError;
    484 		this.initialized = true;
    485 		this.initializationDeferred.resolve();
    486 		
    487 		if(Zotero.isConnector) {
    488 			Zotero.Repo.init();
    489 			Zotero.locked = false;
    490 		}
    491 		
    492 		if(!Zotero.isFirstLoadThisSession) {
    493 			// trigger zotero-reloaded event
    494 			Zotero.debug('Triggering "zotero-reloaded" event');
    495 			Services.obs.notifyObservers(Zotero, "zotero-reloaded", null);
    496 		}
    497 		
    498 		Zotero.debug('Triggering "zotero-loaded" event');
    499 		Services.obs.notifyObservers(Zotero, "zotero-loaded", null);
    500 	}
    501 	
    502 	
    503 	this.uiIsReady = function () {
    504 		if (this.uiReadyPromise.isPending()) {
    505 			Zotero.debug("User interface ready in " + (new Date() - _startupTime) + " ms");
    506 			this.uiReadyDeferred.resolve();
    507 		}
    508 	};
    509 	
    510 	
    511 	var _addToolbarIcon = function () {
    512 		if (Zotero.isStandalone) return;
    513 		
    514 		// Add toolbar icon
    515 		try {
    516 			Services.scriptloader.loadSubScript("chrome://zotero/content/icon.js", {}, "UTF-8");
    517 		}
    518 		catch (e) {
    519 			if (Zotero) {
    520 				Zotero.debug(e, 1);
    521 			}
    522 			Components.utils.reportError(e);
    523 		}
    524 	};
    525 	
    526 	
    527 	/**
    528 	 * Initialization function to be called only if Zotero is in full mode
    529 	 *
    530 	 * @return {Promise:Boolean}
    531 	 */
    532 	var _initFull = Zotero.Promise.coroutine(function* () {
    533 		if (!(yield _initDB())) return false;
    534 		
    535 		Zotero.VersionHeader.init();
    536 		
    537 		// Check for data reset/restore
    538 		var dataDir = Zotero.DataDirectory.dir;
    539 		var restoreFile = OS.Path.join(dataDir, 'restore-from-server');
    540 		var resetDataDirFile = OS.Path.join(dataDir, 'reset-data-directory');
    541 		
    542 		var result = yield Zotero.Promise.all([OS.File.exists(restoreFile), OS.File.exists(resetDataDirFile)]);
    543 		if (result.some(r => r)) {
    544 			[Zotero.restoreFromServer, Zotero.resetDataDir] = result;
    545 			try {
    546 				yield Zotero.DB.closeDatabase();
    547 				
    548 				// TODO: better error handling
    549 				
    550 				// TODO: prompt for location
    551 				// TODO: Back up database
    552 				// TODO: Reset translators and styles
    553 				
    554 				
    555 				
    556 				if (Zotero.restoreFromServer) {
    557 					let dbfile = Zotero.DataDirectory.getDatabase();
    558 					Zotero.debug("Deleting " + dbfile);
    559 					yield OS.File.remove(dbfile, { ignoreAbsent: true });
    560 					let storageDir = OS.Path.join(dataDir, 'storage');
    561 					Zotero.debug("Deleting " + storageDir.path);
    562 					OS.File.removeDir(storageDir, { ignoreAbsent: true }),
    563 					yield OS.File.remove(restoreFile);
    564 					Zotero.restoreFromServer = true;
    565 				}
    566 				else if (Zotero.resetDataDir) {
    567 					Zotero.initAutoSync = true;
    568 					
    569 					// Clear some user prefs
    570 					[
    571 						'sync.server.username',
    572 						'sync.storage.username'
    573 					].forEach(p => Zotero.Prefs.clear(p));
    574 					
    575 					// Clear data directory
    576 					Zotero.debug("Deleting data directory files");
    577 					let lastError;
    578 					// Delete all files in directory rather than removing directory, in case it's
    579 					// a symlink
    580 					yield Zotero.File.iterateDirectory(dataDir, function* (iterator) {
    581 						while (true) {
    582 							let entry = yield iterator.next();
    583 							// Don't delete some files
    584 							if (entry.name == 'pipes') {
    585 								continue;
    586 							}
    587 							Zotero.debug("Deleting " + entry.path);
    588 							try {
    589 								if (entry.isDir) {
    590 									yield OS.File.removeDir(entry.path);
    591 								}
    592 								else {
    593 									yield OS.File.remove(entry.path);
    594 								}
    595 							}
    596 							// Keep trying to delete as much as we can
    597 							catch (e) {
    598 								lastError = e;
    599 								Zotero.logError(e);
    600 							}
    601 						}
    602 					});
    603 					if (lastError) {
    604 						throw lastError;
    605 					}
    606 				}
    607 				Zotero.debug("Done with reset");
    608 				
    609 				if (!(yield _initDB())) return false;
    610 			}
    611 			catch (e) {
    612 				// Restore from backup?
    613 				alert(e);
    614 				return false;
    615 			}
    616 		}
    617 		
    618 		Zotero.HTTP.triggerProxyAuth();
    619 		
    620 		// Add notifier queue callbacks to the DB layer
    621 		Zotero.DB.addCallback('begin', id => Zotero.Notifier.begin(id));
    622 		Zotero.DB.addCallback('commit', id => Zotero.Notifier.commit(null, id));
    623 		Zotero.DB.addCallback('rollback', id => Zotero.Notifier.reset(id));
    624 		
    625 		try {
    626 			// Require >=2.1b3 database to ensure proper locking
    627 			if (Zotero.isStandalone) {
    628 				let dbSystemVersion = yield Zotero.Schema.getDBVersion('system');
    629 				if (dbSystemVersion > 0 && dbSystemVersion < 31) {
    630 					var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    631 								.createInstance(Components.interfaces.nsIPromptService);
    632 					var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    633 						+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
    634 						+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING)
    635 						+ ps.BUTTON_POS_2_DEFAULT;
    636 					var index = ps.confirmEx(
    637 						null,
    638 						Zotero.getString('dataDir.incompatibleDbVersion.title'),
    639 						Zotero.getString('dataDir.incompatibleDbVersion.text'),
    640 						buttonFlags,
    641 						Zotero.getString('general.useDefault'),
    642 						Zotero.getString('dataDir.chooseNewDataDirectory'),
    643 						Zotero.getString('general.quit'),
    644 						null,
    645 						{}
    646 					);
    647 					
    648 					var quit = false;
    649 					
    650 					// Default location
    651 					if (index == 0) {
    652 						Zotero.Prefs.set("useDataDir", false)
    653 						
    654 						Services.startup.quit(
    655 							Components.interfaces.nsIAppStartup.eAttemptQuit
    656 								| Components.interfaces.nsIAppStartup.eRestart
    657 						);
    658 					}
    659 					// Select new data directory
    660 					else if (index == 1) {
    661 						let dir = yield Zotero.DataDirectory.choose(true);
    662 						if (!dir) {
    663 							quit = true;
    664 						}
    665 					}
    666 					else {
    667 						quit = true;
    668 					}
    669 					
    670 					if (quit) {
    671 						Services.startup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
    672 					}
    673 					
    674 					throw true;
    675 				}
    676 			}
    677 			
    678 			try {
    679 				var updated = yield Zotero.Schema.updateSchema({
    680 					onBeforeUpdate: (options = {}) => {
    681 						if (options.minor) return;
    682 						try {
    683 							Zotero.showZoteroPaneProgressMeter(
    684 								Zotero.getString('upgrade.status')
    685 							)
    686 						}
    687 						catch (e) {
    688 							Zotero.logError(e);
    689 						}
    690 					}
    691 				});
    692 			}
    693 			catch (e) {
    694 				Zotero.logError(e);
    695 				
    696 				if (e instanceof Zotero.DB.IncompatibleVersionException) {
    697 					let kbURL = "https://www.zotero.org/support/kb/newer_db_version";
    698 					let msg = (e.dbClientVersion
    699 						? Zotero.getString('startupError.incompatibleDBVersion',
    700 							[Zotero.clientName, e.dbClientVersion])
    701 						: Zotero.getString('startupError.zoteroVersionIsOlder')) + "\n\n"
    702 						+ Zotero.getString('startupError.zoteroVersionIsOlder.current', Zotero.version)
    703 							+ "\n\n"
    704 						+ Zotero.getString('startupError.zoteroVersionIsOlder.upgrade',
    705 							ZOTERO_CONFIG.DOMAIN_NAME);
    706 					Zotero.startupError = msg;
    707 					_startupErrorHandler = function() {
    708 						var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    709 							.getService(Components.interfaces.nsIPromptService);
    710 						var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    711 							+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    712 							+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING)
    713 							+ ps.BUTTON_POS_0_DEFAULT;
    714 						
    715 						var index = ps.confirmEx(
    716 							null,
    717 							Zotero.getString('general.error'),
    718 							Zotero.startupError,
    719 							buttonFlags,
    720 							Zotero.getString('general.checkForUpdate'),
    721 							null,
    722 							Zotero.getString('general.moreInformation'),
    723 							null,
    724 							{}
    725 						);
    726 						
    727 						// "Check for Update" button
    728 						if(index === 0) {
    729 							if(Zotero.isStandalone) {
    730 								Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
    731 									.getService(Components.interfaces.nsIWindowWatcher)
    732 									.openWindow(null, 'chrome://mozapps/content/update/updates.xul',
    733 										'updateChecker', 'chrome,centerscreen,modal', null);
    734 							} else {
    735 								// In Firefox, show the add-on manager
    736 								Components.utils.import("resource://gre/modules/AddonManager.jsm");
    737 								AddonManager.getAddonByID(ZOTERO_CONFIG['GUID'],
    738 									function (addon) {
    739 										// Disable auto-update so that the user is presented with the option
    740 										var initUpdateState = addon.applyBackgroundUpdates;
    741 										addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
    742 										addon.findUpdates({
    743 												onNoUpdateAvailable: function() {
    744 													ps.alert(
    745 														null,
    746 														Zotero.getString('general.noUpdatesFound'),
    747 														Zotero.getString('general.isUpToDate', 'Zotero')
    748 													);
    749 												},
    750 												onUpdateAvailable: function() {
    751 													// Show available update
    752 													Components.classes["@mozilla.org/appshell/window-mediator;1"]
    753 														.getService(Components.interfaces.nsIWindowMediator)
    754 														.getMostRecentWindow('navigator:browser')
    755 														.BrowserOpenAddonsMgr('addons://updates/available');
    756 												},
    757 												onUpdateFinished: function() {
    758 													// Restore add-on auto-update state, but don't fire
    759 													//  too quickly or the update will not show in the
    760 													//  add-on manager
    761 													setTimeout(function() {
    762 															addon.applyBackgroundUpdates = initUpdateState;
    763 													}, 1000);
    764 												}
    765 											},
    766 											AddonManager.UPDATE_WHEN_USER_REQUESTED
    767 										);
    768 									}
    769 								);
    770 							}
    771 						}
    772 						// Load More Info page
    773 						else if (index == 2) {
    774 							let io = Components.classes['@mozilla.org/network/io-service;1']
    775 								.getService(Components.interfaces.nsIIOService);
    776 							let uri = io.newURI(kbURL, null, null);
    777 							let handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
    778 								.getService(Components.interfaces.nsIExternalProtocolService)
    779 								.getProtocolHandlerInfo('http');
    780 							handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault;
    781 							handler.launchWithURI(uri, null);
    782 						}
    783 					};
    784 					throw e;
    785 				}
    786 				
    787 				let stack = e.stack ? Zotero.Utilities.Internal.filterStack(e.stack) : null;
    788 				Zotero.startupError = Zotero.getString('startupError.databaseUpgradeError')
    789 					+ "\n\n"
    790 					+ (stack || e);
    791 				throw e;
    792 			}
    793 			
    794 			yield Zotero.Users.init();
    795 			yield Zotero.Libraries.init();
    796 			
    797 			yield Zotero.ItemTypes.init();
    798 			yield Zotero.ItemFields.init();
    799 			yield Zotero.CreatorTypes.init();
    800 			yield Zotero.FileTypes.init();
    801 			yield Zotero.CharacterSets.init();
    802 			yield Zotero.RelationPredicates.init();
    803 			
    804 			Zotero.locked = false;
    805 			
    806 			// Initialize various services
    807 			Zotero.Integration.init();
    808 			
    809 			if(Zotero.Prefs.get("httpServer.enabled")) {
    810 				Zotero.Server.init();
    811 			}
    812 			
    813 			yield Zotero.Fulltext.init();
    814 			
    815 			Zotero.Notifier.registerObserver(Zotero.Tags, 'setting', 'tags');
    816 			
    817 			yield Zotero.Sync.Data.Local.init();
    818 			yield Zotero.Sync.Data.Utilities.init();
    819 			Zotero.Sync.Runner = new Zotero.Sync.Runner_Module;
    820 			Zotero.Sync.EventListeners.init();
    821 			Zotero.Streamer = new Zotero.Streamer_Module;
    822 			Zotero.Streamer.init();
    823 			
    824 			Zotero.MIMETypeHandler.init();
    825 			yield Zotero.Proxies.init();
    826 			
    827 			// Initialize keyboard shortcuts
    828 			Zotero.Keys.init();
    829 			
    830 			yield Zotero.Date.init();
    831 			Zotero.LocateManager.init();
    832 			yield Zotero.ID.init();
    833 			yield Zotero.Collections.init();
    834 			yield Zotero.Items.init();
    835 			yield Zotero.Searches.init();
    836 			yield Zotero.Tags.init();
    837 			yield Zotero.Creators.init();
    838 			yield Zotero.Groups.init();
    839 			yield Zotero.Relations.init();
    840 			
    841 			// Load all library data except for items, which are loaded when libraries are first
    842 			// clicked on or if otherwise necessary
    843 			yield Zotero.Promise.each(
    844 				Zotero.Libraries.getAll(),
    845 				library => Zotero.Promise.coroutine(function* () {
    846 					yield Zotero.SyncedSettings.loadAll(library.libraryID);
    847 					if (library.libraryType != 'feed') {
    848 						yield Zotero.Collections.loadAll(library.libraryID);
    849 						yield Zotero.Searches.loadAll(library.libraryID);
    850 					}
    851 				})()
    852 			);
    853 
    854 			
    855 			Zotero.Items.startEmptyTrashTimer();
    856 			
    857 			yield Zotero.QuickCopy.init();
    858 			Zotero.addShutdownListener(() => Zotero.QuickCopy.uninit());
    859 			
    860 			Zotero.Feeds.init();
    861 			Zotero.addShutdownListener(() => Zotero.Feeds.uninit());
    862 			
    863 			Zotero.Schema.schemaUpdatePromise.then(Zotero.purgeDataObjects.bind(Zotero));
    864 			
    865 			return true;
    866 		}
    867 		catch (e) {
    868 			Zotero.logError(e);
    869 			if (!Zotero.startupError) {
    870 				Zotero.startupError = Zotero.getString('startupError') + "\n\n" + (e.stack || e);
    871 			}
    872 			return false;
    873 		}
    874 	});
    875 	
    876 	/**
    877 	 * Initializes the DB connection
    878 	 */
    879 	var _initDB = Zotero.Promise.coroutine(function* (haveReleasedLock) {
    880 		// Initialize main database connection
    881 		Zotero.DB = new Zotero.DBConnection('zotero');
    882 		
    883 		try {
    884 			// Test read access
    885 			yield Zotero.DB.test();
    886 			
    887 			let dbfile = Zotero.DataDirectory.getDatabase();
    888 
    889 			// Tell any other Zotero instances to release their lock,
    890 			// in case we lost the lock on the database (how?) and it's
    891 			// now open in two places at once
    892 			Zotero.IPC.broadcast("releaseLock " + dbfile);
    893 			
    894 			// Test write access on Zotero data directory
    895 			if (!Zotero.File.pathToFile(OS.Path.dirname(dbfile)).isWritable()) {
    896 				var msg = 'Cannot write to ' + OS.Path.dirname(dbfile) + '/';
    897 			}
    898 			// Test write access on Zotero database
    899 			else if (!Zotero.File.pathToFile(dbfile).isWritable()) {
    900 				var msg = 'Cannot write to ' + dbfile;
    901 			}
    902 			else {
    903 				var msg = false;
    904 			}
    905 			
    906 			if (msg) {
    907 				var e = {
    908 					name: 'NS_ERROR_FILE_ACCESS_DENIED',
    909 					message: msg,
    910 					toString: function () { return this.message; }
    911 				};
    912 				throw (e);
    913 			}
    914 		}
    915 		catch (e) {
    916 			if (_checkDataDirAccessError(e)) {}
    917 			// Storage busy
    918 			else if (e.message.includes('2153971713')) {
    919 				Zotero.startupError = Zotero.getString('startupError.databaseInUse') + "\n\n"
    920 					+ Zotero.getString(
    921 						"startupError.close" + (Zotero.isStandalone ? 'Firefox' : 'Standalone')
    922 					);
    923 			}
    924 			else {
    925 				let stack = e.stack ? Zotero.Utilities.Internal.filterStack(e.stack) : null;
    926 				Zotero.startupError = Zotero.getString('startupError') + "\n\n" + (stack || e);
    927 			}
    928 			
    929 			Zotero.debug(e.toString(), 1);
    930 			Components.utils.reportError(e); // DEBUG: doesn't always work
    931 			Zotero.skipLoading = true;
    932 			return false;
    933 		}
    934 		
    935 		return true;
    936 	});
    937 	
    938 	
    939 	function _checkDataDirAccessError(e) {
    940 		if (e.name != 'NS_ERROR_FILE_ACCESS_DENIED' && !e.message.includes('2152857621')) {
    941 			return false;
    942 		}
    943 		
    944 		var msg = Zotero.getString('dataDir.databaseCannotBeOpened', Zotero.clientName)
    945 			+ "\n\n"
    946 			+ Zotero.getString('dataDir.checkPermissions', Zotero.clientName);
    947 		// If already using default directory, just show it
    948 		if (Zotero.DataDirectory.dir == Zotero.DataDirectory.defaultDir) {
    949 			msg += "\n\n" + Zotero.getString('dataDir.location', Zotero.DataDirectory.dir);
    950 		}
    951 		// Otherwise suggest moving to default, since there's a good chance this is due to security
    952 		// software preventing Zotero from accessing the selected directory (particularly if it's
    953 		// a Firefox profile)
    954 		else {
    955 			msg += "\n\n"
    956 				+ Zotero.getString('dataDir.moveToDefaultLocation', Zotero.clientName)
    957 				+ "\n\n"
    958 				+ Zotero.getString(
    959 					'dataDir.migration.failure.full.current', Zotero.DataDirectory.dir
    960 				)
    961 				+ "\n"
    962 				+ Zotero.getString(
    963 					'dataDir.migration.failure.full.recommended', Zotero.DataDirectory.defaultDir
    964 				);
    965 		}
    966 		Zotero.startupError = msg;
    967 		return true;
    968 	}
    969 	
    970 	
    971 	/**
    972 	 * Called when the DB has been released by another Zotero process to perform necessary 
    973 	 * initialization steps
    974 	 */
    975 	this.onDBLockReleased = function() {
    976 		if(Zotero.isConnector) {
    977 			// if DB lock is released, switch out of connector mode
    978 			switchConnectorMode(false);
    979 		} else if(_waitingForDBLock) {
    980 			// if waiting for DB lock and we get it, continue init
    981 			_waitingForDBLock = false;
    982 		}
    983 	}
    984 	
    985 	this.shutdown = Zotero.Promise.coroutine(function* () {
    986 		Zotero.debug("Shutting down Zotero");
    987 		
    988 		try {
    989 			// set closing to true
    990 			Zotero.closing = true;
    991 			
    992 			// run shutdown listener
    993 			for (let listener of _shutdownListeners) {
    994 				try {
    995 					listener();
    996 				} catch(e) {
    997 					Zotero.logError(e);
    998 				}
    999 			}
   1000 			
   1001 			// remove temp directory
   1002 			Zotero.removeTempDirectory();
   1003 			
   1004 			if (Zotero.DB) {
   1005 				// close DB
   1006 				yield Zotero.DB.closeDatabase(true)
   1007 				
   1008 				if (!Zotero.restarting) {
   1009 					// broadcast that DB lock has been released
   1010 					Zotero.IPC.broadcast("lockReleased");
   1011 				}
   1012 			}
   1013 		} catch(e) {
   1014 			Zotero.logError(e);
   1015 			throw e;
   1016 		}
   1017 	});
   1018 	
   1019 	
   1020 	this.getProfileDirectory = function () {
   1021 		Zotero.warn("Zotero.getProfileDirectory() is deprecated -- use Zotero.Profile.dir");
   1022 		return Zotero.File.pathToFile(Zotero.Profile.dir);
   1023 	}
   1024 	
   1025 	
   1026 	this.getZoteroDirectory = function () {
   1027 		Zotero.warn("Zotero.getZoteroDirectory() is deprecated -- use Zotero.DataDirectory.dir");
   1028 		return Zotero.File.pathToFile(Zotero.DataDirectory.dir);
   1029 	}
   1030 	
   1031 	
   1032 	function getStorageDirectory(){
   1033 		var file = OS.Path.join(Zotero.DataDirectory.dir, 'storage');
   1034 		file = Zotero.File.pathToFile(file);
   1035 		Zotero.File.createDirectoryIfMissing(file);
   1036 		return file;
   1037 	}
   1038 	
   1039 	
   1040 	this.getZoteroDatabase = function (name, ext) {
   1041 		Zotero.warn("Zotero.getZoteroDatabase() is deprecated -- use Zotero.DataDirectory.getDatabase()");
   1042 		return Zotero.File.pathToFile(Zotero.DataDirectory.getDatabase(name, ext));
   1043 	}
   1044 	
   1045 	
   1046 	/**
   1047 	 * @return	{nsIFile}
   1048 	 */
   1049 	this.getTempDirectory = function () {
   1050 		var tmp = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
   1051 		tmp.append('tmp');
   1052 		Zotero.File.createDirectoryIfMissing(tmp);
   1053 		return tmp;
   1054 	}
   1055 	
   1056 	
   1057 	this.removeTempDirectory = function () {
   1058 		var tmp = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
   1059 		tmp.append('tmp');
   1060 		if (tmp.exists()) {
   1061 			try {
   1062 				tmp.remove(true);
   1063 			}
   1064 			catch (e) {}
   1065 		}
   1066 	}
   1067 	
   1068 	
   1069 	this.getStylesDirectory = function () {
   1070 		var dir = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
   1071 		dir.append('styles');
   1072 		Zotero.File.createDirectoryIfMissing(dir);
   1073 		return dir;
   1074 	}
   1075 	
   1076 	
   1077 	this.getTranslatorsDirectory = function () {
   1078 		var dir = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
   1079 		dir.append('translators');
   1080 		Zotero.File.createDirectoryIfMissing(dir);
   1081 		return dir;
   1082 	}
   1083 	
   1084 	
   1085 	this.openMainWindow = function () {
   1086 		var prefService = Components.classes["@mozilla.org/preferences-service;1"]
   1087 			.getService(Components.interfaces.nsIPrefBranch);
   1088 		var chromeURI = prefService.getCharPref('toolkit.defaultChromeURI');
   1089 		var flags = prefService.getCharPref("toolkit.defaultChromeFeatures", "chrome,dialog=no,all");
   1090 		var ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1']
   1091 			.getService(Components.interfaces.nsIWindowWatcher);
   1092 		return ww.openWindow(null, chromeURI, '_blank', flags, null);
   1093 	}
   1094 	
   1095 	
   1096 	/**
   1097 	 * Launch a file, the best way we can
   1098 	 */
   1099 	this.launchFile = function (file) {
   1100 		file = Zotero.File.pathToFile(file);
   1101 		try {
   1102 			Zotero.debug("Launching " + file.path);
   1103 			file.launch();
   1104 		}
   1105 		catch (e) {
   1106 			Zotero.debug(e, 2);
   1107 			Zotero.debug("launch() not supported -- trying fallback executable", 2);
   1108 			
   1109 			try {
   1110 				if (Zotero.isWin) {
   1111 					var pref = "fallbackLauncher.windows";
   1112 				}
   1113 				else {
   1114 					var pref = "fallbackLauncher.unix";
   1115 				}
   1116 				let launcher = Zotero.Prefs.get(pref);
   1117 				this.launchFileWithApplication(file.path, launcher);
   1118 			}
   1119 			catch (e) {
   1120 				Zotero.debug(e);
   1121 				Zotero.debug("Launching via executable failed -- passing to loadUrl()");
   1122 				
   1123 				// If nsILocalFile.launch() isn't available and the fallback
   1124 				// executable doesn't exist, we just let the Firefox external
   1125 				// helper app window handle it
   1126 				var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
   1127 								.getService(Components.interfaces.nsIFileProtocolHandler);
   1128 				var uri = nsIFPH.newFileURI(file);
   1129 				
   1130 				var nsIEPS = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].
   1131 								getService(Components.interfaces.nsIExternalProtocolService);
   1132 				nsIEPS.loadUrl(uri);
   1133 			}
   1134 		}
   1135 	};
   1136 	
   1137 	
   1138 	/**
   1139 	 * Launch a file with the given application
   1140 	 */
   1141 	this.launchFileWithApplication = function (filePath, applicationPath) {
   1142 		var exec = Zotero.File.pathToFile(applicationPath);
   1143 		if (!exec.exists()) {
   1144 			throw new Error("'" + applicationPath + "' does not exist");
   1145 		}
   1146 		
   1147 		var args;
   1148 		// On macOS, if we only have an .app, launch it using 'open'
   1149 		if (Zotero.isMac && applicationPath.endsWith('.app')) {
   1150 			args = [filePath, '-a', applicationPath];
   1151 			applicationPath = '/usr/bin/open';
   1152 		}
   1153 		else {
   1154 			args = [filePath];
   1155 		}
   1156 		
   1157 		// Async, but we don't want to block
   1158 		Zotero.Utilities.Internal.exec(applicationPath, args);
   1159 	};
   1160 	
   1161 	
   1162 	/**
   1163 	 * Launch an HTTP URL externally, the best way we can
   1164 	 *
   1165 	 * Used only by Standalone
   1166 	 */
   1167 	this.launchURL = function (url) {
   1168 		if (!url.match(/^https?/)) {
   1169 			throw new Error("launchURL() requires an HTTP(S) URL");
   1170 		}
   1171 		
   1172 		try {
   1173 			var io = Components.classes['@mozilla.org/network/io-service;1']
   1174 						.getService(Components.interfaces.nsIIOService);
   1175 			var uri = io.newURI(url, null, null);
   1176 			var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
   1177 							.getService(Components.interfaces.nsIExternalProtocolService)
   1178 							.getProtocolHandlerInfo('http');
   1179 			handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault;
   1180 			handler.launchWithURI(uri, null);
   1181 		}
   1182 		catch (e) {
   1183 			Zotero.debug("launchWithURI() not supported -- trying fallback executable");
   1184 			
   1185 			if (Zotero.isWin) {
   1186 				var pref = "fallbackLauncher.windows";
   1187 			}
   1188 			else {
   1189 				var pref = "fallbackLauncher.unix";
   1190 			}
   1191 			var path = Zotero.Prefs.get(pref);
   1192 			
   1193 			var exec = Components.classes["@mozilla.org/file/local;1"]
   1194 						.createInstance(Components.interfaces.nsILocalFile);
   1195 			exec.initWithPath(path);
   1196 			if (!exec.exists()) {
   1197 				throw ("Fallback executable not found -- check extensions.zotero." + pref + " in about:config");
   1198 			}
   1199 			
   1200 			var proc = Components.classes["@mozilla.org/process/util;1"]
   1201 							.createInstance(Components.interfaces.nsIProcess);
   1202 			proc.init(exec);
   1203 			
   1204 			var args = [url];
   1205 			proc.runw(false, args, args.length);
   1206 		}
   1207 	}
   1208 	
   1209 	
   1210 	/**
   1211 	 * Opens a URL in the basic viewer, and optionally run a callback on load
   1212 	 *
   1213 	 * @param {String} uri
   1214 	 * @param {Function} [onLoad] - Function to run once URI is loaded; passed the loaded document
   1215 	 */
   1216 	this.openInViewer = function (uri, onLoad) {
   1217 		var wm = Services.wm;
   1218 		var win = wm.getMostRecentWindow("zotero:basicViewer");
   1219 		if (win) {
   1220 			win.loadURI(uri);
   1221 		} else {
   1222 			let ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1']
   1223 				.getService(Components.interfaces.nsIWindowWatcher);
   1224 			let arg = Components.classes["@mozilla.org/supports-string;1"]
   1225 				.createInstance(Components.interfaces.nsISupportsString);
   1226 			arg.data = uri;
   1227 			win = ww.openWindow(null, "chrome://zotero/content/standalone/basicViewer.xul",
   1228 				"basicViewer", "chrome,dialog=yes,resizable,centerscreen,menubar,scrollbars", arg);
   1229 		}
   1230 		if (onLoad) {
   1231 			let browser
   1232 			let func = function () {
   1233 				win.removeEventListener("load", func);
   1234 				browser = win.document.documentElement.getElementsByTagName('browser')[0];
   1235 				browser.addEventListener("pageshow", innerFunc);
   1236 			};
   1237 			let innerFunc = function () {
   1238 				browser.removeEventListener("pageshow", innerFunc);
   1239 				onLoad(browser.contentDocument);
   1240 			};
   1241 			win.addEventListener("load", func);
   1242 		}
   1243 	};
   1244 	
   1245 	
   1246 	/*
   1247 	 * Debug logging function
   1248 	 *
   1249 	 * Uses prefs e.z.debug.log and e.z.debug.level (restart required)
   1250 	 *
   1251 	 * @param {} message
   1252 	 * @param {Integer} [level=3]
   1253 	 * @param {Integer} [maxDepth]
   1254 	 * @param {Boolean|Integer} [stack] Whether to display the calling stack.
   1255 	 *   If true, stack is displayed starting from the caller. If an integer,
   1256 	 *   that many stack levels will be omitted starting from the caller.
   1257 	 */
   1258 	function debug(message, level, maxDepth, stack) {
   1259 		// Account for this alias
   1260 		if (stack === true) {
   1261 			stack = 1;
   1262 		} else if (stack >= 0) {
   1263 			stack++;
   1264 		}
   1265 		
   1266 		Zotero.Debug.log(message, level, maxDepth, stack);
   1267 	}
   1268 	
   1269 	
   1270 	/*
   1271 	 * Log a message to the Mozilla JS error console
   1272 	 *
   1273 	 * |type| is a string with one of the flag types in nsIScriptError:
   1274 	 *    'error', 'warning', 'exception', 'strict'
   1275 	 */
   1276 	function log(message, type, sourceName, sourceLine, lineNumber, columnNumber) {
   1277 		var scriptError = Components.classes["@mozilla.org/scripterror;1"]
   1278 			.createInstance(Components.interfaces.nsIScriptError);
   1279 		
   1280 		if (!type) {
   1281 			type = 'warning';
   1282 		}
   1283 		var flags = scriptError[type + 'Flag'];
   1284 		
   1285 		scriptError.init(
   1286 			message,
   1287 			sourceName ? sourceName : null,
   1288 			sourceLine != undefined ? sourceLine : null,
   1289 			lineNumber != undefined ? lineNumber : null, 
   1290 			columnNumber != undefined ? columnNumber : null,
   1291 			flags,
   1292 			'component javascript'
   1293 		);
   1294 		Services.console.logMessage(scriptError);
   1295 	}
   1296 	
   1297 	/**
   1298 	 * Log a JS error to the Mozilla error console and debug output
   1299 	 * @param {Exception} err
   1300 	 */
   1301 	function logError(err) {
   1302 		Zotero.debug(err, 1);
   1303 		log(err.message ? err.message : err.toString(), "error",
   1304 			err.fileName ? err.fileName : (err.filename ? err.filename : null), null,
   1305 			err.lineNumber ? err.lineNumber : null, null);
   1306 	}
   1307 	
   1308 	
   1309 	this.warn = function (err) {
   1310 		Zotero.debug(err, 2);
   1311 		log(err.message ? err.message : err.toString(), "warning",
   1312 			err.fileName ? err.fileName : (err.filename ? err.filename : null), null,
   1313 			err.lineNumber ? err.lineNumber : null, null);
   1314 	}
   1315 	
   1316 	
   1317 	/**
   1318 	 * Display an alert in a given window
   1319 	 *
   1320 	 * @param {Window}
   1321 	 * @param {String} title
   1322 	 * @param {String} msg
   1323 	 */
   1324 	this.alert = function (window, title, msg) {
   1325 		this.debug(`Alert:\n\n${msg}`);
   1326 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   1327 			.getService(Components.interfaces.nsIPromptService);
   1328 		ps.alert(window, title, msg);
   1329 	}
   1330 	
   1331 	
   1332 	this.getErrors = function (asStrings) {
   1333 		var errors = [];
   1334 		
   1335 		for (let msg of _startupErrors.concat(_recentErrors)) {
   1336 			let altMessage;
   1337 			// Remove password in malformed XML errors
   1338 			if (msg.category == 'malformed-xml') {
   1339 				try {
   1340 					// msg.message is read-only, so store separately
   1341 					altMessage = msg.message.replace(/(https?:\/\/[^:]+:)([^@]+)(@[^"]+)/, "$1****$3");
   1342 				}
   1343 				catch (e) {}
   1344 			}
   1345 			
   1346 			if (asStrings) {
   1347 				errors.push(altMessage || msg.message)
   1348 			}
   1349 			else {
   1350 				errors.push(msg);
   1351 			}
   1352 		}
   1353 		return errors;
   1354 	}
   1355 	
   1356 	
   1357 	/**
   1358 	 * Get versions, platform, etc.
   1359 	 */
   1360 	this.getSystemInfo = Zotero.Promise.coroutine(function* () {
   1361 		var info = {
   1362 			version: Zotero.version,
   1363 			platform: Zotero.platform,
   1364 			oscpu: Zotero.oscpu,
   1365 			locale: Zotero.locale,
   1366 			appName: Services.appinfo.name,
   1367 			appVersion: Services.appinfo.version
   1368 		};
   1369 		
   1370 		var extensions = yield Zotero.getInstalledExtensions();
   1371 		info.extensions = extensions.join(', ');
   1372 		
   1373 		var str = '';
   1374 		for (var key in info) {
   1375 			str += key + ' => ' + info[key] + ', ';
   1376 		}
   1377 		str = str.substr(0, str.length - 2);
   1378 		return str;
   1379 	});
   1380 	
   1381 	
   1382 	/**
   1383 	 * @return {Promise<String[]>} - Promise for an array of extension names and versions
   1384 	 */
   1385 	this.getInstalledExtensions = Zotero.Promise.method(function () {
   1386 		var deferred = Zotero.Promise.defer();
   1387 		function onHaveInstalledAddons(installed) {
   1388 			installed.sort(function(a, b) {
   1389 				return ((a.appDisabled || a.userDisabled) ? 1 : 0) -
   1390 					((b.appDisabled || b.userDisabled) ? 1 : 0);
   1391 			});
   1392 			var addons = [];
   1393 			for (let addon of installed) {
   1394 				switch (addon.id) {
   1395 					case "zotero@chnm.gmu.edu":
   1396 					case "{972ce4c6-7e08-4474-a285-3208198ce6fd}": // Default theme
   1397 						continue;
   1398 				}
   1399 				
   1400 				addons.push(addon.name + " (" + addon.version
   1401 					+ (addon.type != 2 ? ", " + addon.type : "")
   1402 					+ ((addon.appDisabled || addon.userDisabled) ? ", disabled" : "")
   1403 					+ ")");
   1404 			}
   1405 			deferred.resolve(addons);
   1406 		}
   1407 		
   1408 		Components.utils.import("resource://gre/modules/AddonManager.jsm");
   1409 		AddonManager.getAllAddons(onHaveInstalledAddons);
   1410 		return deferred.promise;
   1411 	});
   1412 	
   1413 	/**
   1414 	 * @param {String} name
   1415 	 * @param {String[]} [params=[]] - Strings to substitute for placeholders
   1416 	 * @param {Number} [num] - Number (also appearing in `params`) to use when determining which plural
   1417 	 *     form of the string to use; localized strings should include all forms in the order specified
   1418 	 *     in https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals,
   1419 	 *     separated by semicolons
   1420 	 */
   1421 	this.getString = function (name, params, num) {
   1422 		return this.getStringFromBundle(_localizedStringBundle, ...arguments);
   1423 	}
   1424 	
   1425 	
   1426 	this.getStringFromBundle = function (bundle, name, params, num) {
   1427 		try {
   1428 			if (params != undefined) {
   1429 				if (typeof params != 'object'){
   1430 					params = [params];
   1431 				}
   1432 				var l10n = bundle.formatStringFromName(name, params, params.length);
   1433 			}
   1434 			else {
   1435 				var l10n = bundle.GetStringFromName(name);
   1436 			}
   1437 			if (num !== undefined) {
   1438 				let availableForms = l10n.split(/;/);
   1439 				// If not enough available forms, use last one -- PluralForm.get() uses first by
   1440 				// default, but it's more likely that a localizer will translate the two English
   1441 				// strings with some plural form as the second one, so we might as well use that
   1442 				if (availableForms.length < this.pluralFormNumForms()) {
   1443 					l10n = availableForms[availableForms.length - 1];
   1444 				}
   1445 				else {
   1446 					l10n = this.pluralFormGet(num, l10n);
   1447 				}
   1448 			}
   1449 		}
   1450 		catch (e){
   1451 			if (e.name == 'NS_ERROR_ILLEGAL_VALUE') {
   1452 				Zotero.debug(params, 1);
   1453 			}
   1454 			else if (e.name != 'NS_ERROR_FAILURE') {
   1455 				Zotero.logError(e);
   1456 			}
   1457 			throw new Error('Localized string not available for ' + name);
   1458 		}
   1459 		return l10n;
   1460 	}
   1461 	
   1462 	
   1463 	/**
   1464 	 * Defines property on the object
   1465 	 * More compact way to do Object.defineProperty
   1466 	 *
   1467 	 * @param {Object} obj Target object
   1468 	 * @param {String} prop Property to be defined
   1469 	 * @param {Object} desc Propery descriptor. If not overriden, "enumerable" is true
   1470 	 * @param {Object} opts Options:
   1471 	 *   lazy {Boolean} If true, the _getter_ is intended for late
   1472 	 *     initialization of the property. The getter is replaced with a simple
   1473 	 *     property once initialized.
   1474 	 */
   1475 	this.defineProperty = function(obj, prop, desc, opts) {
   1476 		if (typeof prop != 'string') throw new Error("Property must be a string");
   1477 		var d = { __proto__: null, enumerable: true, configurable: true }; // Enumerable by default
   1478 		for (let p in desc) {
   1479 			if (!desc.hasOwnProperty(p)) continue;
   1480 			d[p] = desc[p];
   1481 		}
   1482 		
   1483 		if (opts) {
   1484 			if (opts.lazy && d.get) {
   1485 				let getter = d.get;
   1486 				d.configurable = true; // Make sure we can change the property later
   1487 				d.get = function() {
   1488 					let val = getter.call(this);
   1489 					
   1490 					// Redefine getter on this object as non-writable value
   1491 					delete d.set;
   1492 					delete d.get;
   1493 					d.writable = false;
   1494 					d.value = val;
   1495 					Object.defineProperty(this, prop, d);
   1496 					
   1497 					return val;
   1498 				}
   1499 			}
   1500 		}
   1501 		
   1502 		Object.defineProperty(obj, prop, d);
   1503 	}
   1504 	
   1505 	this.extendClass = function(superClass, newClass) {
   1506 		newClass._super = superClass;
   1507 		newClass.prototype = Object.create(superClass.prototype);
   1508 		newClass.prototype.constructor = newClass;
   1509 	}
   1510 	
   1511 	
   1512 	/*
   1513 	 * This function should be removed
   1514 	 *
   1515 	 * |separator| defaults to a space (not a comma like Array.join()) if
   1516 	 *   not specified
   1517 	 *
   1518 	 * TODO: Substitute localized characters (e.g. Arabic comma and semicolon)
   1519 	 */
   1520 	function localeJoin(arr, separator) {
   1521 		if (typeof separator == 'undefined') {
   1522 			separator = ' ';
   1523 		}
   1524 		return arr.join(separator);
   1525 	}
   1526 	
   1527 	
   1528 	this.getLocaleCollation = function () {
   1529 		if (this.collation) {
   1530 			return this.collation;
   1531 		}
   1532 		
   1533 		try {
   1534 			// DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're
   1535 			// passing manually?
   1536 			
   1537 			let locales;
   1538 			// Fx55+
   1539 			if (Services.locale.getAppLocalesAsBCP47) {
   1540 				locales = Services.locale.getAppLocalesAsBCP47();
   1541 			}
   1542 			else {
   1543 				let locale;
   1544 				// Fx54
   1545 				if (Services.locale.getAppLocale) {
   1546 					locale = Services.locale.getAppLocale();
   1547 				}
   1548 				// Fx <=53
   1549 				else {
   1550 					locale = Services.locale.getApplicationLocale();
   1551 					locale = locale.getCategory('NSILOCALE_COLLATE');
   1552 				}
   1553 				
   1554 				// Extract a valid language tag
   1555 				try {
   1556 					locale = locale.match(/^[a-z]{2}(\-[A-Z]{2})?/)[0];
   1557 				}
   1558 				catch (e) {
   1559 					throw new Error(`Error parsing locale ${locale}`);
   1560 				}
   1561 				locales = [locale];
   1562 			}
   1563 			
   1564 			var collator = new Intl.Collator(locales, {
   1565 				numeric: true,
   1566 				sensitivity: 'base'
   1567 			});
   1568 		}
   1569 		catch (e) {
   1570 			Zotero.logError(e);
   1571 			
   1572 			// Fall back to en-US sorting
   1573 			try {
   1574 				Zotero.logError("Falling back to en-US sorting");
   1575 				collator = new Intl.Collator(['en-US'], {
   1576 					numeric: true,
   1577 					sensitivity: 'base'
   1578 				});
   1579 			}
   1580 			catch (e) {
   1581 				Zotero.logError(e);
   1582 				
   1583 				// If there's still an error, just skip sorting
   1584 				collator = {
   1585 					compare: function (a, b) {
   1586 						return 0;
   1587 					}
   1588 				};
   1589 			}
   1590 		}
   1591 		
   1592 		// Grab all ASCII punctuation and space at the begining of string
   1593 		var initPunctuationRE = /^[\x20-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/;
   1594 		// Punctuation that should be ignored when sorting
   1595 		var ignoreInitRE = /["'[{(]+$/;
   1596 		
   1597 		// Until old code is updated, pretend we're returning an nsICollation
   1598 		return this.collation = {
   1599 			compareString: function (_, a, b) {
   1600 				if (!a && !b) return 0;
   1601 				if (!a || !b) return b ? -1 : 1;
   1602 				
   1603 				// Compare initial punctuation
   1604 				var aInitP = initPunctuationRE.exec(a) || '';
   1605 				var bInitP = initPunctuationRE.exec(b) || '';
   1606 				
   1607 				var aWordStart = 0, bWordStart = 0;
   1608 				if (aInitP) {
   1609 					aWordStart = aInitP[0].length;
   1610 					aInitP = aInitP[0].replace(ignoreInitRE, '');
   1611 				}
   1612 				if (bInitP) {
   1613 					bWordStart = bInitP.length;
   1614 					bInitP = bInitP[0].replace(ignoreInitRE, '');
   1615 				}
   1616 				
   1617 				// If initial punctuation is equivalent, use collator comparison
   1618 				// that ignores all punctuation
   1619 				//
   1620 				// Update: Intl.Collator's ignorePunctuation also ignores whitespace, so we're
   1621 				// no longer using it, meaning we could take out most of the code to handle
   1622 				// initial punctuation separately, unless we think we'll at some point switch to
   1623 				// a collation function that ignores punctuation but not whitespace.
   1624 				if (aInitP == bInitP || !aInitP && !bInitP) return collator.compare(a, b);
   1625 				
   1626 				// Otherwise consider "attached" words as well, e.g. the order should be
   1627 				// "__ n", "__z", "_a"
   1628 				// We don't actually care what the attached word is, just whether it's
   1629 				// there, since at this point we're guaranteed to have non-equivalent
   1630 				// initial punctuation
   1631 				if (aWordStart < a.length) aInitP += 'a';
   1632 				if (bWordStart < b.length) bInitP += 'a';
   1633 				
   1634 				return aInitP.localeCompare(bInitP);
   1635 			}
   1636 		};
   1637 	}
   1638 	
   1639 	this.defineProperty(this, "localeCompare", {
   1640 		get: function() {
   1641 			var collation = this.getLocaleCollation();
   1642 			return collation.compareString.bind(collation, 1);
   1643 		}
   1644 	}, {lazy: true});
   1645 	
   1646 	/*
   1647 	 * Sets font size based on prefs -- intended for use on root element
   1648 	 *  (zotero-pane, note window, etc.)
   1649 	 */
   1650 	function setFontSize(rootElement) {
   1651 		var size = Zotero.Prefs.get('fontSize');
   1652 		rootElement.style.fontSize = size + 'em';
   1653 		if (size <= 1) {
   1654 			size = 'small';
   1655 		}
   1656 		else if (size <= 1.25) {
   1657 			size = 'medium';
   1658 		}
   1659 		else {
   1660 			size = 'large';
   1661 		}
   1662 		// Custom attribute -- allows for additional customizations in zotero.css
   1663 		rootElement.setAttribute('zoteroFontSize', size);
   1664 	}
   1665 	
   1666 	
   1667 	/*
   1668 	 * Flattens mixed arrays/values in a passed _arguments_ object and returns
   1669 	 * an array of values -- allows for functions to accept both arrays of
   1670 	 * values and/or an arbitrary number of individual values
   1671 	 */
   1672 	function flattenArguments(args){
   1673 		// Put passed scalar values into an array
   1674 		if (args === null || typeof args == 'string' || typeof args.length == 'undefined') {
   1675 			args = [args];
   1676 		}
   1677 		
   1678 		var returns = [];
   1679 		for (var i=0; i<args.length; i++){
   1680 			var arg = args[i];
   1681 			if (!arg && arg !== 0) {
   1682 				continue;
   1683 			}
   1684 			if (Array.isArray(arg)) {
   1685 				returns.push(...arg);
   1686 			}
   1687 			else {
   1688 				returns.push(arg);
   1689 			}
   1690 		}
   1691 		return returns;
   1692 	}
   1693 	
   1694 	
   1695 	function getAncestorByTagName(elem, tagName){
   1696 		while (elem.parentNode){
   1697 			elem = elem.parentNode;
   1698 			if (elem.localName == tagName) {
   1699 				return elem;
   1700 			}
   1701 		}
   1702 		return false;
   1703 	}
   1704 	
   1705 	
   1706 	/**
   1707 	* Generate a random string of length 'len' (defaults to 8)
   1708 	**/
   1709 	function randomString(len, chars) {
   1710 		return Zotero.Utilities.randomString(len, chars);
   1711 	}
   1712 	
   1713 	
   1714 	function moveToUnique(file, newFile){
   1715 		newFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o644);
   1716 		var newName = newFile.leafName;
   1717 		newFile.remove(null);
   1718 		
   1719 		// Move file to unique name
   1720 		file.moveTo(newFile.parent, newName);
   1721 		return file;
   1722 	}
   1723 	
   1724 	
   1725 	/**
   1726 	 * Generate a function that produces a static output
   1727 	 *
   1728 	 * Zotero.lazy(fn) returns a function. The first time this function
   1729 	 * is called, it calls fn() and returns its output. Subsequent
   1730 	 * calls return the same output as the first without calling fn()
   1731 	 * again.
   1732 	 */
   1733 	this.lazy = function(fn) {
   1734 		var x, called = false;
   1735 		return function() {
   1736 			if(!called) {
   1737 				x = fn.apply(this);
   1738 				called = true;
   1739 			}
   1740 			return x;
   1741 		};
   1742 	};
   1743 	
   1744 	
   1745 	this.serial = function (fn) {
   1746 		Components.utils.import("resource://zotero/concurrentCaller.js");
   1747 		var caller = new ConcurrentCaller({
   1748 			numConcurrent: 1,
   1749 			onError: e => Zotero.logError(e)
   1750 		});
   1751 		return function () {
   1752 			var args = arguments;
   1753 			return caller.start(function () {
   1754 				return fn.apply(this, args);
   1755 			}.bind(this));
   1756 		};
   1757 	}
   1758 	
   1759 	
   1760 	this.spawn = function (generator, thisObject) {
   1761 		if (thisObject) {
   1762 			return Zotero.Promise.coroutine(generator.bind(thisObject))();
   1763 		}
   1764 		return Zotero.Promise.coroutine(generator)();
   1765 	}
   1766 	
   1767 	
   1768 	/**
   1769 	 * Emulates the behavior of window.setTimeout
   1770 	 *
   1771 	 * @param {Function} func			The function to be called
   1772 	 * @param {Integer} ms				The number of milliseconds to wait before calling func
   1773 	 * @return {Integer} - ID of timer to be passed to clearTimeout()
   1774 	 */
   1775 	var _lastTimeoutID = 0;
   1776 	this.setTimeout = function (func, ms) {
   1777 		var id = ++_lastTimeoutID;
   1778 		
   1779 		var timer = Components.classes["@mozilla.org/timer;1"]
   1780 			.createInstance(Components.interfaces.nsITimer);
   1781 		var timerCallback = {
   1782 			"notify": function () {
   1783 				func();
   1784 				_runningTimers.delete(id);
   1785 			}
   1786 		};
   1787 		timer.initWithCallback(timerCallback, ms, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
   1788 		_runningTimers.set(id, timer);
   1789 		return id;
   1790 	};
   1791 	
   1792 	
   1793 	this.clearTimeout = function (id) {
   1794 		var timer = _runningTimers.get(id);
   1795 		if (timer) {
   1796 			timer.cancel();
   1797 			_runningTimers.delete(id);
   1798 		}
   1799 	};
   1800 	
   1801 	
   1802 	/**
   1803 	 * Show Zotero pane overlay and progress bar in all windows
   1804 	 *
   1805 	 * @param {String} msg
   1806 	 * @param {Boolean} [determinate=false]
   1807 	 * @param {Boolean} [modalOnly=false] - Don't use popup if Zotero pane isn't showing
   1808 	 * @return	void
   1809 	 */
   1810 	this.showZoteroPaneProgressMeter = function (msg, determinate, icon, modalOnly) {
   1811 		// If msg is undefined, keep any existing message. If false/null/"", clear.
   1812 		// The message is also cleared when the meters are hidden.
   1813 		_progressMessage = msg = (msg === undefined ? _progressMessage : msg) || "";
   1814 		var currentWindow = Services.wm.getMostRecentWindow("navigator:browser");
   1815 		var enumerator = Services.wm.getEnumerator("navigator:browser");
   1816 		var progressMeters = [];
   1817 		while (enumerator.hasMoreElements()) {
   1818 			var win = enumerator.getNext();
   1819 			if(!win.ZoteroPane) continue;
   1820 			if (!win.ZoteroPane.isShowing() && !modalOnly) {
   1821 				if (win != currentWindow) {
   1822 					continue;
   1823 				}
   1824 				
   1825 				// If Zotero is closed in the top-most window, show a popup instead
   1826 				_progressPopup = new Zotero.ProgressWindow();
   1827 				_progressPopup.changeHeadline("Zotero");
   1828 				if (icon) {
   1829 					_progressPopup.addLines([msg], [icon]);
   1830 				}
   1831 				else {
   1832 					_progressPopup.addDescription(msg);
   1833 				}
   1834 				_progressPopup.show();
   1835 				continue;
   1836 			}
   1837 			
   1838 			var label = win.ZoteroPane.document.getElementById('zotero-pane-progress-label');
   1839 			if (!label) {
   1840 				Components.utils.reportError("label not found in " + win.document.location.href);
   1841 			}
   1842 			if (msg) {
   1843 				label.hidden = false;
   1844 				label.value = msg;
   1845 			}
   1846 			else {
   1847 				label.hidden = true;
   1848 			}
   1849 			// This is the craziest thing. In Firefox 52.6.0, the very presence of this line
   1850 			// causes Zotero on Linux to burn 5% CPU at idle, even if everything below it in
   1851 			// the block is commented out. Same if the progressmeter itself is hidden="true".
   1852 			// For some reason it also doesn't seem to work to set the progressmeter to
   1853 			// 'determined' when hiding, which we're doing in lookup.js. So instead, create a new
   1854 			// progressmeter each time and delete it in _hideWindowZoteroPaneOverlay().
   1855 			//
   1856 			//let progressMeter = win.ZoteroPane.document.getElementById('zotero-pane-progressmeter');
   1857 			let doc = win.ZoteroPane.document;
   1858 			let container = doc.getElementById('zotero-pane-progressmeter-container');
   1859 			let progressMeter = doc.createElement('progressmeter');
   1860 			progressMeter.id = 'zotero-pane-progressmeter';
   1861 			progressMeter.setAttribute('mode', 'undetermined');
   1862 			if (determinate) {
   1863 				progressMeter.mode = 'determined';
   1864 				progressMeter.value = 0;
   1865 				progressMeter.max = 1000;
   1866 			}
   1867 			else {
   1868 				progressMeter.mode = 'undetermined';
   1869 			}
   1870 			container.appendChild(progressMeter);
   1871 			
   1872 			_showWindowZoteroPaneOverlay(win.ZoteroPane.document);
   1873 			win.ZoteroPane.document.getElementById('zotero-pane-overlay-deck').selectedIndex = 0;
   1874 			
   1875 			progressMeters.push(progressMeter);
   1876 		}
   1877 		this.locked = true;
   1878 		_progressMeters = progressMeters;
   1879 	}
   1880 	
   1881 	
   1882 	/**
   1883 	 * @param	{Number}	percentage		Percentage complete as integer or float
   1884 	 */
   1885 	this.updateZoteroPaneProgressMeter = function (percentage) {
   1886 		if(percentage !== null) {
   1887 			if (percentage < 0 || percentage > 100) {
   1888 				Zotero.debug("Invalid percentage value '" + percentage + "' in Zotero.updateZoteroPaneProgressMeter()");
   1889 				return;
   1890 			}
   1891 			percentage = Math.round(percentage * 10);
   1892 		}
   1893 		if (percentage === _lastPercentage) {
   1894 			return;
   1895 		}
   1896 		for (let pm of _progressMeters) {
   1897 			if (percentage !== null) {
   1898 				if (pm.mode == 'undetermined') {
   1899 					pm.max = 1000;
   1900 					pm.mode = 'determined';
   1901 				}
   1902 				pm.value = percentage;
   1903 			} else if(pm.mode === 'determined') {
   1904 				pm.mode = 'undetermined';
   1905 			}
   1906 		}
   1907 		_lastPercentage = percentage;
   1908 	}
   1909 	
   1910 	
   1911 	/**
   1912 	 * Hide Zotero pane overlay in all windows
   1913 	 */
   1914 	this.hideZoteroPaneOverlays = function () {
   1915 		this.locked = false;
   1916 		
   1917 		var enumerator = Services.wm.getEnumerator("navigator:browser");
   1918 		while (enumerator.hasMoreElements()) {
   1919 			var win = enumerator.getNext();
   1920 			if(win.ZoteroPane && win.ZoteroPane.document) {
   1921 				_hideWindowZoteroPaneOverlay(win.ZoteroPane.document);
   1922 			}
   1923 		}
   1924 		
   1925 		if (_progressPopup) {
   1926 			_progressPopup.close();
   1927 		}
   1928 		
   1929 		_progressMessage = null;
   1930 		_progressMeters = [];
   1931 		_progressPopup = null;
   1932 		_lastPercentage = null;
   1933 	}
   1934 	
   1935 	
   1936 	/**
   1937 	 * Adds a listener to be called when Zotero shuts down (even if Firefox is not shut down)
   1938 	 */
   1939 	this.addShutdownListener = function(listener) {
   1940 		_shutdownListeners.push(listener);
   1941 	}
   1942 	
   1943 	function _showWindowZoteroPaneOverlay(doc) {
   1944 		doc.getElementById('zotero-collections-tree').disabled = true;
   1945 		doc.getElementById('zotero-items-tree').disabled = true;
   1946 		doc.getElementById('zotero-pane-tab-catcher-top').hidden = false;
   1947 		doc.getElementById('zotero-pane-tab-catcher-bottom').hidden = false;
   1948 		doc.getElementById('zotero-pane-overlay').hidden = false;
   1949 	}
   1950 	
   1951 	
   1952 	function _hideWindowZoteroPaneOverlay(doc) {
   1953 		doc.getElementById('zotero-collections-tree').disabled = false;
   1954 		doc.getElementById('zotero-items-tree').disabled = false;
   1955 		doc.getElementById('zotero-pane-tab-catcher-top').hidden = true;
   1956 		doc.getElementById('zotero-pane-tab-catcher-bottom').hidden = true;
   1957 		doc.getElementById('zotero-pane-overlay').hidden = true;
   1958 		
   1959 		// See note in showZoteroPaneProgressMeter()
   1960 		let pm = doc.getElementById('zotero-pane-progressmeter');
   1961 		if (pm) {
   1962 			pm.parentNode.removeChild(pm);
   1963 		}
   1964 	}
   1965 	
   1966 	
   1967 	this.updateQuickSearchBox = function (document) {
   1968 		var searchBox = document.getElementById('zotero-tb-search');
   1969 		if(!searchBox) return;
   1970 		
   1971 		var mode = Zotero.Prefs.get("search.quicksearch-mode");
   1972 		var prefix = 'zotero-tb-search-mode-';
   1973 		var prefixLen = prefix.length;
   1974 		
   1975 		var modes = {
   1976 			titleCreatorYear: {
   1977 				label: Zotero.getString('quickSearch.mode.titleCreatorYear')
   1978 			},
   1979 			
   1980 			fields: {
   1981 				label: Zotero.getString('quickSearch.mode.fieldsAndTags')
   1982 			},
   1983 			
   1984 			everything: {
   1985 				label: Zotero.getString('quickSearch.mode.everything')
   1986 			}
   1987 		};
   1988 		
   1989 		if (!modes[mode]) {
   1990 			Zotero.Prefs.set("search.quicksearch-mode", "fields");
   1991 			mode = 'fields';
   1992 		}
   1993 		
   1994 		var hbox = document.getAnonymousNodes(searchBox)[0];
   1995 		var input = hbox.getElementsByAttribute('class', 'textbox-input')[0];
   1996 		
   1997 		// Already initialized, so just update selection
   1998 		var button = hbox.getElementsByAttribute('id', 'zotero-tb-search-menu-button');
   1999 		if (button.length) {
   2000 			button = button[0];
   2001 			var menupopup = button.firstChild;
   2002 			for (let menuitem of menupopup.childNodes) {
   2003 				if (menuitem.id.substr(prefixLen) == mode) {
   2004 					menuitem.setAttribute('checked', true);
   2005 					searchBox.placeholder = modes[mode].label;
   2006 					return;
   2007 				}
   2008 			}
   2009 			return;
   2010 		}
   2011 		
   2012 		// Otherwise, build menu
   2013 		button = document.createElement('button');
   2014 		button.id = 'zotero-tb-search-menu-button';
   2015 		button.setAttribute('type', 'menu');
   2016 		
   2017 		var menupopup = document.createElement('menupopup');
   2018 		
   2019 		for (var i in modes) {
   2020 			var menuitem = document.createElement('menuitem');
   2021 			menuitem.setAttribute('id', prefix + i);
   2022 			menuitem.setAttribute('label', modes[i].label);
   2023 			menuitem.setAttribute('name', 'searchMode');
   2024 			menuitem.setAttribute('type', 'radio');
   2025 			//menuitem.setAttribute("tooltiptext", "");
   2026 			
   2027 			menupopup.appendChild(menuitem);
   2028 			
   2029 			if (mode == i) {
   2030 				menuitem.setAttribute('checked', true);
   2031 				menupopup.selectedItem = menuitem;
   2032 			}
   2033 		}
   2034 		
   2035 		menupopup.addEventListener("command", function(event) {
   2036 			var mode = event.target.id.substr(22);
   2037 			Zotero.Prefs.set("search.quicksearch-mode", mode);
   2038 			if (document.getElementById("zotero-tb-search").value == "") {
   2039 				event.stopPropagation();
   2040 			}
   2041 		}, false);
   2042 		
   2043 		button.appendChild(menupopup);
   2044 		hbox.insertBefore(button, input);
   2045 		
   2046 		searchBox.placeholder = modes[mode].label;
   2047 		
   2048 		// If Alt-Up/Down, show popup
   2049 		searchBox.addEventListener("keypress", function(event) {
   2050 			if (event.altKey && (event.keyCode == event.DOM_VK_UP || event.keyCode == event.DOM_VK_DOWN)) {
   2051 				document.getElementById('zotero-tb-search-menu-button').open = true;
   2052 				event.stopPropagation();
   2053 			}
   2054 		}, false);
   2055 	}
   2056 	
   2057 	
   2058 	/*
   2059 	 * Clear entries that no longer exist from various tables
   2060 	 */
   2061 	this.purgeDataObjects = Zotero.Promise.coroutine(function* () {
   2062 		var d = new Date();
   2063 		
   2064 		yield Zotero.DB.executeTransaction(function* () {
   2065 			return Zotero.Creators.purge();
   2066 		});
   2067 		yield Zotero.DB.executeTransaction(function* () {
   2068 			return Zotero.Tags.purge();
   2069 		});
   2070 		yield Zotero.Fulltext.purgeUnusedWords();
   2071 		yield Zotero.DB.executeTransaction(function* () {
   2072 			return Zotero.Items.purge();
   2073 		});
   2074 		// DEBUG: this might not need to be permanent
   2075 		//yield Zotero.DB.executeTransaction(function* () {
   2076 		//	return Zotero.Relations.purge();
   2077 		//});
   2078 		
   2079 		Zotero.debug("Purged data tables in " + (new Date() - d) + " ms");
   2080 	});
   2081 	
   2082 	
   2083 	this.reloadDataObjects = function () {
   2084 		return Zotero.Promise.all([
   2085 			Zotero.Collections.reloadAll(),
   2086 			Zotero.Creators.reloadAll(),
   2087 			Zotero.Items.reloadAll()
   2088 		]);
   2089 	}
   2090 	
   2091 	
   2092 	/**
   2093 	 * Brings Zotero Standalone to the foreground
   2094 	 */
   2095 	this.activateStandalone = function() {
   2096 		var uri = Services.io.newURI('zotero://select', null, null);
   2097 		var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
   2098 					.getService(Components.interfaces.nsIExternalProtocolService)
   2099 					.getProtocolHandlerInfo('zotero');
   2100 		handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault;
   2101 		handler.launchWithURI(uri, null);
   2102 	}
   2103 	
   2104 	/**
   2105 	 * Determines whether to keep an error message so that it can (potentially) be reported later
   2106 	 */
   2107 	function _shouldKeepError(msg) {
   2108 		const skip = ['CSS Parser', 'content javascript'];
   2109 		
   2110 		//Zotero.debug(msg);
   2111 		try {
   2112 			msg.QueryInterface(Components.interfaces.nsIScriptError);
   2113 			//Zotero.debug(msg);
   2114 			if (skip.indexOf(msg.category) != -1 || msg.flags & msg.warningFlag) {
   2115 				return false;
   2116 			}
   2117 		}
   2118 		catch (e) { }
   2119 		
   2120 		const blacklist = [
   2121 			"No chrome package registered for chrome://communicator",
   2122 			'[JavaScript Error: "Components is not defined" {file: "chrome://nightly/content/talkback/talkback.js',
   2123 			'[JavaScript Error: "document.getElementById("sanitizeItem")',
   2124 			'No chrome package registered for chrome://piggy-bank',
   2125 			'[JavaScript Error: "[Exception... "\'Component is not available\' when calling method: [nsIHandlerService::getTypeFromExtension',
   2126 			'[JavaScript Error: "this._uiElement is null',
   2127 			'Error: a._updateVisibleText is not a function',
   2128 			'[JavaScript Error: "Warning: unrecognized command line flag ',
   2129 			'LibX:',
   2130 			'function skype_',
   2131 			'[JavaScript Error: "uncaught exception: Permission denied to call method Location.toString"]',
   2132 			'CVE-2009-3555',
   2133 			'OpenGL',
   2134 			'trying to re-register CID',
   2135 			'Services.HealthReport',
   2136 			'[JavaScript Error: "this.docShell is null"',
   2137 			'[JavaScript Error: "downloadable font:',
   2138 			'[JavaScript Error: "Image corrupt or truncated:',
   2139 			'[JavaScript Error: "The character encoding of the',
   2140 			'nsLivemarkService.js',
   2141 			'Sync.Engine.Tabs',
   2142 			'content-sessionStore.js',
   2143 			'org.mozilla.appSessions',
   2144 			'bad script XDR magic number',
   2145 			'did not contain an updates property',
   2146 		];
   2147 		
   2148 		for (var i=0; i<blacklist.length; i++) {
   2149 			if (msg.message.indexOf(blacklist[i]) != -1) {
   2150 				//Zotero.debug("Skipping blacklisted error: " + msg.message);
   2151 				return false;
   2152 			}
   2153 		}
   2154 		
   2155 		return true;
   2156 	}
   2157 
   2158 	/**
   2159 	 * Warn if Zotero Standalone is running as root and clobber the cache directory if it is
   2160 	 */
   2161 	function _checkRoot() {
   2162 		var env = Components.classes["@mozilla.org/process/environment;1"].
   2163 			getService(Components.interfaces.nsIEnvironment);
   2164 		var user = env.get("USER") || env.get("USERNAME");
   2165 		if(user === "root") {
   2166 			// Show warning
   2167 			if(Services.prompt.confirmEx(null, "", Zotero.getString("standalone.rootWarning"),
   2168 					Services.prompt.BUTTON_POS_0*Services.prompt.BUTTON_TITLE_IS_STRING |
   2169 					Services.prompt.BUTTON_POS_1*Services.prompt.BUTTON_TITLE_IS_STRING,
   2170 					Zotero.getString("standalone.rootWarning.exit"),
   2171 					Zotero.getString("standalone.rootWarning.continue"),
   2172 					null, null, {}) == 0) {
   2173 				Components.utils.import("resource://gre/modules/ctypes.jsm");
   2174 				var exit = Zotero.IPC.getLibc().declare("exit", ctypes.default_abi,
   2175 					                                    ctypes.void_t, ctypes.int);
   2176 				// Zap cache files
   2177 				try {
   2178 					Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).remove(true);
   2179 				} catch(e) {}
   2180 				// Exit Zotero without giving XULRunner the opportunity to figure out the
   2181 				// cache is missing. Otherwise XULRunner will zap the prefs
   2182 				exit(0);
   2183 			}
   2184 		}
   2185 	}
   2186 	
   2187 	/**
   2188 	 * Observer for console messages
   2189 	 * @namespace
   2190 	 */
   2191 	var ConsoleListener = {
   2192 		"QueryInterface":XPCOMUtils.generateQI([Components.interfaces.nsIConsoleMessage,
   2193 			Components.interfaces.nsISupports]),
   2194 		"observe":function(msg) {
   2195 			if(!_shouldKeepError(msg)) return;
   2196 			if(_recentErrors.length === ERROR_BUFFER_SIZE) _recentErrors.shift();
   2197 			_recentErrors.push(msg);
   2198 		}
   2199 	};
   2200 }).call(Zotero);
   2201 
   2202 Zotero.Prefs = new function(){
   2203 	// Privileged methods
   2204 	this.init = init;
   2205 	this.get = get;
   2206 	this.set = set;
   2207 	
   2208 	this.register = register;
   2209 	this.unregister = unregister;
   2210 	this.observe = observe;
   2211 	
   2212 	// Public properties
   2213 	this.prefBranch;
   2214 	
   2215 	function init(){
   2216 		this.prefBranch = Services.prefs.getBranch(ZOTERO_CONFIG.PREF_BRANCH);
   2217 		
   2218 		// Register observer to handle pref changes
   2219 		this.register();
   2220 
   2221 		// Unregister observer handling pref changes
   2222 		if (Zotero.addShutdownListener) {
   2223 			Zotero.addShutdownListener(this.unregister.bind(this));
   2224 		}
   2225 
   2226 		// Process pref version updates
   2227 		var fromVersion = this.get('prefVersion');
   2228 		if (!fromVersion) {
   2229 			fromVersion = 0;
   2230 		}
   2231 		var toVersion = 2;
   2232 		if (fromVersion < toVersion) {
   2233 			for (var i = fromVersion + 1; i <= toVersion; i++) {
   2234 				switch (i) {
   2235 					case 1:
   2236 						// If a sync username is entered and ZFS is enabled, turn
   2237 						// on-demand downloading off to maintain current behavior
   2238 						if (this.get('sync.server.username')) {
   2239 							if (this.get('sync.storage.enabled')
   2240 									&& this.get('sync.storage.protocol') == 'zotero') {
   2241 								this.set('sync.storage.downloadMode.personal', 'on-sync');
   2242 							}
   2243 							if (this.get('sync.storage.groups.enabled')) {
   2244 								this.set('sync.storage.downloadMode.groups', 'on-sync');
   2245 							}
   2246 						}
   2247 						break;
   2248 					
   2249 					case 2:
   2250 						// Re-show saveButton guidance panel (and clear old saveIcon pref).
   2251 						// The saveButton guidance panel initially could auto-hide too easily.
   2252 						this.clear('firstRunGuidanceShown.saveIcon');
   2253 						this.clear('firstRunGuidanceShown.saveButton');
   2254 						break;
   2255 				}
   2256 			}
   2257 			this.set('prefVersion', toVersion);
   2258 		}
   2259 	}
   2260 	
   2261 	
   2262 	/**
   2263 	* Retrieve a preference
   2264 	**/
   2265 	function get(pref, global){
   2266 		try {
   2267 			if (global) {
   2268 				var branch = Services.prefs.getBranch("");
   2269 			}
   2270 			else {
   2271 				var branch = this.prefBranch;
   2272 			}
   2273 			
   2274 			switch (branch.getPrefType(pref)){
   2275 				case branch.PREF_BOOL:
   2276 					return branch.getBoolPref(pref);
   2277 				case branch.PREF_STRING:
   2278 					return '' + branch.getComplexValue(pref, Components.interfaces.nsISupportsString);
   2279 				case branch.PREF_INT:
   2280 					return branch.getIntPref(pref);
   2281 			}
   2282 		}
   2283 		catch (e){
   2284 			throw ("Invalid preference '" + pref + "'");
   2285 		}
   2286 	}
   2287 	
   2288 	
   2289 	/**
   2290 	* Set a preference
   2291 	**/
   2292 	function set(pref, value, global) {
   2293 		try {
   2294 			if (global) {
   2295 				var branch = Services.prefs.getBranch("");
   2296 			}
   2297 			else {
   2298 				var branch = this.prefBranch;
   2299 			}
   2300 			
   2301 			switch (branch.getPrefType(pref)) {
   2302 				case branch.PREF_BOOL:
   2303 					return branch.setBoolPref(pref, value);
   2304 				case branch.PREF_STRING:
   2305 					let str = Cc["@mozilla.org/supports-string;1"]
   2306 						.createInstance(Ci.nsISupportsString);
   2307 					str.data = value;
   2308 					return branch.setComplexValue(pref, Ci.nsISupportsString, str);
   2309 				case branch.PREF_INT:
   2310 					return branch.setIntPref(pref, value);
   2311 				
   2312 				// If not an existing pref, create appropriate type automatically
   2313 				case 0:
   2314 					if (typeof value == 'boolean') {
   2315 						Zotero.debug("Creating boolean pref '" + pref + "'");
   2316 						return branch.setBoolPref(pref, value);
   2317 					}
   2318 					if (typeof value == 'string') {
   2319 						Zotero.debug("Creating string pref '" + pref + "'");
   2320 						return branch.setCharPref(pref, value);
   2321 					}
   2322 					if (parseInt(value) == value) {
   2323 						Zotero.debug("Creating integer pref '" + pref + "'");
   2324 						return branch.setIntPref(pref, value);
   2325 					}
   2326 					throw new Error("Invalid preference value '" + value + "' for pref '" + pref + "'");
   2327 			}
   2328 		}
   2329 		catch (e) {
   2330 			Zotero.logError(e);
   2331 			throw new Error("Invalid preference '" + pref + "'");
   2332 		}
   2333 	}
   2334 	
   2335 	
   2336 	this.clear = function (pref, global) {
   2337 		if (global) {
   2338 			var branch = Services.prefs.getBranch("");
   2339 		}
   2340 		else {
   2341 			var branch = this.prefBranch;
   2342 		}
   2343 		branch.clearUserPref(pref);
   2344 	}
   2345 	
   2346 	
   2347 	this.resetBranch = function (exclude = []) {
   2348 		var keys = this.prefBranch.getChildList("", {});
   2349 		for (let key of keys) {
   2350 			if (this.prefBranch.prefHasUserValue(key)) {
   2351 				if (exclude.includes(key)) {
   2352 					continue;
   2353 				}
   2354 				Zotero.debug("Clearing " + key);
   2355 				this.prefBranch.clearUserPref(key);
   2356 			}
   2357 		}
   2358 	};
   2359 	
   2360 	
   2361 	// Import settings bundles
   2362 	this.importSettings = function (str, uri) {
   2363 		var ps = Services.prompt;
   2364 		
   2365 		if (!uri.match(/https:\/\/([^\.]+\.)?zotero.org\//)) {
   2366 			Zotero.debug("Ignoring settings file not from https://zotero.org");
   2367 			return;
   2368 		}
   2369 		
   2370 		str = Zotero.Utilities.trim(str.replace(/<\?xml.*\?>\s*/, ''));
   2371 		Zotero.debug(str);
   2372 		
   2373 		var confirm = ps.confirm(
   2374 			null,
   2375 			"",
   2376 			"Apply settings from zotero.org?"
   2377 		);
   2378 		
   2379 		if (!confirm) {
   2380 			return;
   2381 		}
   2382 		
   2383 		// TODO: parse settings XML
   2384 	}
   2385 	
   2386 	// Handlers for some Zotero preferences
   2387 	var _handlers = [
   2388 		[ "automaticScraperUpdates", function(val) {
   2389 			if (val){
   2390 				Zotero.Schema.updateFromRepository(1);
   2391 			}
   2392 			else {
   2393 				Zotero.Schema.stopRepositoryTimer();
   2394 			}
   2395 		}],
   2396 		["fontSize", function (val) {
   2397 			Zotero.setFontSize(
   2398 				Zotero.getActiveZoteroPane().document.getElementById('zotero-pane')
   2399 			);
   2400 		}],
   2401 		[ "layout", function(val) {
   2402 			Zotero.getActiveZoteroPane().updateLayout();
   2403 		}],
   2404 		[ "note.fontSize", function(val) {
   2405 			if (val < 6) {
   2406 				Zotero.Prefs.set('note.fontSize', 11);
   2407 			}
   2408 		}],
   2409 		[ "zoteroDotOrgVersionHeader", function(val) {
   2410 			if (val) {
   2411 				Zotero.VersionHeader.register();
   2412 			}
   2413 			else {
   2414 				Zotero.VersionHeader.unregister();
   2415 			}
   2416 		}],
   2417 		[ "sync.autoSync", function(val) {
   2418 			if (val) {
   2419 				Zotero.Sync.EventListeners.AutoSyncListener.register();
   2420 				Zotero.Sync.EventListeners.IdleListener.register();
   2421 			}
   2422 			else {
   2423 				Zotero.Sync.EventListeners.AutoSyncListener.unregister();
   2424 				Zotero.Sync.EventListeners.IdleListener.unregister();
   2425 			}
   2426 		}],
   2427 		[ "search.quicksearch-mode", function(val) {
   2428 			var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
   2429 						.getService(Components.interfaces.nsIWindowMediator);
   2430 			var enumerator = wm.getEnumerator("navigator:browser");
   2431 			while (enumerator.hasMoreElements()) {
   2432 				var win = enumerator.getNext();
   2433 				if (!win.ZoteroPane) continue;
   2434 				Zotero.updateQuickSearchBox(win.ZoteroPane.document);
   2435 			}
   2436 			
   2437 			var enumerator = wm.getEnumerator("zotero:item-selector");
   2438 			while (enumerator.hasMoreElements()) {
   2439 				var win = enumerator.getNext();
   2440 				if (!win.Zotero) continue;
   2441 				Zotero.updateQuickSearchBox(win.document);
   2442 			}
   2443 		}]
   2444 	];
   2445 	
   2446 	//
   2447 	// Methods to register a preferences observer
   2448 	//
   2449 	function register(){
   2450 		this.prefBranch.QueryInterface(Components.interfaces.nsIPrefBranch2);
   2451 		this.prefBranch.addObserver("", this, false);
   2452 		
   2453 		// Register pre-set handlers
   2454 		for (var i=0; i<_handlers.length; i++) {
   2455 			this.registerObserver(_handlers[i][0], _handlers[i][1]);
   2456 		}
   2457 	}
   2458 	
   2459 	function unregister(){
   2460 		if (!this.prefBranch){
   2461 			return;
   2462 		}
   2463 		this.prefBranch.removeObserver("", this);
   2464 	}
   2465 	
   2466 	/**
   2467 	 * @param {nsIPrefBranch} subject The nsIPrefBranch we're observing (after appropriate QI)
   2468 	 * @param {String} topic The string defined by NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
   2469 	 * @param {String} data The name of the pref that's been changed (relative to subject)
   2470 	 */
   2471 	function observe(subject, topic, data){
   2472 		if (topic != "nsPref:changed" || !_observers[data] || !_observers[data].length) {
   2473 			return;
   2474 		}
   2475 		
   2476 		var obs = _observers[data];
   2477 		for (var i=0; i<obs.length; i++) {
   2478 			try {
   2479 				obs[i](this.get(data));
   2480 			}
   2481 			catch (e) {
   2482 				Zotero.debug("Error while executing preference observer handler for " + data);
   2483 				Zotero.debug(e);
   2484 			}
   2485 		}
   2486 	}
   2487 	
   2488 	var _observers = {};
   2489 	this.registerObserver = function(name, handler) {
   2490 		_observers[name] = _observers[name] || [];
   2491 		_observers[name].push(handler);
   2492 	}
   2493 	
   2494 	this.unregisterObserver = function(name, handler) {
   2495 		var obs = _observers[name];
   2496 		if (!obs) {
   2497 			Zotero.debug("No preferences observer registered for " + name);
   2498 			return;
   2499 		}
   2500 		
   2501 		var i = obs.indexOf(handler);
   2502 		if (i == -1) {
   2503 			Zotero.debug("Handler was not registered for preference " + name);
   2504 			return;
   2505 		}
   2506 		
   2507 		obs.splice(i, 1);
   2508 	}
   2509 }
   2510 
   2511 
   2512 /*
   2513  * Handles keyboard shortcut initialization from preferences, optionally
   2514  * overriding existing global shortcuts
   2515  *
   2516  * Actions are configured in ZoteroPane.handleKeyPress()
   2517  */
   2518 Zotero.Keys = new function() {
   2519 	this.init = init;
   2520 	this.windowInit = windowInit;
   2521 	this.getCommand = getCommand;
   2522 	
   2523 	var _keys = {};
   2524 	
   2525 	
   2526 	/*
   2527 	 * Called by Zotero.init()
   2528 	 */
   2529 	function init() {
   2530 		var cmds = Zotero.Prefs.prefBranch.getChildList('keys', {}, {});
   2531 		
   2532 		// Get the key=>command mappings from the prefs
   2533 		for (let cmd of cmds) {
   2534 			cmd = cmd.substr(5); // strips 'keys.'
   2535 			// Remove old pref
   2536 			if (cmd == 'overrideGlobal') {
   2537 				Zotero.Prefs.clear('keys.overrideGlobal');
   2538 				continue;
   2539 			}
   2540 			_keys[this.getKeyForCommand(cmd)] = cmd;
   2541 		}
   2542 	}
   2543 	
   2544 	
   2545 	/*
   2546 	 * Called by ZoteroPane.onLoad()
   2547 	 */
   2548 	function windowInit(document) {
   2549 		var globalKeys = [
   2550 			{
   2551 				name: 'openZotero',
   2552 				defaultKey: 'Z'
   2553 			},
   2554 			{
   2555 				name: 'saveToZotero',
   2556 				defaultKey: 'S'
   2557 			}
   2558 		];
   2559 		
   2560 		globalKeys.forEach(function (x) {
   2561 			let keyElem = document.getElementById('key_' + x.name);
   2562 			if (keyElem) {
   2563 				let prefKey = this.getKeyForCommand(x.name);
   2564 				// Only override the default with the pref if the <key> hasn't
   2565 				// been manually changed and the pref has been
   2566 				if (keyElem.getAttribute('key') == x.defaultKey
   2567 						&& keyElem.getAttribute('modifiers') == 'accel shift'
   2568 						&& prefKey != x.defaultKey) {
   2569 					keyElem.setAttribute('key', prefKey);
   2570 				}
   2571 			}
   2572 		}.bind(this));
   2573 	}
   2574 	
   2575 	
   2576 	function getCommand(key) {
   2577 		key = key.toUpperCase();
   2578 		return _keys[key] ? _keys[key] : false;
   2579 	}
   2580 	
   2581 	
   2582 	this.getKeyForCommand = function (cmd) {
   2583 		try {
   2584 			var key = Zotero.Prefs.get('keys.' + cmd);
   2585 		}
   2586 		catch (e) {}
   2587 		return key !== undefined ? key.toUpperCase() : false;
   2588 	}
   2589 }
   2590 
   2591 
   2592 /**
   2593  * Add X-Zotero-Version header to HTTP requests to zotero.org
   2594  *
   2595  * @namespace
   2596  */
   2597 Zotero.VersionHeader = {
   2598 	init: function () {
   2599 		if (Zotero.Prefs.get("zoteroDotOrgVersionHeader")) {
   2600 			this.register();
   2601 		}
   2602 		Zotero.addShutdownListener(this.unregister);
   2603 	},
   2604 	
   2605 	// Called from this.init() and Zotero.Prefs.observe()
   2606 	register: function () {
   2607 		Services.obs.addObserver(this, "http-on-modify-request", false);
   2608 	},
   2609 	
   2610 	observe: function (subject, topic, data) {
   2611 		try {
   2612 			let channel = subject.QueryInterface(Components.interfaces.nsIHttpChannel);
   2613 			let domain = channel.URI.host;
   2614 			if (domain.endsWith(ZOTERO_CONFIG.DOMAIN_NAME)) {
   2615 				channel.setRequestHeader("X-Zotero-Version", Zotero.version, false);
   2616 			}
   2617 			else {
   2618 				let ua = channel.getRequestHeader('User-Agent');
   2619 				ua = this.update(domain, ua);
   2620 				channel.setRequestHeader('User-Agent', ua, false);
   2621 			}
   2622 		}
   2623 		catch (e) {
   2624 			Zotero.debug(e, 1);
   2625 		}
   2626 	},
   2627 	
   2628 	/**
   2629 	 * Add Firefox/[version] to the default user agent and replace Zotero/[version] with
   2630 	 * Zotero/[major.minor] (except for requests to zotero.org, where we include the full version)
   2631 	 *
   2632 	 * @param {String} domain
   2633 	 * @param {String} ua - User Agent
   2634 	 * @param {String} [testAppName] - App name to look for (necessary in tests, which are
   2635 	 *     currently run in Firefox)
   2636 	 */
   2637 	update: function (domain, ua, testAppName) {
   2638 		var info = Services.appinfo;
   2639 		var appName = testAppName || info.name;
   2640 		
   2641 		var pos = ua.indexOf(appName + '/');
   2642 		
   2643 		// Default UA
   2644 		if (pos != -1) {
   2645 			ua = ua.slice(0, pos) + `Firefox/${info.platformVersion.match(/^\d+/)[0]}.0 `
   2646 				+ appName + '/';
   2647 		}
   2648 		// Fake UA from connector
   2649 		else {
   2650 			ua += ' ' + appName + '/';
   2651 		}
   2652 		ua += Zotero.version.replace(/(\d+\.\d+).*/, '$1');
   2653 		
   2654 		return ua;
   2655 	},
   2656 	
   2657 	unregister: function () {
   2658 		Services.obs.removeObserver(Zotero.VersionHeader, "http-on-modify-request");
   2659 	}
   2660 }
   2661 
   2662 Zotero.DragDrop = {
   2663 	currentEvent: null,
   2664 	currentOrientation: 0,
   2665 	currentSourceNode: null,
   2666 	
   2667 	getDataFromDataTransfer: function (dataTransfer, firstOnly) {
   2668 		var dt = dataTransfer;
   2669 		
   2670 		var dragData = {
   2671 			dataType: '',
   2672 			data: [],
   2673 			dropEffect: dt.dropEffect
   2674 		};
   2675 		
   2676 		var len = firstOnly ? 1 : dt.mozItemCount;
   2677 		
   2678 		if (dt.types.contains('zotero/collection')) {
   2679 			dragData.dataType = 'zotero/collection';
   2680 			let ids = dt.getData('zotero/collection').split(",").map(id => parseInt(id));
   2681 			dragData.data = ids;
   2682 		}
   2683 		else if (dt.types.contains('zotero/item')) {
   2684 			dragData.dataType = 'zotero/item';
   2685 			let ids = dt.getData('zotero/item').split(",").map(id => parseInt(id));
   2686 			dragData.data = ids;
   2687 		}
   2688 		else {
   2689 			if (dt.types.contains('application/x-moz-file')) {
   2690 				dragData.dataType = 'application/x-moz-file';
   2691 				var files = [];
   2692 				for (var i=0; i<len; i++) {
   2693 					var file = dt.mozGetDataAt("application/x-moz-file", i);
   2694 					if (!file) {
   2695 						continue;
   2696 					}
   2697 					file.QueryInterface(Components.interfaces.nsIFile);
   2698 					// Don't allow folder drag
   2699 					if (file.isDirectory()) {
   2700 						continue;
   2701 					}
   2702 					files.push(file);
   2703 				}
   2704 				dragData.data = files;
   2705 			}
   2706 			// This isn't an else because on Linux a link drag contains an empty application/x-moz-file too
   2707 			if (!dragData.data || !dragData.data.length) {
   2708 				if (dt.types.contains('text/x-moz-url')) {
   2709 					dragData.dataType = 'text/x-moz-url';
   2710 					var urls = [];
   2711 					for (var i=0; i<len; i++) {
   2712 						var url = dt.getData("text/x-moz-url").split("\n")[0];
   2713 						urls.push(url);
   2714 					}
   2715 					dragData.data = urls;
   2716 				}
   2717 			}
   2718 		}
   2719 		
   2720 		return dragData;
   2721 	},
   2722 	
   2723 	
   2724 	getDragSource: function (dataTransfer) {
   2725 		if (!dataTransfer) {
   2726 			//Zotero.debug("Drag data not available", 2);
   2727 			return false;
   2728 		}
   2729 		
   2730 		// For items, the drag source is the CollectionTreeRow of the parent window
   2731 		// of the source tree
   2732 		if (dataTransfer.types.contains("zotero/item")) {
   2733 			let sourceNode = dataTransfer.mozSourceNode || this.currentSourceNode;
   2734 			if (!sourceNode || sourceNode.tagName != 'treechildren'
   2735 					|| sourceNode.parentElement.id != 'zotero-items-tree') {
   2736 				return false;
   2737 			}
   2738 			var win = sourceNode.ownerDocument.defaultView;
   2739 			if (win.document.documentElement.getAttribute('windowtype') == 'zotero:search') {
   2740 				return win.ZoteroAdvancedSearch.itemsView.collectionTreeRow;
   2741 			}
   2742 			return win.ZoteroPane.collectionsView.selectedTreeRow;
   2743 		}
   2744 		
   2745 		return false;
   2746 	},
   2747 	
   2748 	
   2749 	getDragTarget: function (event) {
   2750 		var target = event.target;
   2751 		if (target.tagName == 'treechildren') {
   2752 			var tree = target.parentNode;
   2753 			if (tree.id == 'zotero-collections-tree') {
   2754 				let row = {}, col = {}, obj = {};
   2755 				tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
   2756 				let win = tree.ownerDocument.defaultView;
   2757 				return win.ZoteroPane.collectionsView.getRow(row.value);
   2758 			}
   2759 		}
   2760 		return false;
   2761 	}
   2762 }
   2763 
   2764 
   2765 /**
   2766  * Functions for creating and destroying hidden browser objects
   2767  **/
   2768 Zotero.Browser = new function() {
   2769 	var nBrowsers = 0;
   2770 	
   2771 	this.createHiddenBrowser = function (win) {
   2772 		if (!win) {
   2773 			win = Services.wm.getMostRecentWindow("navigator:browser");
   2774 			if (!win) {
   2775 				win = Services.ww.activeWindow;
   2776 			}
   2777 			// Use the hidden DOM window on macOS with the main window closed
   2778 			if (!win) {
   2779 				let appShellService = Components.classes["@mozilla.org/appshell/appShellService;1"]
   2780 					.getService(Components.interfaces.nsIAppShellService);
   2781 				win = appShellService.hiddenDOMWindow;
   2782 			}
   2783 			if (!win) {
   2784 				throw new Error("Parent window not available for hidden browser");
   2785 			}
   2786 		}
   2787 		
   2788 		// Create a hidden browser
   2789 		var hiddenBrowser = win.document.createElement("browser");
   2790 		hiddenBrowser.setAttribute('type', 'content');
   2791 		hiddenBrowser.setAttribute('disablehistory', 'true');
   2792 		win.document.documentElement.appendChild(hiddenBrowser);
   2793 		// Disable some features
   2794 		hiddenBrowser.docShell.allowAuth = false;
   2795 		hiddenBrowser.docShell.allowDNSPrefetch = false;
   2796 		hiddenBrowser.docShell.allowImages = false;
   2797 		hiddenBrowser.docShell.allowJavascript = true;
   2798 		hiddenBrowser.docShell.allowMetaRedirects = false;
   2799 		hiddenBrowser.docShell.allowPlugins = false;
   2800 		Zotero.debug("Created hidden browser (" + (nBrowsers++) + ")");
   2801 		return hiddenBrowser;
   2802 	}
   2803 	
   2804 	this.deleteHiddenBrowser = function (myBrowsers) {
   2805 		if(!(myBrowsers instanceof Array)) myBrowsers = [myBrowsers];
   2806 		for(var i=0; i<myBrowsers.length; i++) {
   2807 			var myBrowser = myBrowsers[i];
   2808 			myBrowser.stop();
   2809 			myBrowser.destroy();
   2810 			myBrowser.parentNode.removeChild(myBrowser);
   2811 			myBrowser = null;
   2812 			Zotero.debug("Deleted hidden browser (" + (--nBrowsers) + ")");
   2813 		}
   2814 	}
   2815 }
   2816 
   2817 
   2818 /*
   2819  * Implements nsIWebProgressListener
   2820  */
   2821 Zotero.WebProgressFinishListener = function(onFinish) {
   2822 	var _request;
   2823 	
   2824 	this.getRequest = function () {
   2825 		return _request;
   2826 	};
   2827 	
   2828 	this.onStateChange = function(wp, req, stateFlags, status) {
   2829 		//Zotero.debug('onStageChange: ' + stateFlags);
   2830 		if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP
   2831 				&& stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK) {
   2832 			_request = null;
   2833 			onFinish();
   2834 		}
   2835 		else {
   2836 			_request = req;
   2837 		}
   2838 	}
   2839 	
   2840 	this.onProgressChange = function(wp, req, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {
   2841 		//Zotero.debug('onProgressChange');
   2842 		//Zotero.debug('Current: ' + curTotalProgress);
   2843 		//Zotero.debug('Max: ' + maxTotalProgress);
   2844 	}
   2845 	
   2846 	this.onLocationChange = function(wp, req, location) {}
   2847 	this.onSecurityChange = function(wp, req, stateFlags, status) {}
   2848 	this.onStatusChange = function(wp, req, status, msg) {}
   2849 }
   2850 
   2851 /*
   2852  * Saves or loads JSON objects.
   2853  */
   2854 Zotero.JSON = new function() {
   2855 	this.serialize = function(arg) {
   2856 		Zotero.debug("WARNING: Zotero.JSON.serialize() is deprecated; use JSON.stringify()");
   2857 		return JSON.stringify(arg);
   2858 	}
   2859 	
   2860 	this.unserialize = function(arg) {
   2861 		Zotero.debug("WARNING: Zotero.JSON.unserialize() is deprecated; use JSON.parse()");
   2862 		return JSON.parse(arg);
   2863 	}
   2864 }