/* ***** BEGIN LICENSE BLOCK ***** Copyright © 2008–2013 Center for History and New Media George Mason University, Fairfax, Virginia, USA http://zotero.org This file is part of Zotero. Zotero is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Zotero is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Zotero. If not, see . ***** END LICENSE BLOCK ***** */ "use strict"; Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/osfile.jsm"); Zotero_Preferences.Sync = { init: Zotero.Promise.coroutine(function* () { this.updateStorageSettingsUI(); var username = Zotero.Users.getCurrentUsername() || Zotero.Prefs.get('sync.server.username') || " "; var apiKey = yield Zotero.Sync.Data.Local.getAPIKey(); this.displayFields(apiKey ? username : ""); var pass = Zotero.Sync.Runner.getStorageController('webdav').password; if (pass) { document.getElementById('storage-password').value = pass; } if (apiKey) { try { var keyInfo = yield Zotero.Sync.Runner.checkAccess( Zotero.Sync.Runner.getAPIClient({apiKey}), {timeout: 5000} ); this.displayFields(keyInfo.username); Zotero.Users.setCurrentUsername(keyInfo.username); } catch (e) { // API key wrong/invalid if (!(e instanceof Zotero.HTTP.UnexpectedStatusException) && !(e instanceof Zotero.HTTP.TimeoutException) && !(e instanceof Zotero.HTTP.BrowserOfflineException)) { Zotero.alert( window, Zotero.getString('general.error'), Zotero.getString('sync.error.apiKeyInvalid', Zotero.clientName) ); this.unlinkAccount(false); } else { throw e; } } } }), displayFields: function (username) { document.getElementById('sync-unauthorized').hidden = !!username; document.getElementById('sync-authorized').hidden = !username; document.getElementById('sync-reset-tab').disabled = !username; document.getElementById('sync-username').value = username; document.getElementById('sync-password').value = ''; document.getElementById('sync-username-textbox').value = Zotero.Prefs.get('sync.server.username'); var img = document.getElementById('sync-status-indicator'); img.removeAttribute('verified'); img.removeAttribute('animated'); window.sizeToContent(); }, credentialsKeyPress: function (event) { var username = document.getElementById('sync-username-textbox'); var password = document.getElementById('sync-password'); var syncAuthButton = document.getElementById('sync-auth-button'); syncAuthButton.setAttribute('disabled', 'true'); // When using backspace, the value is not updated until after the keypress event setTimeout(function() { if (username.value.length && password.value.length) { syncAuthButton.setAttribute('disabled', 'false'); } }); if (event.keyCode == 13) { Zotero_Preferences.Sync.linkAccount(event); event.preventDefault(); } }, trimUsername: function () { var tb = document.getElementById('sync-username-textbox'); var username = tb.value; var trimmed = username.trim(); if (username != trimmed) { tb.value = trimmed; // Setting .value alone doesn't seem to cause the pref to sync, so set it manually Zotero.Prefs.set('sync.server.username', trimmed); } }, linkAccount: Zotero.Promise.coroutine(function* (event) { this.trimUsername(); var username = document.getElementById('sync-username-textbox').value; var password = document.getElementById('sync-password').value; if (!username.length || !password.length) { this.updateSyncIndicator(); return; } // Try to acquire API key with current credentials this.updateSyncIndicator('animated'); try { var json = yield Zotero.Sync.Runner.createAPIKeyFromCredentials(username, password); } catch (e) { setTimeout(function () { Zotero.alert( window, Zotero.getString('general.error'), e.message ); }); throw e; } finally { this.updateSyncIndicator(); } // Invalid credentials if (!json) { Zotero.alert(window, Zotero.getString('general.error'), Zotero.getString('sync.error.invalidLogin') ); return; } if (!(yield Zotero.Sync.Data.Local.checkUser(window, json.userID, json.username))) { // createAPIKeyFromCredentials will have created an API key, // but user decided not to use it, so we remove it here. Zotero.Sync.Runner.deleteAPIKey(); return; } this.displayFields(json.username); }), /** * Updates the auth indicator icon, depending on status * @param {string} status */ updateSyncIndicator: function (status) { var img = document.getElementById('sync-status-indicator'); img.removeAttribute('animated'); if (status == 'animated') { img.setAttribute('animated', true); } }, unlinkAccount: Zotero.Promise.coroutine(function* (showAlert=true) { if (showAlert) { var check = {value: false}; var ps = Services.prompt; var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL); var index = ps.confirmEx( null, Zotero.getString('general.warning'), Zotero.getString('account.unlinkWarning', Zotero.clientName), buttonFlags, Zotero.getString('account.unlinkWarning.button'), null, null, Zotero.getString('account.unlinkWarning.removeData', Zotero.clientName), check ); if (index == 0) { if (check.value) { var resetDataDirFile = OS.Path.join(Zotero.DataDirectory.dir, 'reset-data-directory'); yield Zotero.File.putContentsAsync(resetDataDirFile, ''); yield Zotero.Sync.Runner.deleteAPIKey(); Zotero.Prefs.clear('sync.server.username'); return Zotero.Utilities.Internal.quitZotero(true); } } else { return; } } this.displayFields(); Zotero.Prefs.clear('sync.librariesToSync'); yield Zotero.Sync.Runner.deleteAPIKey(); }), showLibrariesToSyncDialog: function() { var io = {}; window.openDialog('chrome://zotero/content/preferences/librariesToSync.xul', "zotero-preferences-librariesToSyncDialog", "chrome,modal,centerscreen", io); }, dblClickLibraryToSync: function (event) { var tree = document.getElementById("libraries-to-sync-tree"); var row = {}, col = {}, child = {}; tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child); if (col.value.element.id == 'libraries-to-sync-checked') { return; } // if dblclicked anywhere but the checkbox update pref return this.toggleLibraryToSync(row.value); }, clickLibraryToSync: function (event) { var tree = document.getElementById("libraries-to-sync-tree"); var row = {}, col = {}, child = {}; tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, child); if (col.value.element.id != 'libraries-to-sync-checked') { return; } // if clicked on checkbox update pref return this.toggleLibraryToSync(row.value); }, toggleLibraryToSync: function (index) { var treechildren = document.getElementById('libraries-to-sync-rows'); if (index >= treechildren.childNodes.length) { return; } var row = treechildren.childNodes[index]; var val = row.firstChild.childNodes[1].getAttribute('value'); if (!val) { return } var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]'); var indexOfId = librariesToSkip.indexOf(val); if (indexOfId == -1) { librariesToSkip.push(val); } else { librariesToSkip.splice(indexOfId, 1); } Zotero.Prefs.set('sync.librariesToSkip', JSON.stringify(librariesToSkip)); var cell = row.firstChild.firstChild; cell.setAttribute('value', indexOfId != -1); }, initLibrariesToSync: Zotero.Promise.coroutine(function* () { var tree = document.getElementById("libraries-to-sync-tree"); var treechildren = document.getElementById('libraries-to-sync-rows'); while (treechildren.hasChildNodes()) { treechildren.removeChild(treechildren.firstChild); } function addRow(libraryName, id, checked=false, editable=true) { var treeitem = document.createElement('treeitem'); var treerow = document.createElement('treerow'); var checkboxCell = document.createElement('treecell'); var nameCell = document.createElement('treecell'); nameCell.setAttribute('label', libraryName); nameCell.setAttribute('value', id); nameCell.setAttribute('editable', false); checkboxCell.setAttribute('value', checked); checkboxCell.setAttribute('editable', editable); treerow.appendChild(checkboxCell); treerow.appendChild(nameCell); treeitem.appendChild(treerow); treechildren.appendChild(treeitem); } // Add loading row while we're loading a group list var loadingLabel = Zotero.getString("zotero.preferences.sync.librariesToSync.loadingLibraries"); addRow(loadingLabel, "loading", false, false); var apiKey = yield Zotero.Sync.Data.Local.getAPIKey(); var client = Zotero.Sync.Runner.getAPIClient({apiKey}); var groups = []; try { // Load up remote groups var keyInfo = yield Zotero.Sync.Runner.checkAccess(client, {timeout: 5000}); groups = yield client.getGroups(keyInfo.userID); } catch (e) { // Connection problems if ((e instanceof Zotero.HTTP.UnexpectedStatusException) || (e instanceof Zotero.HTTP.TimeoutException) || (e instanceof Zotero.HTTP.BrowserOfflineException)) { Zotero.alert( window, Zotero.getString('general.error'), Zotero.getString('sync.error.checkConnection', Zotero.clientName) ); } else { throw e; } document.getElementsByTagName('dialog')[0].acceptDialog(); } // Remove the loading row treechildren.removeChild(treechildren.firstChild); var librariesToSkip = JSON.parse(Zotero.Prefs.get('sync.librariesToSkip') || '[]'); // Add default rows addRow(Zotero.getString("pane.collections.libraryAndFeeds"), "L" + Zotero.Libraries.userLibraryID, librariesToSkip.indexOf("L" + Zotero.Libraries.userLibraryID) == -1); // Sort groups var collation = Zotero.getLocaleCollation(); groups.sort((a, b) => collation.compareString(1, a.data.name, b.data.name)); // Add group rows for (let group of groups) { addRow(group.data.name, "G" + group.id, librariesToSkip.indexOf("G" + group.id) == -1); } }), updateStorageSettingsUI: Zotero.Promise.coroutine(function* () { this.unverifyStorageServer(); var protocol = document.getElementById('pref-storage-protocol').value; var enabled = document.getElementById('pref-storage-enabled').value; var storageSettings = document.getElementById('storage-settings'); var protocolMenu = document.getElementById('storage-protocol'); var settings = document.getElementById('storage-webdav-settings'); var sep = document.getElementById('storage-separator'); if (!enabled || protocol == 'zotero') { settings.hidden = true; sep.hidden = false; } else { settings.hidden = false; sep.hidden = true; } var menulists = document.querySelectorAll('#storage-settings menulist.storage-mode'); for (let menulist of menulists) { menulist.disabled = !enabled; } this.updateStorageTerms(); window.sizeToContent(); }), updateStorageSettingsGroups: function (enabled) { var storageSettings = document.getElementById('storage-settings'); var menulists = storageSettings.getElementsByTagName('menulist'); for (let menulist of menulists) { if (menulist.className == 'storage-groups') { menulist.disabled = !enabled; } } var self = this; setTimeout(function () { self.updateStorageTerms(); }, 1) }, updateStorageTerms: function () { var terms = document.getElementById('storage-terms'); var libraryEnabled = document.getElementById('pref-storage-enabled').value; var storageProtocol = document.getElementById('pref-storage-protocol').value; var groupsEnabled = document.getElementById('pref-group-storage-enabled').value; terms.hidden = !((libraryEnabled && storageProtocol == 'zotero') || groupsEnabled); }, onStorageSettingsKeyPress: Zotero.Promise.coroutine(function* (event) { if (event.keyCode == 13) { yield this.onStorageSettingsChange(); yield this.verifyStorageServer(); } }), onStorageSettingsChange: Zotero.Promise.coroutine(function* () { // Clean URL var urlPref = document.getElementById('pref-storage-url'); urlPref.value = urlPref.value.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, ''); var oldProtocol = document.getElementById('pref-storage-protocol').value; var oldEnabled = document.getElementById('pref-storage-enabled').value; yield Zotero.Promise.delay(1); var newProtocol = document.getElementById('pref-storage-protocol').value; var newEnabled = document.getElementById('pref-storage-enabled').value; if (oldProtocol != newProtocol) { yield Zotero.Sync.Storage.Local.resetAllSyncStates(); } if (oldProtocol == 'webdav') { this.unverifyStorageServer(); Zotero.Sync.Runner.resetStorageController(oldProtocol); var username = document.getElementById('storage-username').value; var password = document.getElementById('storage-password').value; if (username) { Zotero.Sync.Runner.getStorageController('webdav').password = password; } } if (oldProtocol == 'zotero' && newProtocol == 'webdav') { var sql = "SELECT COUNT(*) FROM settings " + "WHERE setting='storage' AND key='zfsPurge' AND value='user'"; if (!Zotero.DB.valueQueryAsync(sql)) { var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING) + ps.BUTTON_DELAY_ENABLE; var account = Zotero.Sync.Server.username; var index = ps.confirmEx( null, Zotero.getString('zotero.preferences.sync.purgeStorage.title'), Zotero.getString('zotero.preferences.sync.purgeStorage.desc'), buttonFlags, Zotero.getString('zotero.preferences.sync.purgeStorage.confirmButton'), Zotero.getString('zotero.preferences.sync.purgeStorage.cancelButton'), null, null, {} ); if (index == 0) { var sql = "INSERT OR IGNORE INTO settings VALUES (?,?,?)"; yield Zotero.DB.queryAsync(sql, ['storage', 'zfsPurge', 'user']); try { yield Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles(); ps.alert( null, Zotero.getString("general.success"), "Attachment files from your personal library have been removed from the Zotero servers." ); } catch (e) { Zotero.logError(e); ps.alert( null, Zotero.getString("general.error"), "An error occurred. Please try again later." ); } } } } this.updateStorageSettingsUI(); }), verifyStorageServer: Zotero.Promise.coroutine(function* () { Zotero.debug("Verifying storage"); var verifyButton = document.getElementById("storage-verify"); var abortButton = document.getElementById("storage-abort"); var progressMeter = document.getElementById("storage-progress"); var urlField = document.getElementById("storage-url"); var usernameField = document.getElementById("storage-username"); var passwordField = document.getElementById("storage-password"); verifyButton.hidden = true; abortButton.hidden = false; progressMeter.hidden = false; var success = false; var request = null; var controller = Zotero.Sync.Runner.getStorageController('webdav'); try { yield controller.checkServer({ // Get the XMLHttpRequest for possible cancelling onRequest: r => request = r }) success = true; } catch (e) { if (e instanceof controller.VerificationError) { switch (e.error) { case "NO_URL": urlField.focus(); break; case "NO_USERNAME": usernameField.focus(); break; case "NO_PASSWORD": case "AUTH_FAILED": passwordField.focus(); break; } } success = yield controller.handleVerificationError(e); } finally { verifyButton.hidden = false; abortButton.hidden = true; progressMeter.hidden = true; } if (success) { Zotero.debug("WebDAV verification succeeded"); Zotero.alert( window, Zotero.getString('sync.storage.serverConfigurationVerified'), Zotero.getString('sync.storage.fileSyncSetUp') ); } else { Zotero.logError("WebDAV verification failed"); } abortButton.onclick = function () { if (request) { Zotero.debug("Cancelling verification request"); request.onreadystatechange = undefined; request.abort(); verifyButton.hidden = false; abortButton.hidden = true; progressMeter.hidden = true; } } }), unverifyStorageServer: function () { Zotero.debug("Unverifying storage"); Zotero.Prefs.set('sync.storage.verified', false); }, handleSyncResetSelect: function (obj) { var index = obj.selectedIndex; var rows = obj.getElementsByTagName('row'); for (var i=0; i library.syncable); yield Zotero.DB.executeTransaction(function* () { for (let library of libraries) { library.libraryVersion = -1; yield library.save(); } }); break; // Cancel case 1: return; } break; case 'restore-from-server': var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) + ps.BUTTON_POS_1_DEFAULT; var index = ps.confirmEx( null, Zotero.getString('general.warning'), Zotero.getString('zotero.preferences.sync.reset.restoreFromServer', account), buttonFlags, Zotero.getString('zotero.preferences.sync.reset.replaceLocalData'), null, null, null, {} ); switch (index) { case 0: // TODO: better error handling // Verify username and password var callback = Zotero.Promise.coroutine(function* () { Zotero.Schema.stopRepositoryTimer(); Zotero.Sync.Runner.clearSyncTimeout(); Zotero.DB.skipBackup = true; yield Zotero.File.putContentsAsync( OS.Path.join(Zotero.DataDirectory.dir, 'restore-from-server'), '' ); var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING); var index = ps.confirmEx( null, Zotero.getString('general.restartRequired'), Zotero.getString('zotero.preferences.sync.reset.restartToComplete'), buttonFlags, Zotero.getString('general.restartNow'), null, null, null, {} ); var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"] .getService(Components.interfaces.nsIAppStartup); appStartup.quit(Components.interfaces.nsIAppStartup.eRestart | Components.interfaces.nsIAppStartup.eAttemptQuit); }); // TODO: better way of checking for an active session? if (Zotero.Sync.Server.sessionIDComponent == 'sessionid=') { Zotero.Sync.Server.login() .then(callback) .done(); } else { callback(); } break; // Cancel case 1: return; } break; case 'restore-to-server': var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) + ps.BUTTON_POS_1_DEFAULT; var index = ps.confirmEx( null, Zotero.getString('general.warning'), Zotero.getString('zotero.preferences.sync.reset.restoreToServer', account), buttonFlags, Zotero.getString('zotero.preferences.sync.reset.replaceServerData'), null, null, null, {} ); switch (index) { case 0: // TODO: better error handling Zotero.Sync.Server.clear(function () { Zotero.Sync.Server.sync(/*{ // TODO: this doesn't work if the pref window is closed. fix, perhaps by making original callbacks available to the custom callbacks onSuccess: function () { Zotero.Sync.Runner.updateIcons(); ps.alert( null, "Restore Completed", "Data on the Zotero server has been successfully restored." ); }, onError: function (msg) { // TODO: combine with error dialog for regular syncs ps.alert( null, "Restore Failed", "An error occurred uploading your data to the server.\n\n" + "Click the sync error icon in the Zotero toolbar " + "for further information." ); Zotero.Sync.Runner.error(msg); } }*/); }); break; // Cancel case 1: return; } break; case 'reset-storage-history': var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) + ps.BUTTON_POS_1_DEFAULT; var index = ps.confirmEx( null, Zotero.getString('general.warning'), Zotero.getString('zotero.preferences.sync.reset.fileSyncHistory', Zotero.clientName), buttonFlags, Zotero.getString('general.reset'), null, null, null, {} ); switch (index) { case 0: yield Zotero.Sync.Storage.Local.resetAllSyncStates(); ps.alert( null, "File Sync History Cleared", "The file sync history has been cleared." ); break; // Cancel case 1: return; } break; default: throw ("Invalid action '" + action + "' in handleSyncReset()"); } }) };