www

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

preferences_sync.js (25212B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2008–2013 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 "use strict";
     27 Components.utils.import("resource://gre/modules/Services.jsm");
     28 Components.utils.import("resource://gre/modules/osfile.jsm");
     29 Components.utils.import("resource://zotero/config.js");
     30 
     31 Zotero_Preferences.Sync = {
     32 	init: Zotero.Promise.coroutine(function* () {
     33 		this.updateStorageSettingsUI();
     34 		this.updateStorageSettingsGroupsUI();
     35 
     36 		var username = Zotero.Users.getCurrentUsername() || Zotero.Prefs.get('sync.server.username') || " ";
     37 		var apiKey = yield Zotero.Sync.Data.Local.getAPIKey();
     38 		this.displayFields(apiKey ? username : "");
     39 		
     40 		var pass = Zotero.Sync.Runner.getStorageController('webdav').password;
     41 		if (pass) {
     42 			document.getElementById('storage-password').value = pass;
     43 		}
     44 		
     45 		if (apiKey) {
     46 			try {
     47 				var keyInfo = yield Zotero.Sync.Runner.checkAccess(
     48 					Zotero.Sync.Runner.getAPIClient({apiKey}),
     49 					{timeout: 5000}
     50 				);
     51 				this.displayFields(keyInfo.username);
     52 			}
     53 			catch (e) {
     54 				// API key wrong/invalid
     55 				if (e instanceof Zotero.Error && e.error == Zotero.Error.ERROR_API_KEY_INVALID) {
     56 					Zotero.alert(
     57 						window,
     58 						Zotero.getString('general.error'),
     59 						Zotero.getString('sync.error.apiKeyInvalid', Zotero.clientName)
     60 					);
     61 					this.unlinkAccount(false);
     62 				}
     63 				else {
     64 					throw e;
     65 				}
     66 			}
     67 		}
     68 		
     69 		this.initResetPane();
     70 	}),
     71 	
     72 	displayFields: function (username) {
     73 		document.getElementById('sync-unauthorized').hidden = !!username;
     74 		document.getElementById('sync-authorized').hidden = !username;
     75 		document.getElementById('sync-reset-tab').disabled = !username;
     76 		document.getElementById('sync-username').value = username;
     77 		document.getElementById('sync-password').value = '';
     78 		document.getElementById('sync-username-textbox').value = Zotero.Prefs.get('sync.server.username');
     79 
     80 		var img = document.getElementById('sync-status-indicator');
     81 		img.removeAttribute('verified');
     82 		img.removeAttribute('animated');
     83 		
     84 		window.sizeToContent();
     85 	},
     86 
     87 
     88 	credentialsChange: function (event) {
     89 		var username = document.getElementById('sync-username-textbox');
     90 		var password = document.getElementById('sync-password');
     91 
     92 		var syncAuthButton = document.getElementById('sync-auth-button');
     93 
     94 		syncAuthButton.setAttribute('disabled', 'true');
     95 
     96 		// When using backspace, the value is not updated until after the keypress event
     97 		setTimeout(function() {
     98 			if (username.value.length && password.value.length) {
     99 				syncAuthButton.setAttribute('disabled', 'false');
    100 			}
    101 		});
    102 	},
    103 	
    104 	
    105 	credentialsKeyPress: function (event) {
    106 		if (event.keyCode == 13) {
    107 			this.linkAccount(event);
    108 			event.preventDefault();
    109 		}
    110 	},
    111 	
    112 	
    113 	trimUsername: function () {
    114 		var tb = document.getElementById('sync-username-textbox');
    115 		var username = tb.value;
    116 		var trimmed = username.trim();
    117 		if (username != trimmed) {
    118 			tb.value = trimmed;
    119 			// Setting .value alone doesn't seem to cause the pref to sync, so set it manually
    120 			Zotero.Prefs.set('sync.server.username', trimmed);
    121 		}
    122 	},
    123 	
    124 	
    125 	linkAccount: Zotero.Promise.coroutine(function* (event) {
    126 		this.trimUsername();
    127 		var username = document.getElementById('sync-username-textbox').value;
    128 		var password = document.getElementById('sync-password').value;
    129 
    130 		if (!username.length || !password.length) {
    131 			this.updateSyncIndicator();
    132 			return;
    133 		}
    134 
    135 		// Try to acquire API key with current credentials
    136 		this.updateSyncIndicator('animated');
    137 		try {
    138 			var json = yield Zotero.Sync.Runner.createAPIKeyFromCredentials(username, password);
    139 		}
    140 		catch (e) {
    141 			setTimeout(function () {
    142 				Zotero.alert(
    143 					window,
    144 					Zotero.getString('general.error'),
    145 					e.message
    146 				);
    147 			});
    148 			throw e;
    149 		}
    150 		finally {
    151 			this.updateSyncIndicator();
    152 		}
    153 		
    154 		// Invalid credentials
    155 		if (!json) {
    156 			Zotero.alert(window,
    157 				Zotero.getString('general.error'),
    158 				Zotero.getString('sync.error.invalidLogin')
    159 			);
    160 			return;
    161 		}
    162 
    163 		if (!(yield Zotero.Sync.Data.Local.checkUser(window, json.userID, json.username))) {
    164 			// createAPIKeyFromCredentials will have created an API key,
    165 			// but user decided not to use it, so we remove it here.
    166 			Zotero.Sync.Runner.deleteAPIKey();
    167 			return;
    168 		}
    169 		this.displayFields(json.username);
    170 	}),
    171 
    172 	/**
    173 	 * Updates the auth indicator icon, depending on status
    174 	 * @param {string} status
    175 	 */
    176 	updateSyncIndicator: function (status) {
    177 		var img = document.getElementById('sync-status-indicator');
    178 		
    179 		img.removeAttribute('animated');
    180 		if (status == 'animated') {
    181 			img.setAttribute('animated', true);
    182 		}
    183 	},
    184 
    185 	unlinkAccount: Zotero.Promise.coroutine(function* (showAlert=true) {
    186 		if (showAlert) {
    187 			var check = {value: false};
    188 			var ps = Services.prompt;
    189 			var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) +
    190 				(ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
    191 			var index = ps.confirmEx(
    192 				null,
    193 				Zotero.getString('general.warning'),
    194 				Zotero.getString('account.unlinkWarning', Zotero.clientName),
    195 				buttonFlags,
    196 				Zotero.getString('account.unlinkWarning.button'), null, null,
    197 				Zotero.getString('account.unlinkWarning.removeData', Zotero.clientName),
    198 				check
    199 			);
    200 			if (index == 0) {
    201 				if (check.value) {
    202 					var resetDataDirFile = OS.Path.join(Zotero.DataDirectory.dir, 'reset-data-directory');
    203 					yield Zotero.File.putContentsAsync(resetDataDirFile, '');
    204 
    205 					yield Zotero.Sync.Runner.deleteAPIKey();
    206 					Zotero.Prefs.clear('sync.server.username');
    207 					return Zotero.Utilities.Internal.quitZotero(true);
    208 				}
    209 			} else {
    210 				return;
    211 			}
    212 		}
    213 
    214 		this.displayFields();
    215 		Zotero.Prefs.clear('sync.librariesToSync');
    216 		yield Zotero.Sync.Runner.deleteAPIKey();
    217 	}),
    218 	
    219 	
    220 	showLibrariesToSyncDialog: function() {
    221 		var io = {};
    222 		window.openDialog('chrome://zotero/content/preferences/librariesToSync.xul',
    223 			"zotero-preferences-librariesToSyncDialog", "chrome,modal,centerscreen", io);
    224 	},
    225 	
    226 	
    227 	dblClickLibraryToSync: function (event) {
    228 		var tree = document.getElementById("libraries-to-sync-tree");
    229 		var row = {}, col = {}, child = {};
    230 		tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child);
    231 		
    232 		// Below the list or on checkmark column
    233 		if (!col.value || col.value.element.id == 'libraries-to-sync-checked') {
    234 			return;
    235 		}
    236 		// if dblclicked anywhere but the checkbox update pref
    237 		return this.toggleLibraryToSync(row.value);
    238 	},
    239 
    240 
    241 	clickLibraryToSync: function (event) {
    242 		var tree = document.getElementById("libraries-to-sync-tree");
    243 		var row = {}, col = {}, child = {};
    244 		tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child);
    245 		
    246 		// Below the list or not on checkmark column
    247 		if (!col.value || col.value.element.id != 'libraries-to-sync-checked') {
    248 			return;
    249 		}
    250 		// if clicked on checkbox update pref
    251 		return this.toggleLibraryToSync(row.value);
    252 	},
    253 	
    254 	
    255 	toggleLibraryToSync: function (index) {
    256 		var treechildren = document.getElementById('libraries-to-sync-rows');
    257 		if (index >= treechildren.childNodes.length) {
    258 			return;
    259 		}
    260 		var row = treechildren.childNodes[index];
    261 		var val = row.firstChild.childNodes[1].getAttribute('value');
    262 		if (!val) {
    263 			return
    264 		}
    265 		
    266 		var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]');
    267 		var indexOfId = librariesToSkip.indexOf(val);
    268 		if (indexOfId == -1) {
    269 			librariesToSkip.push(val);
    270 		} else {
    271 			librariesToSkip.splice(indexOfId, 1);
    272 		}
    273 		Zotero.Prefs.set('sync.librariesToSkip', JSON.stringify(librariesToSkip));
    274 		 
    275 		var cell = row.firstChild.firstChild;
    276 		cell.setAttribute('value', indexOfId != -1);
    277 	},
    278 	
    279 	
    280 	initLibrariesToSync: Zotero.Promise.coroutine(function* () {
    281 		var tree = document.getElementById("libraries-to-sync-tree");
    282 		var treechildren = document.getElementById('libraries-to-sync-rows');
    283 		while (treechildren.hasChildNodes()) {
    284 			treechildren.removeChild(treechildren.firstChild);
    285 		}
    286 		
    287 		function addRow(libraryName, id, checked=false, editable=true) {
    288 			var treeitem = document.createElement('treeitem');
    289 			var treerow = document.createElement('treerow');
    290 			var checkboxCell = document.createElement('treecell');
    291 			var nameCell = document.createElement('treecell');
    292 			
    293 			nameCell.setAttribute('label', libraryName);
    294 			nameCell.setAttribute('value', id);
    295 			nameCell.setAttribute('editable', false);
    296 			checkboxCell.setAttribute('value', checked);
    297 			checkboxCell.setAttribute('editable', editable);
    298 			
    299 			treerow.appendChild(checkboxCell);
    300 			treerow.appendChild(nameCell);
    301 			treeitem.appendChild(treerow);
    302 			treechildren.appendChild(treeitem);
    303 		}
    304 		
    305 		// Add loading row while we're loading a group list
    306 		var loadingLabel = Zotero.getString("zotero.preferences.sync.librariesToSync.loadingLibraries");
    307 		addRow(loadingLabel, "loading", false, false);
    308 
    309 		var apiKey = yield Zotero.Sync.Data.Local.getAPIKey();
    310 		var client = Zotero.Sync.Runner.getAPIClient({apiKey});
    311 		var groups = [];
    312 		try {
    313 			// Load up remote groups
    314 			var keyInfo = yield Zotero.Sync.Runner.checkAccess(client, {timeout: 5000});
    315 			groups = yield client.getGroups(keyInfo.userID);
    316 		}
    317 		catch (e) {
    318 			// Connection problems
    319 			if ((e instanceof Zotero.HTTP.UnexpectedStatusException)
    320 					|| (e instanceof Zotero.HTTP.TimeoutException)
    321 					|| (e instanceof Zotero.HTTP.BrowserOfflineException)) {
    322 				Zotero.alert(
    323 					window,
    324 					Zotero.getString('general.error'),
    325 					Zotero.getString('sync.error.checkConnection', Zotero.clientName)
    326 				);
    327 			}
    328 			else {
    329 				throw e;
    330 			}
    331 			document.getElementsByTagName('dialog')[0].acceptDialog();
    332 		}
    333 
    334 		// Remove the loading row
    335 		treechildren.removeChild(treechildren.firstChild);
    336 
    337 		var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]');
    338 		// Add default rows
    339 		addRow(Zotero.getString("pane.collections.libraryAndFeeds"), "L" + Zotero.Libraries.userLibraryID, 
    340 			librariesToSkip.indexOf("L" + Zotero.Libraries.userLibraryID) == -1);
    341 		
    342 		// Sort groups
    343 		var collation = Zotero.getLocaleCollation();
    344 		groups.sort((a, b) => collation.compareString(1, a.data.name, b.data.name));
    345 		// Add group rows
    346 		for (let group of groups) {
    347 			addRow(group.data.name, "G" + group.id, librariesToSkip.indexOf("G" + group.id) == -1);
    348 		}
    349 	}),
    350 
    351 
    352 	updateStorageSettingsUI: Zotero.Promise.coroutine(function* () {
    353 		this.unverifyStorageServer();
    354 		
    355 		var protocol = document.getElementById('pref-storage-protocol').value;
    356 		var enabled = document.getElementById('pref-storage-enabled').value;
    357 		
    358 		var storageSettings = document.getElementById('storage-settings');
    359 		var protocolMenu = document.getElementById('storage-protocol');
    360 		var settings = document.getElementById('storage-webdav-settings');
    361 		var sep = document.getElementById('storage-separator');
    362 		
    363 		if (!enabled || protocol == 'zotero') {
    364 			settings.hidden = true;
    365 			sep.hidden = false;
    366 		}
    367 		else {
    368 			settings.hidden = false;
    369 			sep.hidden = true;
    370 		}
    371 		
    372 		document.getElementById('storage-user-download-mode').disabled = !enabled;
    373 		this.updateStorageTerms();
    374 		
    375 		window.sizeToContent();
    376 	}),
    377 	
    378 	
    379 	updateStorageSettingsGroupsUI: function () {
    380 		setTimeout(() => {
    381 			var enabled = document.getElementById('pref-storage-groups-enabled').value;
    382 			document.getElementById('storage-groups-download-mode').disabled = !enabled;
    383 			this.updateStorageTerms();
    384 		});
    385 	},
    386 	
    387 	
    388 	updateStorageTerms: function () {
    389 		var terms = document.getElementById('storage-terms');
    390 		
    391 		var libraryEnabled = document.getElementById('pref-storage-enabled').value;
    392 		var storageProtocol = document.getElementById('pref-storage-protocol').value;
    393 		var groupsEnabled = document.getElementById('pref-storage-groups-enabled').value;
    394 		
    395 		terms.hidden = !((libraryEnabled && storageProtocol == 'zotero') || groupsEnabled);
    396 	},
    397 	
    398 	
    399 	onStorageSettingsKeyPress: Zotero.Promise.coroutine(function* (event) {
    400 		if (event.keyCode == 13) {
    401 			yield this.verifyStorageServer();
    402 		}
    403 	}),
    404 	
    405 	
    406 	onStorageSettingsChange: Zotero.Promise.coroutine(function* () {
    407 		// Clean URL
    408 		var urlPref = document.getElementById('pref-storage-url');
    409 		urlPref.value = urlPref.value.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, '');
    410 		
    411 		var oldProtocol = document.getElementById('pref-storage-protocol').value;
    412 		var oldEnabled = document.getElementById('pref-storage-enabled').value;
    413 		
    414 		yield Zotero.Promise.delay(1);
    415 		
    416 		var newProtocol = document.getElementById('pref-storage-protocol').value;
    417 		var newEnabled = document.getElementById('pref-storage-enabled').value;
    418 		
    419 		if (oldProtocol != newProtocol) {
    420 			yield Zotero.Sync.Storage.Local.resetAllSyncStates(Zotero.Libraries.userLibraryID);
    421 		}
    422 		
    423 		if (oldProtocol == 'webdav') {
    424 			this.unverifyStorageServer();
    425 			Zotero.Sync.Runner.resetStorageController(oldProtocol);
    426 			
    427 			var username = document.getElementById('storage-username').value;
    428 			var password = document.getElementById('storage-password').value;
    429 			if (username) {
    430 				Zotero.Sync.Runner.getStorageController('webdav').password = password;
    431 			}
    432 		}
    433 		
    434 		if (oldProtocol == 'zotero' && newProtocol == 'webdav') {
    435 			var sql = "SELECT COUNT(*) FROM settings "
    436 				+ "WHERE setting='storage' AND key='zfsPurge' AND value='user'";
    437 			if (!Zotero.DB.valueQueryAsync(sql)) {
    438 				var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    439 					.getService(Components.interfaces.nsIPromptService);
    440 				var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    441 					+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
    442 					+ ps.BUTTON_DELAY_ENABLE;
    443 				var account = Zotero.Sync.Server.username;
    444 				var index = ps.confirmEx(
    445 					null,
    446 					Zotero.getString('zotero.preferences.sync.purgeStorage.title'),
    447 					Zotero.getString('zotero.preferences.sync.purgeStorage.desc'),
    448 					buttonFlags,
    449 					Zotero.getString('zotero.preferences.sync.purgeStorage.confirmButton'),
    450 					Zotero.getString('zotero.preferences.sync.purgeStorage.cancelButton'), null, null, {}
    451 				);
    452 				
    453 				if (index == 0) {
    454 					var sql = "INSERT OR IGNORE INTO settings VALUES (?,?,?)";
    455 					yield Zotero.DB.queryAsync(sql, ['storage', 'zfsPurge', 'user']);
    456 					
    457 					try {
    458 						yield Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles();
    459 						ps.alert(
    460 							null,
    461 							Zotero.getString("general.success"),
    462 							"Attachment files from your personal library have been removed from the Zotero servers."
    463 						);
    464 					}
    465 					catch (e) {
    466 						Zotero.logError(e);
    467 						ps.alert(
    468 							null,
    469 							Zotero.getString("general.error"),
    470 							"An error occurred. Please try again later."
    471 						);
    472 					}
    473 				}
    474 			}
    475 		}
    476 		
    477 		this.updateStorageSettingsUI();
    478 	}),
    479 	
    480 	
    481 	verifyStorageServer: Zotero.Promise.coroutine(function* () {
    482 		// onchange weirdly isn't triggered when clicking straight from a field to the button,
    483 		// so we have to trigger this here (and we don't trigger it for Enter in
    484 		// onStorageSettingsKeyPress()).
    485 		yield this.onStorageSettingsChange();
    486 		
    487 		Zotero.debug("Verifying storage");
    488 		
    489 		var verifyButton = document.getElementById("storage-verify");
    490 		var abortButton = document.getElementById("storage-abort");
    491 		var progressMeter = document.getElementById("storage-progress");
    492 		var urlField = document.getElementById("storage-url");
    493 		var usernameField = document.getElementById("storage-username");
    494 		var passwordField = document.getElementById("storage-password");
    495 		
    496 		verifyButton.hidden = true;
    497 		abortButton.hidden = false;
    498 		progressMeter.hidden = false;
    499 		
    500 		var success = false;
    501 		var request = null;
    502 		
    503 		var controller = Zotero.Sync.Runner.getStorageController('webdav');
    504 		
    505 		try {
    506 			yield controller.checkServer({
    507 				// Get the XMLHttpRequest for possible cancelling
    508 				onRequest: r => request = r
    509 			})
    510 			
    511 			success = true;
    512 		}
    513 		catch (e) {
    514 			if (e instanceof controller.VerificationError) {
    515 				switch (e.error) {
    516 				case "NO_URL":
    517 					urlField.focus();
    518 					break;
    519 				
    520 				case "NO_USERNAME":
    521 					usernameField.focus();
    522 					break;
    523 				
    524 				case "NO_PASSWORD":
    525 				case "AUTH_FAILED":
    526 					passwordField.focus();
    527 					break;
    528 				}
    529 			}
    530 			success = yield controller.handleVerificationError(e);
    531 		}
    532 		finally {
    533 			verifyButton.hidden = false;
    534 			abortButton.hidden = true;
    535 			progressMeter.hidden = true;
    536 		}
    537 		
    538 		if (success) {
    539 			Zotero.debug("WebDAV verification succeeded");
    540 			
    541 			Zotero.alert(
    542 				window,
    543 				Zotero.getString('sync.storage.serverConfigurationVerified'),
    544 				Zotero.getString('sync.storage.fileSyncSetUp')
    545 			);
    546 		}
    547 		else {
    548 			Zotero.logError("WebDAV verification failed");
    549 		}
    550 		
    551 		abortButton.onclick = function () {
    552 			if (request) {
    553 				Zotero.debug("Cancelling verification request");
    554 				request.onreadystatechange = undefined;
    555 				request.abort();
    556 				verifyButton.hidden = false;
    557 				abortButton.hidden = true;
    558 				progressMeter.hidden = true;
    559 			}
    560 		}
    561 	}),
    562 	
    563 	
    564 	unverifyStorageServer: function () {
    565 		Zotero.debug("Unverifying storage");
    566 		Zotero.Prefs.set('sync.storage.verified', false);
    567 	},
    568 	
    569 	
    570 	//
    571 	// Reset pane
    572 	//
    573 	initResetPane: function () {
    574 		//
    575 		// Build library selector
    576 		//
    577 		var libraryMenu = document.getElementById('sync-reset-library-menu');
    578 		// Some options need to be disabled when certain libraries are selected
    579 		libraryMenu.onchange = (event) => {
    580 			this.onResetLibraryChange(parseInt(event.target.value));
    581 		}
    582 		this.onResetLibraryChange(Zotero.Libraries.userLibraryID);
    583 		var libraries = Zotero.Libraries.getAll()
    584 			.filter(x => x.libraryType == 'user' || x.libraryType == 'group');
    585 		Zotero.Utilities.Internal.buildLibraryMenuHTML(libraryMenu, libraries);
    586 		// Disable read-only libraries, at least until there are options that make sense for those
    587 		Array.from(libraryMenu.querySelectorAll('option'))
    588 			.filter(x => x.getAttribute('data-editable') == 'false')
    589 			.forEach(x => x.disabled = true);
    590 		
    591 		var list = document.getElementById('sync-reset-list');
    592 		for (let li of document.querySelectorAll('#sync-reset-list li')) {
    593 			li.addEventListener('click', function (event) {
    594 				// Ignore clicks if disabled
    595 				if (this.hasAttribute('disabled')) {
    596 					event.stopPropagation();
    597 					return;
    598 				}
    599 				document.getElementById('sync-reset-button').disabled = false;
    600 			});
    601 		}
    602 	},
    603 	
    604 	
    605 	onResetLibraryChange: function (libraryID) {
    606 		var library = Zotero.Libraries.get(libraryID);
    607 		var section = document.getElementById('reset-file-sync-history');
    608 		var input = section.querySelector('input');
    609 		if (library.filesEditable) {
    610 			section.removeAttribute('disabled');
    611 			input.disabled = false;
    612 		}
    613 		else {
    614 			section.setAttribute('disabled', '');
    615 			// If radio we're disabling is already selected, select the first one in the list
    616 			// instead
    617 			if (input.checked) {
    618 				document.querySelector('#sync-reset-list li:first-child input').checked = true;
    619 			}
    620 			input.disabled = true;
    621 		}
    622 	},
    623 	
    624 	
    625 	reset: async function () {
    626 		var ps = Services.prompt;
    627 		
    628 		if (Zotero.Sync.Runner.syncInProgress) {
    629 			Zotero.alert(
    630 				null,
    631 				Zotero.getString('general.error'),
    632 				Zotero.getString('sync.error.syncInProgress')
    633 					+ "\n\n"
    634 					+ Zotero.getString('general.operationInProgress.waitUntilFinishedAndTryAgain')
    635 			);
    636 			return;
    637 		}
    638 		
    639 		var libraryID = parseInt(
    640 			Array.from(document.querySelectorAll('#sync-reset-library-menu option'))
    641 				.filter(x => x.selected)[0]
    642 				.value
    643 		);
    644 		var library = Zotero.Libraries.get(libraryID);
    645 		var action = Array.from(document.querySelectorAll('#sync-reset-list input[name=sync-reset-radiogroup]'))
    646 			.filter(x => x.checked)[0]
    647 			.getAttribute('value');
    648 		
    649 		switch (action) {
    650 			/*case 'full-sync':
    651 				var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    652 					+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    653 					+ ps.BUTTON_POS_1_DEFAULT;
    654 				var index = ps.confirmEx(
    655 					null,
    656 					Zotero.getString('general.warning'),
    657 					// TODO: localize
    658 					"On the next sync, Zotero will compare all local and remote data and merge any "
    659 						+ "data that does not exist in both locations.\n\n"
    660 						+ "This option is not necessary during normal usage and should "
    661 						+ "generally be used only to troubleshoot specific issues as recommended "
    662 						+ "by Zotero support staff.",
    663 					buttonFlags,
    664 					Zotero.getString('general.reset'),
    665 					null, null, null, {}
    666 				);
    667 				
    668 				switch (index) {
    669 				case 0:
    670 					let libraries = Zotero.Libraries.getAll().filter(library => library.syncable);
    671 					await Zotero.DB.executeTransaction(function* () {
    672 						for (let library of libraries) {
    673 							library.libraryVersion = -1;
    674 							yield library.save();
    675 						}
    676 					});
    677 					break;
    678 					
    679 					// Cancel
    680 				case 1:
    681 					return;
    682 				}
    683 				
    684 				break;
    685 			
    686 			case 'restore-from-server':
    687 				var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    688 									+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    689 									+ ps.BUTTON_POS_1_DEFAULT;
    690 				var index = ps.confirmEx(
    691 					null,
    692 					Zotero.getString('general.warning'),
    693 					Zotero.getString('zotero.preferences.sync.reset.restoreFromServer', account),
    694 					buttonFlags,
    695 					Zotero.getString('zotero.preferences.sync.reset.replaceLocalData'),
    696 					null, null, null, {}
    697 				);
    698 				
    699 				switch (index) {
    700 					case 0:
    701 						// TODO: better error handling
    702 						
    703 						// Verify username and password
    704 						var callback = async function () {
    705 							Zotero.Schema.stopRepositoryTimer();
    706 							Zotero.Sync.Runner.clearSyncTimeout();
    707 							
    708 							Zotero.DB.skipBackup = true;
    709 							
    710 							await Zotero.File.putContentsAsync(
    711 								OS.Path.join(Zotero.DataDirectory.dir, 'restore-from-server'),
    712 								''
    713 							);
    714 							
    715 							var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING);
    716 							var index = ps.confirmEx(
    717 								null,
    718 								Zotero.getString('general.restartRequired'),
    719 								Zotero.getString('zotero.preferences.sync.reset.restartToComplete'),
    720 								buttonFlags,
    721 								Zotero.getString('general.restartNow'),
    722 								null, null, null, {}
    723 							);
    724 							
    725 							var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
    726 									.getService(Components.interfaces.nsIAppStartup);
    727 							appStartup.quit(Components.interfaces.nsIAppStartup.eRestart | Components.interfaces.nsIAppStartup.eAttemptQuit);
    728 						};
    729 						
    730 						// TODO: better way of checking for an active session?
    731 						if (Zotero.Sync.Server.sessionIDComponent == 'sessionid=') {
    732 							Zotero.Sync.Server.login()
    733 							.then(callback)
    734 							.done();
    735 						}
    736 						else {
    737 							callback();
    738 						}
    739 						break;
    740 					
    741 					// Cancel
    742 					case 1:
    743 						return;
    744 				}
    745 				break;*/
    746 			
    747 			case 'restore-to-server':
    748 				var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    749 					+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    750 					+ ps.BUTTON_POS_1_DEFAULT;
    751 				var index = ps.confirmEx(
    752 					null,
    753 					Zotero.getString('general.warning'),
    754 					Zotero.getString(
    755 						'zotero.preferences.sync.reset.restoreToServer',
    756 						[Zotero.clientName, library.name, ZOTERO_CONFIG.DOMAIN_NAME]
    757 					),
    758 					buttonFlags,
    759 					Zotero.getString('zotero.preferences.sync.reset.restoreToServer.button'),
    760 					null, null, null, {}
    761 				);
    762 				
    763 				switch (index) {
    764 					case 0:
    765 						var resetButton = document.getElementById('sync-reset-button');
    766 						resetButton.disabled = true;
    767 						try {
    768 							await Zotero.Sync.Runner.sync({
    769 								libraries: [libraryID],
    770 								resetMode: Zotero.Sync.Runner.RESET_MODE_TO_SERVER
    771 							});
    772 						}
    773 						finally {
    774 							resetButton.disabled = false;
    775 						}
    776 						break;
    777 					
    778 					// Cancel
    779 					case 1:
    780 						return;
    781 				}
    782 				
    783 				break;
    784 			
    785 			
    786 			case 'reset-file-sync-history':
    787 				var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
    788 					+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL
    789 					+ ps.BUTTON_POS_1_DEFAULT;
    790 				var index = ps.confirmEx(
    791 					null,
    792 					Zotero.getString('general.warning'),
    793 					Zotero.getString(
    794 						'zotero.preferences.sync.reset.fileSyncHistory',
    795 						[Zotero.clientName, library.name]
    796 					),
    797 					buttonFlags,
    798 					Zotero.getString('general.reset'),
    799 					null, null, null, {}
    800 				);
    801 				
    802 				switch (index) {
    803 					case 0:
    804 						await Zotero.Sync.Storage.Local.resetAllSyncStates(libraryID);
    805 						ps.alert(
    806 							null,
    807 							Zotero.getString('general.success'),
    808 							Zotero.getString(
    809 								'zotero.preferences.sync.reset.fileSyncHistory.cleared',
    810 								library.name
    811 							)
    812 						);
    813 						break;
    814 					
    815 					// Cancel
    816 					case 1:
    817 						return;
    818 				}
    819 				
    820 				break;
    821 			
    822 			default:
    823 				throw new Error(`Invalid action '${action}' in handleSyncReset()`);
    824 		}
    825 	}
    826 };