2015-07-20 21:27:55 +00:00
|
|
|
/*
|
|
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
|
|
|
|
Copyright © 2009 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
***** END LICENSE BLOCK *****
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
if (!Zotero.Sync) {
|
|
|
|
Zotero.Sync = {};
|
|
|
|
}
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
// Initialized as Zotero.Sync.Runner in zotero.js
|
|
|
|
Zotero.Sync.Runner_Module = function (options = {}) {
|
2016-02-29 09:23:00 +00:00
|
|
|
const stopOnError = false;
|
2021-02-09 21:36:06 +00:00
|
|
|
const HTML_NS = 'http://www.w3.org/1999/xhtml';
|
2015-10-29 07:41:54 +00:00
|
|
|
|
2016-09-05 07:59:42 +00:00
|
|
|
Zotero.defineProperty(this, 'enabled', {
|
|
|
|
get: () => {
|
2016-09-21 04:46:28 +00:00
|
|
|
return _apiKey || Zotero.Sync.Data.Local.hasCredentials();
|
2016-09-05 07:59:42 +00:00
|
|
|
}
|
|
|
|
});
|
2016-04-01 06:24:50 +00:00
|
|
|
Zotero.defineProperty(this, 'syncInProgress', { get: () => _syncInProgress });
|
2015-07-20 21:27:55 +00:00
|
|
|
Zotero.defineProperty(this, 'lastSyncStatus', { get: () => _lastSyncStatus });
|
|
|
|
|
2017-12-08 05:27:29 +00:00
|
|
|
Zotero.defineProperty(this, 'RESET_MODE_FROM_SERVER', { value: 1 });
|
|
|
|
Zotero.defineProperty(this, 'RESET_MODE_TO_SERVER', { value: 2 });
|
|
|
|
|
2016-12-21 10:52:55 +00:00
|
|
|
Zotero.defineProperty(this, 'baseURL', {
|
|
|
|
get: () => {
|
|
|
|
let url = options.baseURL || Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL;
|
|
|
|
if (!url.endsWith('/')) {
|
|
|
|
url += '/';
|
|
|
|
}
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
});
|
2015-10-29 07:41:54 +00:00
|
|
|
this.apiVersion = options.apiVersion || ZOTERO_CONFIG.API_VERSION;
|
2015-11-02 08:22:37 +00:00
|
|
|
|
|
|
|
// Allows tests to set apiKey in options or as property, overriding login manager
|
|
|
|
var _apiKey = options.apiKey;
|
|
|
|
Zotero.defineProperty(this, 'apiKey', { set: val => _apiKey = val });
|
2015-10-29 07:41:54 +00:00
|
|
|
|
|
|
|
Components.utils.import("resource://zotero/concurrentCaller.js");
|
2017-04-27 08:40:26 +00:00
|
|
|
this.caller = new ConcurrentCaller({
|
|
|
|
numConcurrent: 4,
|
2017-04-27 08:46:35 +00:00
|
|
|
stopOnError,
|
2017-04-27 08:40:26 +00:00
|
|
|
logger: msg => Zotero.debug(msg),
|
2019-01-22 11:48:11 +00:00
|
|
|
onError: e => Zotero.logError(e),
|
|
|
|
Promise: Zotero.Promise
|
2017-04-27 08:40:26 +00:00
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
var _enabled = false;
|
2015-07-20 21:27:55 +00:00
|
|
|
var _autoSyncTimer;
|
2018-03-25 08:56:50 +00:00
|
|
|
var _delaySyncUntil;
|
2018-05-04 23:53:54 +00:00
|
|
|
var _delayPromises = [];
|
2015-07-20 21:27:55 +00:00
|
|
|
var _firstInSession = true;
|
|
|
|
var _syncInProgress = false;
|
2021-04-06 08:57:19 +00:00
|
|
|
var _queuedSyncOptions = [];
|
2017-07-07 09:18:23 +00:00
|
|
|
var _stopping = false;
|
2019-09-16 05:08:18 +00:00
|
|
|
var _canceller;
|
2016-04-01 06:24:50 +00:00
|
|
|
var _manualSyncRequired = false; // TODO: make public?
|
2015-07-20 21:27:55 +00:00
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
var _currentEngine = null;
|
2015-12-23 09:52:09 +00:00
|
|
|
var _storageControllers = {};
|
2015-10-29 07:41:54 +00:00
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
var _lastSyncStatus;
|
|
|
|
var _currentSyncStatusLabel;
|
|
|
|
var _currentLastSyncLabel;
|
2021-02-09 21:36:06 +00:00
|
|
|
var _currentTooltipMessages;
|
2015-07-20 21:27:55 +00:00
|
|
|
var _errors = [];
|
2021-02-09 21:36:06 +00:00
|
|
|
var _tooltipMessages = [];
|
2015-07-20 21:27:55 +00:00
|
|
|
|
2017-08-06 15:50:26 +00:00
|
|
|
Zotero.addShutdownListener(() => this.stop());
|
|
|
|
|
2015-11-02 08:22:37 +00:00
|
|
|
this.getAPIClient = function (options = {}) {
|
2015-10-29 07:41:54 +00:00
|
|
|
return new Zotero.Sync.APIClient({
|
|
|
|
baseURL: this.baseURL,
|
|
|
|
apiVersion: this.apiVersion,
|
2020-09-08 08:12:07 +00:00
|
|
|
schemaVersion: this.globalSchemaVersion,
|
2015-11-02 08:22:37 +00:00
|
|
|
apiKey: options.apiKey,
|
2019-09-16 05:08:18 +00:00
|
|
|
caller: this.caller,
|
|
|
|
cancellerReceiver: _cancellerReceiver,
|
2015-10-29 07:41:54 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
/**
|
|
|
|
* Begin a sync session
|
|
|
|
*
|
2015-10-29 07:41:54 +00:00
|
|
|
* @param {Object} [options]
|
|
|
|
* @param {Boolean} [options.background=false] Whether this is a background request, which
|
|
|
|
* prevents some alerts from being shown
|
2016-06-27 20:42:55 +00:00
|
|
|
* @param {Integer[]} [options.libraries] IDs of libraries to sync; skipped libraries must
|
|
|
|
* be removed if unwanted
|
2015-10-29 07:41:54 +00:00
|
|
|
* @param {Function} [options.onError] Function to pass errors to instead of
|
|
|
|
* handling internally (used for testing)
|
2015-07-20 21:27:55 +00:00
|
|
|
*/
|
2017-01-03 09:40:18 +00:00
|
|
|
this.sync = Zotero.serial(function (options = {}) {
|
|
|
|
return this._sync(options);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this._sync = Zotero.Promise.coroutine(function* (options) {
|
2015-07-20 21:27:55 +00:00
|
|
|
// Clear message list
|
|
|
|
_errors = [];
|
2021-02-09 21:36:06 +00:00
|
|
|
_tooltipMessages = [];
|
2015-07-20 21:27:55 +00:00
|
|
|
|
2017-01-03 09:40:18 +00:00
|
|
|
// Shouldn't be possible because of serial()
|
2015-07-20 21:27:55 +00:00
|
|
|
if (_syncInProgress) {
|
|
|
|
let msg = Zotero.getString('sync.error.syncInProgress');
|
|
|
|
let e = new Zotero.Error(msg, 0, { dialogButtonText: null, frontWindowOnly: true });
|
|
|
|
this.updateIcons(e);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
_syncInProgress = true;
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopping = false;
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
try {
|
2017-07-11 06:42:00 +00:00
|
|
|
yield Zotero.Notifier.trigger('start', 'sync', []);
|
|
|
|
|
2016-04-23 07:32:32 +00:00
|
|
|
let apiKey = yield _getAPIKey();
|
|
|
|
if (!apiKey) {
|
2016-04-25 07:12:11 +00:00
|
|
|
throw new Zotero.Error("API key not set", Zotero.Error.ERROR_API_KEY_NOT_SET);
|
2016-04-23 07:32:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (_firstInSession) {
|
|
|
|
options.firstInSession = true;
|
|
|
|
_firstInSession = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.updateIcons('animate');
|
2016-05-12 06:21:18 +00:00
|
|
|
|
2018-03-25 08:56:50 +00:00
|
|
|
// If a delay is set (e.g., from the connector target selector), wait to sync
|
|
|
|
while (_delaySyncUntil && new Date() < _delaySyncUntil) {
|
|
|
|
this.setSyncStatus(Zotero.getString('sync.status.waiting'));
|
|
|
|
let delay = _delaySyncUntil - new Date();
|
|
|
|
Zotero.debug(`Waiting ${delay} ms to sync`);
|
|
|
|
yield Zotero.Promise.delay(delay);
|
|
|
|
}
|
|
|
|
|
2018-05-04 23:53:54 +00:00
|
|
|
// If paused, wait until we're done
|
|
|
|
while (true) {
|
|
|
|
if (_delayPromises.some(p => p.isPending())) {
|
|
|
|
this.setSyncStatus(Zotero.getString('sync.status.waiting'));
|
|
|
|
Zotero.debug("Syncing is paused -- waiting to sync");
|
|
|
|
yield Zotero.Promise.all(_delayPromises);
|
|
|
|
// If more were added, continue
|
|
|
|
if (_delayPromises.some(p => p.isPending())) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
_delayPromises = [];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-02-08 08:06:06 +00:00
|
|
|
// purgeDataObjects() starts a transaction, so if there's an active one then show a
|
|
|
|
// nice message and wait until there's not. Another transaction could still start
|
|
|
|
// before purgeDataObjects() and result in a wait timeout, but this should reduce the
|
|
|
|
// frequency of that.
|
|
|
|
while (Zotero.DB.inTransaction()) {
|
|
|
|
this.setSyncStatus(Zotero.getString('sync.status.waiting'));
|
|
|
|
Zotero.debug("Transaction in progress -- waiting to sync");
|
|
|
|
yield Zotero.DB.waitForTransaction('sync');
|
|
|
|
_stopCheck();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setSyncStatus(Zotero.getString('sync.status.preparing'));
|
|
|
|
|
2018-01-15 08:22:00 +00:00
|
|
|
// Purge deleted objects so they don't cause sync errors (e.g., long tags)
|
|
|
|
yield Zotero.purgeDataObjects(true);
|
|
|
|
|
2015-11-02 08:22:37 +00:00
|
|
|
let client = this.getAPIClient({ apiKey });
|
2015-10-29 07:41:54 +00:00
|
|
|
let keyInfo = yield this.checkAccess(client, options);
|
2015-12-23 09:56:47 +00:00
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2015-12-23 09:56:47 +00:00
|
|
|
let emptyLibraryContinue = yield this.checkEmptyLibrary(keyInfo);
|
|
|
|
if (!emptyLibraryContinue) {
|
|
|
|
Zotero.debug("Syncing cancelled because user library is empty");
|
2015-07-20 21:27:55 +00:00
|
|
|
return false;
|
|
|
|
}
|
2016-04-26 20:27:49 +00:00
|
|
|
|
|
|
|
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
let lastWin = wm.getMostRecentWindow("navigator:browser");
|
2022-02-19 17:32:33 +00:00
|
|
|
let ok = yield Zotero.Sync.Data.Local.checkUser(
|
|
|
|
lastWin,
|
|
|
|
keyInfo.userID,
|
|
|
|
keyInfo.username,
|
|
|
|
keyInfo.displayName
|
|
|
|
);
|
|
|
|
if (!ok) {
|
2016-04-26 20:27:49 +00:00
|
|
|
Zotero.debug("User cancelled sync on username mismatch");
|
|
|
|
return false;
|
2015-12-23 09:56:47 +00:00
|
|
|
}
|
2016-04-26 20:27:49 +00:00
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
let engineOptions = {
|
2018-02-23 22:59:32 +00:00
|
|
|
userID: keyInfo.userID,
|
2015-10-29 07:41:54 +00:00
|
|
|
apiClient: client,
|
|
|
|
caller: this.caller,
|
|
|
|
setStatus: this.setSyncStatus.bind(this),
|
|
|
|
stopOnError,
|
|
|
|
onError: function (e) {
|
2019-10-14 05:27:59 +00:00
|
|
|
// Ignore cancelled requests
|
|
|
|
if (e instanceof Zotero.HTTP.CancelledException) {
|
|
|
|
Zotero.debug("Request was cancelled");
|
|
|
|
return;
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
if (options.onError) {
|
|
|
|
options.onError(e);
|
|
|
|
}
|
|
|
|
else {
|
2016-02-25 09:51:55 +00:00
|
|
|
this.addError(e);
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
2015-10-29 07:41:54 +00:00
|
|
|
}.bind(this),
|
2016-04-01 06:24:50 +00:00
|
|
|
background: !!options.background,
|
2017-12-08 05:27:29 +00:00
|
|
|
firstInSession: _firstInSession,
|
|
|
|
resetMode: options.resetMode
|
2015-10-29 07:41:54 +00:00
|
|
|
};
|
|
|
|
|
2016-06-22 07:24:22 +00:00
|
|
|
var librariesToSync = options.libraries = yield this.checkLibraries(
|
2016-04-01 06:24:50 +00:00
|
|
|
client,
|
|
|
|
options,
|
|
|
|
keyInfo,
|
|
|
|
options.libraries ? Array.from(options.libraries) : []
|
2015-10-29 07:41:54 +00:00
|
|
|
);
|
2016-04-10 22:58:01 +00:00
|
|
|
|
2021-04-06 08:57:19 +00:00
|
|
|
// If file and full-text libraries are specified, limit to libraries we're already
|
|
|
|
// syncing
|
|
|
|
var fileLibrariesToSync = new Set(
|
|
|
|
options.fileLibraries
|
|
|
|
? options.fileLibraries.filter(id => librariesToSync.includes(id))
|
|
|
|
: librariesToSync
|
|
|
|
);
|
|
|
|
var fullTextLibrariesToSync = new Set(
|
|
|
|
options.fullTextLibraries
|
|
|
|
? options.fullTextLibraries.filter(id => librariesToSync.includes(id))
|
|
|
|
: librariesToSync
|
|
|
|
);
|
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2016-04-10 22:58:01 +00:00
|
|
|
// If items not yet loaded for libraries we need, load them now
|
|
|
|
for (let libraryID of librariesToSync) {
|
|
|
|
let library = Zotero.Libraries.get(libraryID);
|
|
|
|
if (!library.getDataLoaded('item')) {
|
|
|
|
yield library.waitForDataLoad('item');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2015-11-12 07:54:51 +00:00
|
|
|
// Sync data and files, and then repeat if necessary
|
2015-10-29 07:41:54 +00:00
|
|
|
let attempt = 1;
|
2016-05-02 17:13:19 +00:00
|
|
|
let successfulLibraries = new Set(librariesToSync);
|
|
|
|
while (librariesToSync.length) {
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
if (attempt > 3) {
|
2016-05-02 17:13:19 +00:00
|
|
|
// TODO: Back off and/or nicer error
|
2015-10-29 07:41:54 +00:00
|
|
|
throw new Error("Too many sync attempts -- stopping");
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
2016-05-02 17:13:19 +00:00
|
|
|
let nextLibraries = yield _doDataSync(librariesToSync, engineOptions);
|
|
|
|
// Remove failed libraries from the successful set
|
|
|
|
Zotero.Utilities.arrayDiff(librariesToSync, nextLibraries).forEach(libraryID => {
|
|
|
|
successfulLibraries.delete(libraryID);
|
|
|
|
});
|
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2021-04-06 08:57:19 +00:00
|
|
|
// Run file sync on all allowed libraries that passed the last data sync
|
|
|
|
librariesToSync = yield _doFileSync(
|
|
|
|
nextLibraries.filter(libraryID => fileLibrariesToSync.has(libraryID)),
|
|
|
|
engineOptions
|
|
|
|
);
|
2016-05-02 17:13:19 +00:00
|
|
|
if (librariesToSync.length) {
|
|
|
|
attempt++;
|
|
|
|
continue;
|
2015-11-12 07:54:51 +00:00
|
|
|
}
|
2016-05-02 17:13:19 +00:00
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
|
|
|
|
2021-04-06 08:57:19 +00:00
|
|
|
// Run full-text sync on all allowed libraries that haven't failed a data sync
|
|
|
|
librariesToSync = yield _doFullTextSync(
|
|
|
|
[...successfulLibraries].filter(libraryID => fullTextLibrariesToSync.has(libraryID)),
|
|
|
|
engineOptions
|
|
|
|
);
|
2016-05-02 17:13:19 +00:00
|
|
|
if (librariesToSync.length) {
|
|
|
|
attempt++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
2015-11-12 07:54:51 +00:00
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
catch (e) {
|
2016-05-06 08:59:04 +00:00
|
|
|
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
|
|
|
|
let msg = Zotero.getString('general.browserIsOffline', Zotero.appName);
|
|
|
|
e = new Zotero.Error(msg, 0, { dialogButtonText: null })
|
|
|
|
Zotero.logError(e);
|
|
|
|
_errors = [];
|
|
|
|
}
|
|
|
|
|
2019-09-16 05:08:18 +00:00
|
|
|
if (e instanceof Zotero.Sync.UserCancelledException
|
|
|
|
|| e instanceof Zotero.HTTP.CancelledException) {
|
2016-05-06 07:08:22 +00:00
|
|
|
Zotero.debug("Sync was cancelled");
|
|
|
|
}
|
|
|
|
else if (options.onError) {
|
2015-07-20 21:27:55 +00:00
|
|
|
options.onError(e);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.addError(e);
|
|
|
|
}
|
|
|
|
}
|
2015-10-29 07:41:54 +00:00
|
|
|
finally {
|
2016-04-21 15:08:48 +00:00
|
|
|
yield this.end(options);
|
|
|
|
|
|
|
|
if (options.restartSync) {
|
|
|
|
delete options.restartSync;
|
|
|
|
Zotero.debug("Restarting sync");
|
2017-01-03 09:40:18 +00:00
|
|
|
yield this._sync(options);
|
2016-04-21 15:08:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-04-06 08:57:19 +00:00
|
|
|
// If an auto-sync was queued while a sync was ongoing, start again with its options
|
|
|
|
else if (_queuedSyncOptions.length) {
|
|
|
|
Zotero.debug("Restarting sync");
|
2021-05-14 07:16:44 +00:00
|
|
|
yield this._sync(JSON.parse(_queuedSyncOptions.shift()));
|
2021-04-06 08:57:19 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-04-21 15:08:48 +00:00
|
|
|
|
|
|
|
Zotero.debug("Done syncing");
|
2016-06-23 06:57:57 +00:00
|
|
|
Zotero.Notifier.trigger('finish', 'sync', librariesToSync || []);
|
2015-10-29 07:41:54 +00:00
|
|
|
}
|
2017-01-03 09:40:18 +00:00
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check key for current user info and return access info
|
|
|
|
*/
|
2015-12-02 16:13:29 +00:00
|
|
|
this.checkAccess = Zotero.Promise.coroutine(function* (client, options={}) {
|
|
|
|
var json = yield client.getKeyInfo(options);
|
2015-07-20 21:27:55 +00:00
|
|
|
Zotero.debug(json);
|
|
|
|
if (!json) {
|
2017-08-04 23:07:49 +00:00
|
|
|
throw new Zotero.Error("API key not set", Zotero.Error.ERROR_API_KEY_INVALID);
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sanity check
|
2015-10-29 07:41:54 +00:00
|
|
|
if (!json.userID) throw new Error("userID not found in key response");
|
|
|
|
if (!json.username) throw new Error("username not found in key response");
|
|
|
|
if (!json.access) throw new Error("'access' not found in key response");
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
return json;
|
|
|
|
});
|
2015-12-23 09:56:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
// Prompt if library empty and there is no userID stored
|
|
|
|
this.checkEmptyLibrary = Zotero.Promise.coroutine(function* (keyInfo) {
|
|
|
|
let library = Zotero.Libraries.userLibrary;
|
2016-02-11 11:02:38 +00:00
|
|
|
let feeds = Zotero.Feeds.getAll();
|
2015-12-23 09:56:47 +00:00
|
|
|
let userID = Zotero.Users.getCurrentUserID();
|
|
|
|
|
|
|
|
if (!userID) {
|
|
|
|
let hasItems = yield library.hasItems();
|
2016-06-27 16:40:38 +00:00
|
|
|
if (!hasItems && feeds.length <= 0 && !Zotero.resetDataDir) {
|
2015-12-23 09:56:47 +00:00
|
|
|
let ps = Services.prompt;
|
|
|
|
let index = ps.confirmEx(
|
|
|
|
null,
|
|
|
|
Zotero.getString('general.warning'),
|
2021-05-15 19:08:30 +00:00
|
|
|
Zotero.getString(
|
|
|
|
'account.warning.emptyLibrary',
|
|
|
|
[Zotero.clientName, OS.Path.basename(Zotero.DB.path)]
|
|
|
|
) + "\n\n"
|
|
|
|
+ Zotero.getString(
|
|
|
|
'account.warning.emptyLibrary.dataWillBeDownloaded',
|
|
|
|
keyInfo.username
|
|
|
|
)
|
|
|
|
+ "\n\n"
|
|
|
|
+ Zotero.getString(
|
|
|
|
'account.warning.existingDataElsewhere',
|
|
|
|
Zotero.clientName
|
|
|
|
)
|
|
|
|
+ "\n\n"
|
|
|
|
+ Zotero.getString('dataDir.location', Zotero.DataDirectory.dir),
|
2015-12-23 09:56:47 +00:00
|
|
|
(ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING)
|
|
|
|
+ (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL)
|
|
|
|
+ (ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING),
|
|
|
|
Zotero.getString('sync.sync'),
|
|
|
|
null,
|
2021-05-15 19:08:30 +00:00
|
|
|
Zotero.getString('general.moreInformation'),
|
2015-12-23 09:56:47 +00:00
|
|
|
null, {}
|
|
|
|
);
|
|
|
|
if (index == 1) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
else if (index == 2) {
|
2021-05-15 19:08:30 +00:00
|
|
|
Zotero.launchURL('https://www.zotero.org/support/zotero_data#locating_missing_zotero_data');
|
2015-12-23 09:56:47 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
|
2016-04-10 22:58:01 +00:00
|
|
|
/**
|
|
|
|
* @return {Promise<Integer[]> - IDs of libraries to sync
|
|
|
|
*/
|
2015-07-20 21:27:55 +00:00
|
|
|
this.checkLibraries = Zotero.Promise.coroutine(function* (client, options, keyInfo, libraries = []) {
|
|
|
|
var access = keyInfo.access;
|
2016-07-20 14:04:55 +00:00
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
var syncAllLibraries = !libraries || !libraries.length;
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
// TODO: Ability to remove or disable editing of user library?
|
|
|
|
|
|
|
|
if (syncAllLibraries) {
|
|
|
|
if (access.user && access.user.library) {
|
2017-04-12 04:56:37 +00:00
|
|
|
libraries = [Zotero.Libraries.userLibraryID];
|
2017-12-15 04:20:57 +00:00
|
|
|
let skippedLibraries = Zotero.Sync.Data.Local.getSkippedLibraries();
|
|
|
|
|
2016-07-19 22:58:48 +00:00
|
|
|
// If syncing all libraries, remove skipped libraries
|
2017-12-15 04:20:57 +00:00
|
|
|
if (skippedLibraries.length) {
|
|
|
|
Zotero.debug("Skipped libraries:");
|
|
|
|
Zotero.debug(skippedLibraries);
|
|
|
|
libraries = Zotero.Utilities.arrayDiff(libraries, skippedLibraries);
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Check access to specified libraries
|
|
|
|
for (let libraryID of libraries) {
|
2015-11-01 23:29:02 +00:00
|
|
|
let type = Zotero.Libraries.get(libraryID).libraryType;
|
2017-04-12 04:56:37 +00:00
|
|
|
if (type == 'user') {
|
2015-07-20 21:27:55 +00:00
|
|
|
if (!access.user || !access.user.library) {
|
|
|
|
// TODO: Alert
|
|
|
|
throw new Error("Key does not have access to library " + libraryID);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Check group access
|
|
|
|
//
|
|
|
|
let remotelyMissingGroups = [];
|
|
|
|
let groupsToDownload = [];
|
|
|
|
|
|
|
|
if (!Zotero.Utilities.isEmpty(access.groups)) {
|
|
|
|
// TEMP: Require all-group access for now
|
|
|
|
if (access.groups.all) {
|
|
|
|
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw new Error("Full group access is currently required");
|
|
|
|
}
|
|
|
|
|
|
|
|
let remoteGroupVersions = yield client.getGroupVersions(keyInfo.userID);
|
|
|
|
let remoteGroupIDs = Object.keys(remoteGroupVersions).map(id => parseInt(id));
|
2016-07-20 14:04:55 +00:00
|
|
|
let skippedGroups = Zotero.Sync.Data.Local.getSkippedGroups();
|
2015-07-20 21:27:55 +00:00
|
|
|
|
2016-06-27 20:42:55 +00:00
|
|
|
// Remove skipped groups
|
|
|
|
if (syncAllLibraries) {
|
2016-07-20 14:04:55 +00:00
|
|
|
let newGroups = Zotero.Utilities.arrayDiff(remoteGroupIDs, skippedGroups);
|
2016-06-27 20:42:55 +00:00
|
|
|
Zotero.Utilities.arrayDiff(remoteGroupIDs, newGroups)
|
|
|
|
.forEach(id => { delete remoteGroupVersions[id] });
|
|
|
|
remoteGroupIDs = newGroups;
|
|
|
|
}
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
for (let id in remoteGroupVersions) {
|
|
|
|
id = parseInt(id);
|
|
|
|
let group = Zotero.Groups.get(id);
|
|
|
|
|
|
|
|
if (syncAllLibraries) {
|
2017-02-24 07:31:08 +00:00
|
|
|
// If syncing all libraries, mark any that don't exist, are outdated, or are
|
|
|
|
// archived locally for update. Group is added to the library list after downloading.
|
|
|
|
if (!group || group.version < remoteGroupVersions[id] || group.archived) {
|
2017-10-26 03:51:25 +00:00
|
|
|
Zotero.debug(`Marking group ${id} to download`);
|
2015-07-20 21:27:55 +00:00
|
|
|
groupsToDownload.push(id);
|
|
|
|
}
|
|
|
|
// If not outdated, just add to library list
|
|
|
|
else {
|
2017-10-26 03:51:25 +00:00
|
|
|
Zotero.debug(`Adding group library ${group.libraryID} to sync`);
|
2015-07-20 21:27:55 +00:00
|
|
|
libraries.push(group.libraryID);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// If specific libraries were provided, ignore remote groups that don't
|
|
|
|
// exist locally or aren't in the given list
|
|
|
|
if (!group || libraries.indexOf(group.libraryID) == -1) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// If group metadata is outdated, mark for update
|
|
|
|
if (group.version < remoteGroupVersions[id]) {
|
|
|
|
groupsToDownload.push(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get local groups (all if syncing all libraries or just selected ones) that don't
|
|
|
|
// exist remotely
|
|
|
|
// TODO: Use explicit removals?
|
2016-07-20 14:04:55 +00:00
|
|
|
let localGroups;
|
|
|
|
if (syncAllLibraries) {
|
|
|
|
localGroups = Zotero.Groups.getAll()
|
|
|
|
.map(g => g.id)
|
|
|
|
// Don't include skipped groups
|
|
|
|
.filter(id => skippedGroups.indexOf(id) == -1);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
localGroups = libraries
|
|
|
|
.filter(id => Zotero.Libraries.get(id).libraryType == 'group')
|
|
|
|
.map(id => Zotero.Groups.getGroupIDFromLibraryID(id))
|
|
|
|
}
|
2017-10-26 03:51:25 +00:00
|
|
|
Zotero.debug("Local groups:");
|
|
|
|
Zotero.debug(localGroups);
|
2016-07-20 14:04:55 +00:00
|
|
|
remotelyMissingGroups = Zotero.Utilities.arrayDiff(localGroups, remoteGroupIDs)
|
|
|
|
.map(id => Zotero.Groups.get(id));
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
// No group access
|
|
|
|
else {
|
|
|
|
remotelyMissingGroups = Zotero.Groups.getAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (remotelyMissingGroups.length) {
|
|
|
|
// TODO: What about explicit deletions?
|
|
|
|
|
|
|
|
let removedGroups = [];
|
2017-02-24 07:31:08 +00:00
|
|
|
let keptGroups = [];
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
|
|
.getService(Components.interfaces.nsIPromptService);
|
|
|
|
let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
|
|
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
|
|
|
|
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING)
|
|
|
|
+ ps.BUTTON_DELAY_ENABLE;
|
|
|
|
|
|
|
|
// Prompt for each group
|
|
|
|
//
|
|
|
|
// TODO: Localize
|
|
|
|
for (let group of remotelyMissingGroups) {
|
2017-10-26 23:04:38 +00:00
|
|
|
// Ignore remotely missing archived groups
|
2017-02-24 07:31:08 +00:00
|
|
|
if (group.archived) {
|
2017-10-26 23:04:38 +00:00
|
|
|
groupsToDownload = groupsToDownload.filter(groupID => groupID != group.id);
|
2017-02-24 07:31:08 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
let msg;
|
|
|
|
// If all-groups access but group is missing, user left it
|
|
|
|
if (access.groups && access.groups.all) {
|
|
|
|
msg = "You are no longer a member of the group \u2018" + group.name + "\u2019.";
|
|
|
|
}
|
|
|
|
// If not all-groups access, key might just not have access
|
|
|
|
else {
|
|
|
|
msg = "You no longer have access to the group \u2018" + group.name + "\u2019.";
|
|
|
|
}
|
|
|
|
|
|
|
|
msg += "\n\n" + "Would you like to remove it from this computer or keep it "
|
|
|
|
+ "as a read-only library?";
|
|
|
|
|
|
|
|
let index = ps.confirmEx(
|
|
|
|
null,
|
|
|
|
"Group Not Found",
|
|
|
|
msg,
|
|
|
|
buttonFlags,
|
|
|
|
"Remove Group",
|
|
|
|
// TODO: Any way to have Esc trigger extra1 instead so it doesn't
|
|
|
|
// have to be in this order?
|
|
|
|
"Cancel Sync",
|
|
|
|
"Keep Group",
|
|
|
|
null, {}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (index == 0) {
|
|
|
|
removedGroups.push(group);
|
|
|
|
}
|
|
|
|
else if (index == 1) {
|
|
|
|
Zotero.debug("Cancelling sync");
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
else if (index == 2) {
|
2017-02-24 07:31:08 +00:00
|
|
|
keptGroups.push(group);
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let removedLibraryIDs = [];
|
|
|
|
for (let group of removedGroups) {
|
|
|
|
removedLibraryIDs.push(group.libraryID);
|
2017-02-24 07:31:08 +00:00
|
|
|
yield group.eraseTx();
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
libraries = Zotero.Utilities.arrayDiff(libraries, removedLibraryIDs);
|
2017-02-24 07:31:08 +00:00
|
|
|
|
|
|
|
let keptLibraryIDs = [];
|
|
|
|
for (let group of keptGroups) {
|
|
|
|
keptLibraryIDs.push(group.libraryID);
|
|
|
|
group.editable = false;
|
|
|
|
group.archived = true;
|
|
|
|
yield group.saveTx();
|
|
|
|
}
|
|
|
|
libraries = Zotero.Utilities.arrayDiff(libraries, keptLibraryIDs);
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update metadata and permissions on missing or outdated groups
|
|
|
|
for (let groupID of groupsToDownload) {
|
2016-06-27 16:47:11 +00:00
|
|
|
let info = yield client.getGroup(groupID);
|
2015-07-20 21:27:55 +00:00
|
|
|
if (!info) {
|
|
|
|
throw new Error("Group " + groupID + " not found");
|
|
|
|
}
|
|
|
|
let group = Zotero.Groups.get(groupID);
|
2016-07-19 22:58:48 +00:00
|
|
|
if (group) {
|
|
|
|
// Check if the user's permissions for the group have changed, and prompt to reset
|
|
|
|
// data if so
|
|
|
|
let { editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(
|
|
|
|
info.data, keyInfo.userID
|
|
|
|
);
|
|
|
|
let keepGoing = yield Zotero.Sync.Data.Local.checkLibraryForAccess(
|
|
|
|
null, group.libraryID, editable, filesEditable
|
|
|
|
);
|
|
|
|
// User chose to skip library
|
|
|
|
if (!keepGoing) {
|
|
|
|
Zotero.debug("Skipping sync of group " + group.id);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2015-07-20 21:27:55 +00:00
|
|
|
group = new Zotero.Group;
|
|
|
|
group.id = groupID;
|
|
|
|
}
|
|
|
|
group.version = info.version;
|
2017-02-24 07:31:08 +00:00
|
|
|
group.archived = false;
|
2015-07-20 21:27:55 +00:00
|
|
|
group.fromJSON(info.data, Zotero.Users.getCurrentUserID());
|
2017-06-16 09:56:06 +00:00
|
|
|
yield group.saveTx();
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
// Add group to library list
|
|
|
|
libraries.push(group.libraryID);
|
|
|
|
}
|
|
|
|
|
2017-02-24 07:31:08 +00:00
|
|
|
// Note: If any non-group library types become archivable, they'll need to be unarchived here.
|
2017-10-26 03:51:25 +00:00
|
|
|
Zotero.debug("Final libraries to sync:");
|
|
|
|
Zotero.debug(libraries);
|
2017-02-24 07:31:08 +00:00
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
return [...new Set(libraries)];
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2015-11-12 07:54:51 +00:00
|
|
|
/**
|
|
|
|
* Run sync engine for passed libraries
|
|
|
|
*
|
|
|
|
* @param {Integer[]} libraries
|
|
|
|
* @param {Object} options
|
|
|
|
* @param {Boolean} skipUpdateLastSyncTime
|
|
|
|
* @return {Integer[]} - Array of libraryIDs that completed successfully
|
|
|
|
*/
|
2015-10-29 07:41:54 +00:00
|
|
|
var _doDataSync = Zotero.Promise.coroutine(function* (libraries, options, skipUpdateLastSyncTime) {
|
|
|
|
var successfulLibraries = [];
|
|
|
|
for (let libraryID of libraries) {
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
2015-10-29 07:41:54 +00:00
|
|
|
try {
|
|
|
|
let opts = {};
|
|
|
|
Object.assign(opts, options);
|
|
|
|
opts.libraryID = libraryID;
|
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_currentEngine = new Zotero.Sync.Data.Engine(opts);
|
|
|
|
yield _currentEngine.start();
|
|
|
|
_currentEngine = null;
|
2015-10-29 07:41:54 +00:00
|
|
|
successfulLibraries.push(libraryID);
|
|
|
|
}
|
|
|
|
catch (e) {
|
2016-05-06 07:08:22 +00:00
|
|
|
if (e instanceof Zotero.Sync.UserCancelledException) {
|
|
|
|
if (e.advanceToNextLibrary) {
|
|
|
|
Zotero.debug("Sync cancelled for library " + libraryID + " -- "
|
|
|
|
+ "advancing to next library");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
2017-04-08 02:45:16 +00:00
|
|
|
Zotero.debug("Sync failed for library " + libraryID, 1);
|
2015-10-29 07:41:54 +00:00
|
|
|
Zotero.logError(e);
|
|
|
|
this.checkError(e);
|
2016-02-25 09:51:55 +00:00
|
|
|
options.onError(e);
|
2015-10-29 07:41:54 +00:00
|
|
|
if (stopOnError || e.fatal) {
|
|
|
|
Zotero.debug("Stopping on error", 1);
|
|
|
|
options.caller.stop();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Update last-sync time if any libraries synced
|
|
|
|
// TEMP: Do we want to show updated time if some libraries haven't synced?
|
|
|
|
if (!libraries.length || successfulLibraries.length) {
|
|
|
|
yield Zotero.Sync.Data.Local.updateLastSyncTime();
|
|
|
|
}
|
|
|
|
return successfulLibraries;
|
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
|
2015-11-12 07:54:51 +00:00
|
|
|
/**
|
|
|
|
* @return {Integer[]} - Array of libraries that need data syncing again
|
|
|
|
*/
|
2015-10-29 07:41:54 +00:00
|
|
|
var _doFileSync = Zotero.Promise.coroutine(function* (libraries, options) {
|
|
|
|
Zotero.debug("Starting file syncing");
|
2015-11-12 07:54:51 +00:00
|
|
|
var resyncLibraries = []
|
2015-10-29 07:41:54 +00:00
|
|
|
for (let libraryID of libraries) {
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
2017-08-13 01:26:23 +00:00
|
|
|
let libraryName = Zotero.Libraries.get(libraryID).name;
|
2017-08-10 20:41:27 +00:00
|
|
|
this.setSyncStatus(
|
2017-08-13 01:26:23 +00:00
|
|
|
Zotero.getString('sync.status.syncingFilesInLibrary', libraryName)
|
2017-08-10 20:41:27 +00:00
|
|
|
);
|
2015-10-29 07:41:54 +00:00
|
|
|
try {
|
2017-08-10 20:41:27 +00:00
|
|
|
let opts = {
|
|
|
|
onProgress: (progress, progressMax) => {
|
|
|
|
var remaining = progressMax - progress;
|
|
|
|
this.setSyncStatus(
|
|
|
|
Zotero.getString(
|
|
|
|
'sync.status.syncingFilesInLibraryWithRemaining',
|
2017-08-13 01:26:23 +00:00
|
|
|
[libraryName, remaining],
|
2017-08-10 20:41:27 +00:00
|
|
|
remaining
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
2015-10-29 07:41:54 +00:00
|
|
|
Object.assign(opts, options);
|
|
|
|
opts.libraryID = libraryID;
|
|
|
|
|
2015-12-23 09:52:09 +00:00
|
|
|
let mode = Zotero.Sync.Storage.Local.getModeForLibrary(libraryID);
|
|
|
|
opts.controller = this.getStorageController(mode, opts);
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
let tries = 3;
|
|
|
|
while (true) {
|
|
|
|
if (tries == 0) {
|
|
|
|
throw new Error("Too many file sync attempts for library " + libraryID);
|
|
|
|
}
|
|
|
|
tries--;
|
2017-07-07 09:18:23 +00:00
|
|
|
_currentEngine = new Zotero.Sync.Storage.Engine(opts);
|
|
|
|
let results = yield _currentEngine.start();
|
|
|
|
_currentEngine = null;
|
2015-10-29 07:41:54 +00:00
|
|
|
if (results.syncRequired) {
|
2015-11-12 07:54:51 +00:00
|
|
|
resyncLibraries.push(libraryID);
|
2015-10-29 07:41:54 +00:00
|
|
|
}
|
|
|
|
else if (results.fileSyncRequired) {
|
|
|
|
Zotero.debug("Another file sync required -- restarting");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (e) {
|
2017-07-07 09:18:23 +00:00
|
|
|
if (e instanceof Zotero.Sync.UserCancelledException) {
|
|
|
|
if (e.advanceToNextLibrary) {
|
|
|
|
Zotero.debug("Storage sync cancelled for library " + libraryID + " -- "
|
|
|
|
+ "advancing to next library");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
Zotero.debug("File sync failed for library " + libraryID);
|
2015-11-12 07:54:51 +00:00
|
|
|
Zotero.logError(e);
|
2015-10-29 07:41:54 +00:00
|
|
|
this.checkError(e);
|
2016-02-25 09:51:55 +00:00
|
|
|
options.onError(e);
|
2015-10-29 07:41:54 +00:00
|
|
|
if (stopOnError || e.fatal) {
|
|
|
|
options.caller.stop();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Zotero.debug("Done with file syncing");
|
2016-05-02 17:13:19 +00:00
|
|
|
if (resyncLibraries.length) {
|
|
|
|
Zotero.debug("Libraries to resync: " + resyncLibraries.join(", "));
|
|
|
|
}
|
2015-11-12 07:54:51 +00:00
|
|
|
return resyncLibraries;
|
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
|
2016-05-02 17:13:19 +00:00
|
|
|
/**
|
|
|
|
* @return {Integer[]} - Array of libraries that need data syncing again
|
|
|
|
*/
|
2015-11-12 07:54:51 +00:00
|
|
|
var _doFullTextSync = Zotero.Promise.coroutine(function* (libraries, options) {
|
2016-06-23 18:28:13 +00:00
|
|
|
if (!Zotero.Prefs.get("sync.fulltext.enabled")) return [];
|
2015-11-12 07:54:51 +00:00
|
|
|
|
|
|
|
Zotero.debug("Starting full-text syncing");
|
|
|
|
this.setSyncStatus(Zotero.getString('sync.status.syncingFullText'));
|
2016-05-02 17:13:19 +00:00
|
|
|
var resyncLibraries = [];
|
2015-11-12 07:54:51 +00:00
|
|
|
for (let libraryID of libraries) {
|
2017-07-07 09:18:23 +00:00
|
|
|
_stopCheck();
|
2015-11-12 07:54:51 +00:00
|
|
|
try {
|
|
|
|
let opts = {};
|
|
|
|
Object.assign(opts, options);
|
|
|
|
opts.libraryID = libraryID;
|
|
|
|
|
2017-07-07 09:18:23 +00:00
|
|
|
_currentEngine = new Zotero.Sync.Data.FullTextEngine(opts);
|
|
|
|
yield _currentEngine.start();
|
|
|
|
_currentEngine = null;
|
2015-11-12 07:54:51 +00:00
|
|
|
}
|
|
|
|
catch (e) {
|
2017-07-07 09:18:23 +00:00
|
|
|
if (e instanceof Zotero.Sync.UserCancelledException) {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
2016-05-02 17:13:19 +00:00
|
|
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.status == 412) {
|
|
|
|
resyncLibraries.push(libraryID);
|
|
|
|
continue;
|
|
|
|
}
|
2015-11-12 07:54:51 +00:00
|
|
|
Zotero.debug("Full-text sync failed for library " + libraryID);
|
|
|
|
Zotero.logError(e);
|
|
|
|
this.checkError(e);
|
2016-02-25 09:51:55 +00:00
|
|
|
options.onError(e);
|
2015-11-12 07:54:51 +00:00
|
|
|
if (stopOnError || e.fatal) {
|
|
|
|
options.caller.stop();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Zotero.debug("Done with full-text syncing");
|
2016-05-02 17:13:19 +00:00
|
|
|
if (resyncLibraries.length) {
|
|
|
|
Zotero.debug("Libraries to resync: " + resyncLibraries.join(", "));
|
|
|
|
}
|
|
|
|
return resyncLibraries;
|
2015-10-29 07:41:54 +00:00
|
|
|
}.bind(this));
|
|
|
|
|
|
|
|
|
2015-12-23 09:52:09 +00:00
|
|
|
/**
|
|
|
|
* Get a storage controller for a given mode ('zfs', 'webdav'),
|
|
|
|
* caching it if necessary
|
|
|
|
*/
|
|
|
|
this.getStorageController = function (mode, options) {
|
|
|
|
if (_storageControllers[mode]) {
|
|
|
|
return _storageControllers[mode];
|
|
|
|
}
|
|
|
|
var modeClass = Zotero.Sync.Storage.Utilities.getClassForMode(mode);
|
|
|
|
return _storageControllers[mode] = new modeClass(options);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Call on API key change
|
|
|
|
this.resetStorageController = function (mode) {
|
|
|
|
delete _storageControllers[mode];
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
/**
|
|
|
|
* Download a single file on demand (not within a sync process)
|
|
|
|
*/
|
|
|
|
this.downloadFile = Zotero.Promise.coroutine(function* (item, requestCallbacks) {
|
|
|
|
if (Zotero.HTTP.browserIsOffline()) {
|
|
|
|
Zotero.debug("Browser is offline", 2);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-11-02 08:22:37 +00:00
|
|
|
var apiKey = yield _getAPIKey();
|
|
|
|
if (!apiKey) {
|
|
|
|
Zotero.debug("API key not set -- skipping download");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-10-29 07:41:54 +00:00
|
|
|
// TEMP
|
|
|
|
var options = {};
|
|
|
|
|
|
|
|
var itemID = item.id;
|
|
|
|
var modeClass = Zotero.Sync.Storage.Local.getClassForLibrary(item.libraryID);
|
|
|
|
var controller = new modeClass({
|
2015-11-02 08:22:37 +00:00
|
|
|
apiClient: this.getAPIClient({apiKey })
|
2015-10-29 07:41:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// TODO: verify WebDAV on-demand?
|
|
|
|
if (!controller.verified) {
|
|
|
|
Zotero.debug("File syncing is not active for item's library -- skipping download");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-07-26 10:04:38 +00:00
|
|
|
if (!item.isStoredFileAttachment()) {
|
|
|
|
throw new Error("Not a stored file attachment");
|
2015-10-29 07:41:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (yield item.getFilePathAsync()) {
|
|
|
|
Zotero.debug("File already exists -- replacing");
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: start sync icon?
|
|
|
|
// TODO: create queue for cancelling
|
|
|
|
|
|
|
|
if (!requestCallbacks) {
|
|
|
|
requestCallbacks = {};
|
|
|
|
}
|
|
|
|
var onStart = function (request) {
|
|
|
|
return controller.downloadFile(request);
|
|
|
|
};
|
|
|
|
var request = new Zotero.Sync.Storage.Request({
|
|
|
|
type: 'download',
|
|
|
|
libraryID: item.libraryID,
|
|
|
|
name: item.libraryKey,
|
|
|
|
onStart: requestCallbacks.onStart
|
|
|
|
? [onStart, requestCallbacks.onStart]
|
|
|
|
: [onStart]
|
|
|
|
});
|
|
|
|
return request.start();
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
this.stop = function () {
|
2017-07-07 09:18:23 +00:00
|
|
|
this.setSyncStatus(Zotero.getString('sync.stopping'));
|
|
|
|
_stopping = true;
|
|
|
|
if (_currentEngine) {
|
|
|
|
_currentEngine.stop();
|
|
|
|
}
|
2019-09-16 05:08:18 +00:00
|
|
|
if (_canceller) {
|
|
|
|
_canceller();
|
|
|
|
}
|
2015-10-29 07:41:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-21 15:08:48 +00:00
|
|
|
this.end = Zotero.Promise.coroutine(function* (options) {
|
2017-07-11 06:42:00 +00:00
|
|
|
_syncInProgress = false;
|
2016-04-21 15:08:48 +00:00
|
|
|
yield this.checkErrors(_errors, options);
|
|
|
|
if (!options.restartSync) {
|
|
|
|
this.updateIcons(_errors);
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
_errors = [];
|
2016-04-21 15:08:48 +00:00
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-04-01 06:24:50 +00:00
|
|
|
* @param {Integer} timeout - Timeout in seconds
|
|
|
|
* @param {Boolean} [recurring=false]
|
2021-05-14 07:16:44 +00:00
|
|
|
* @param {Object} [options] - Sync options (e.g., 'libraries', 'fileLibraries', 'fullTextLibraries')
|
2015-07-20 21:27:55 +00:00
|
|
|
*/
|
2016-04-01 06:24:50 +00:00
|
|
|
this.setSyncTimeout = function (timeout, recurring, options = {}) {
|
|
|
|
if (!Zotero.Prefs.get('sync.autoSync') || !this.enabled) {
|
|
|
|
return;
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
if (!timeout) {
|
2016-04-01 06:24:50 +00:00
|
|
|
throw new Error("Timeout not provided");
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
2021-05-14 09:13:19 +00:00
|
|
|
if (timeout != parseInt(timeout)) {
|
|
|
|
throw new Error(`Timeout must be an integer (${timeout} given)`);
|
|
|
|
}
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
if (_autoSyncTimer) {
|
|
|
|
Zotero.debug("Cancelling auto-sync timer");
|
|
|
|
_autoSyncTimer.cancel();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
_autoSyncTimer = Components.classes["@mozilla.org/timer;1"].
|
|
|
|
createInstance(Components.interfaces.nsITimer);
|
|
|
|
}
|
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
var mergedOpts = {
|
|
|
|
background: true
|
|
|
|
};
|
|
|
|
Object.assign(mergedOpts, options);
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
// Implements nsITimerCallback
|
|
|
|
var callback = {
|
2018-03-25 08:56:50 +00:00
|
|
|
notify: async function (timer) {
|
2016-04-01 06:24:50 +00:00
|
|
|
if (!_getAPIKey()) {
|
2015-07-20 21:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-25 08:56:50 +00:00
|
|
|
// If a delay is set (e.g., from the connector target selector), wait to sync.
|
|
|
|
// We do this in sync() too for manual syncs, but no need to start spinning if
|
|
|
|
// it's just an auto-sync.
|
|
|
|
while (_delaySyncUntil && new Date() < _delaySyncUntil) {
|
|
|
|
let delay = _delaySyncUntil - new Date();
|
|
|
|
Zotero.debug(`Waiting ${delay} ms to start auto-sync`);
|
|
|
|
await Zotero.Promise.delay(delay);
|
|
|
|
}
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
if (Zotero.locked) {
|
|
|
|
Zotero.debug('Zotero is locked -- skipping auto-sync', 4);
|
2021-05-14 07:16:44 +00:00
|
|
|
_queueSyncOptions(mergedOpts);
|
2015-07-20 21:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
if (_syncInProgress) {
|
2015-07-20 21:27:55 +00:00
|
|
|
Zotero.debug('Sync already in progress -- skipping auto-sync', 4);
|
2021-05-14 07:16:44 +00:00
|
|
|
_queueSyncOptions(mergedOpts);
|
2015-07-20 21:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
if (_manualSyncRequired) {
|
2015-07-20 21:27:55 +00:00
|
|
|
Zotero.debug('Manual sync required -- skipping auto-sync', 4);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-04-01 06:24:50 +00:00
|
|
|
this.sync(mergedOpts);
|
|
|
|
}.bind(this)
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (recurring) {
|
|
|
|
Zotero.debug('Setting auto-sync interval to ' + timeout + ' seconds');
|
|
|
|
_autoSyncTimer.initWithCallback(
|
|
|
|
callback, timeout * 1000, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK
|
|
|
|
);
|
|
|
|
}
|
|
|
|
else {
|
2016-04-01 06:24:50 +00:00
|
|
|
if (_syncInProgress) {
|
2015-07-20 21:27:55 +00:00
|
|
|
Zotero.debug('Sync in progress -- not setting auto-sync timeout', 4);
|
2021-05-14 07:16:44 +00:00
|
|
|
_queueSyncOptions(mergedOpts);
|
2015-07-20 21:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Zotero.debug('Setting auto-sync timeout to ' + timeout + ' seconds');
|
|
|
|
_autoSyncTimer.initWithCallback(
|
|
|
|
callback, timeout * 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-05-14 07:16:44 +00:00
|
|
|
function _queueSyncOptions(options) {
|
|
|
|
var jsonOptions = JSON.stringify(options);
|
|
|
|
// Don't queue options if already queued
|
|
|
|
if (_queuedSyncOptions.includes(jsonOptions)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Zotero.debug("Queueing sync options");
|
|
|
|
Zotero.debug(options);
|
|
|
|
_queuedSyncOptions.push(jsonOptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
this.clearSyncTimeout = function () {
|
|
|
|
if (_autoSyncTimer) {
|
|
|
|
_autoSyncTimer.cancel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-25 08:56:50 +00:00
|
|
|
this.delaySync = function (ms) {
|
|
|
|
_delaySyncUntil = new Date(Date.now() + ms);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2018-05-04 23:53:54 +00:00
|
|
|
/**
|
|
|
|
* Delay syncs until the returned function is called
|
|
|
|
*
|
|
|
|
* @return {Function} - Resolve function
|
|
|
|
*/
|
|
|
|
this.delayIndefinite = function () {
|
|
|
|
var resolve;
|
|
|
|
var promise = new Zotero.Promise(function () {
|
|
|
|
resolve = arguments[0];
|
|
|
|
});
|
|
|
|
_delayPromises.push(promise);
|
|
|
|
return resolve;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
/**
|
|
|
|
* Trigger updating of the main sync icon, the sync error icon, and
|
|
|
|
* library-specific sync error icons across all windows
|
|
|
|
*/
|
|
|
|
this.addError = function (e, libraryID) {
|
|
|
|
if (e.added) return;
|
|
|
|
e.added = true;
|
|
|
|
if (libraryID) {
|
|
|
|
e.libraryID = libraryID;
|
|
|
|
}
|
2016-06-23 18:37:20 +00:00
|
|
|
Zotero.logError(e);
|
2015-07-20 21:27:55 +00:00
|
|
|
_errors.push(this.parseError(e));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.getErrorsByLibrary = function (libraryID) {
|
|
|
|
return _errors.filter(e => e.libraryID === libraryID);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get most severe error type from an array of parsed errors
|
|
|
|
*/
|
|
|
|
this.getPrimaryErrorType = function (errors) {
|
|
|
|
// Set highest priority error as the primary (sync error icon)
|
|
|
|
var errorTypes = {
|
|
|
|
info: 1,
|
|
|
|
warning: 2,
|
|
|
|
error: 3,
|
|
|
|
upgrade: 4,
|
|
|
|
|
|
|
|
// Skip these
|
2021-02-09 21:36:06 +00:00
|
|
|
animate: -1,
|
|
|
|
ignore: -2
|
2015-07-20 21:27:55 +00:00
|
|
|
};
|
|
|
|
var state = false;
|
|
|
|
for (let i = 0; i < errors.length; i++) {
|
|
|
|
let e = errors[i];
|
|
|
|
|
|
|
|
let errorType = e.errorType;
|
|
|
|
|
|
|
|
if (e.fatal) {
|
|
|
|
return 'error';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!errorType || errorTypes[errorType] < 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!state || errorTypes[errorType] > errorTypes[state]) {
|
|
|
|
state = errorType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-21 15:08:48 +00:00
|
|
|
this.checkErrors = Zotero.Promise.coroutine(function* (errors, options = {}) {
|
|
|
|
for (let e of errors) {
|
|
|
|
let handled = yield this.checkError(e, options);
|
|
|
|
if (handled) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.checkError = Zotero.Promise.coroutine(function* (e, options = {}) {
|
2015-07-20 21:27:55 +00:00
|
|
|
if (e.name && e.name == 'Zotero Error') {
|
|
|
|
switch (e.error) {
|
2016-04-23 07:32:32 +00:00
|
|
|
case Zotero.Error.ERROR_API_KEY_NOT_SET:
|
2016-04-25 07:12:11 +00:00
|
|
|
case Zotero.Error.ERROR_API_KEY_INVALID:
|
2015-07-20 21:27:55 +00:00
|
|
|
// TODO: the setTimeout() call below should just simulate a click on the sync error icon
|
|
|
|
// instead of creating its own dialog, but updateIcons() doesn't yet provide full control
|
|
|
|
// over dialog title and primary button text/action, which is why this version of the
|
|
|
|
// dialog is a bit uglier than the manual click version
|
|
|
|
// TODO: localize (=>done) and combine with below (=>?)
|
|
|
|
var msg = Zotero.getString('sync.error.invalidLogin.text');
|
|
|
|
e.message = msg;
|
2016-04-21 15:08:48 +00:00
|
|
|
e.dialogButtonText = Zotero.getString('sync.openSyncPreferences');
|
|
|
|
e.dialogButtonCallback = function () {
|
2015-07-20 21:27:55 +00:00
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var win = wm.getMostRecentWindow("navigator:browser");
|
|
|
|
win.ZoteroPane.openPreferences("zotero-prefpane-sync");
|
|
|
|
};
|
|
|
|
|
|
|
|
// Manual click
|
2016-04-23 07:32:32 +00:00
|
|
|
if (!options.background) {
|
2015-07-20 21:27:55 +00:00
|
|
|
setTimeout(function () {
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var win = wm.getMostRecentWindow("navigator:browser");
|
|
|
|
|
|
|
|
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_CANCEL);
|
2016-04-23 07:32:32 +00:00
|
|
|
if (e.error == Zotero.Error.ERROR_API_KEY_NOT_SET) {
|
2015-07-20 21:27:55 +00:00
|
|
|
var title = Zotero.getString('sync.error.usernameNotSet');
|
|
|
|
var msg = Zotero.getString('sync.error.usernameNotSet.text');
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
var title = Zotero.getString('sync.error.invalidLogin');
|
|
|
|
var msg = Zotero.getString('sync.error.invalidLogin.text');
|
|
|
|
}
|
|
|
|
var index = ps.confirmEx(
|
|
|
|
win,
|
|
|
|
title,
|
|
|
|
msg,
|
|
|
|
buttonFlags,
|
|
|
|
Zotero.getString('sync.openSyncPreferences'),
|
|
|
|
null, null, null, {}
|
|
|
|
);
|
|
|
|
|
|
|
|
if (index == 0) {
|
2017-07-07 09:20:10 +00:00
|
|
|
Zotero.Utilities.Internal.openPreferences("zotero-prefpane-sync");
|
2015-07-20 21:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}, 1);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2016-04-21 15:08:48 +00:00
|
|
|
else if (e.name && e.name == 'ZoteroObjectUploadError') {
|
2017-06-01 19:33:00 +00:00
|
|
|
let { code, data, objectType, object } = e;
|
|
|
|
|
2018-02-18 20:21:13 +00:00
|
|
|
if (code == 413) {
|
|
|
|
// Collection name too long
|
|
|
|
if (objectType == 'collection' && data && data.value) {
|
|
|
|
e.message = Zotero.getString('sync.error.collectionTooLong', [data.value]);
|
|
|
|
|
|
|
|
e.dialogButtonText = Zotero.getString('pane.collections.showCollectionInLibrary');
|
|
|
|
e.dialogButtonCallback = () => {
|
2017-06-01 19:33:00 +00:00
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
2018-02-18 20:21:13 +00:00
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var win = wm.getMostRecentWindow("navigator:browser");
|
|
|
|
win.ZoteroPane.collectionsView.selectCollection(object.id);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else if (objectType == 'item') {
|
|
|
|
// Tag too long
|
|
|
|
if (data && data.tag !== undefined) {
|
|
|
|
// Show long tag fixer and handle result
|
|
|
|
e.dialogButtonText = Zotero.getString('general.fix');
|
|
|
|
e.dialogButtonCallback = Zotero.Promise.coroutine(function* () {
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var lastWin = wm.getMostRecentWindow("navigator:browser");
|
|
|
|
|
|
|
|
// Open long tag fixer for every long tag in every editable library we're syncing
|
|
|
|
var editableLibraries = options.libraries
|
|
|
|
.filter(x => Zotero.Libraries.get(x).editable);
|
|
|
|
for (let libraryID of editableLibraries) {
|
|
|
|
let oldTagIDs = yield Zotero.Tags.getLongTagsInLibrary(libraryID);
|
|
|
|
for (let oldTagID of oldTagIDs) {
|
|
|
|
let oldTag = Zotero.Tags.getName(oldTagID);
|
|
|
|
let dataOut = { result: null };
|
|
|
|
lastWin.openDialog(
|
|
|
|
'chrome://zotero/content/longTagFixer.xul',
|
|
|
|
'',
|
|
|
|
'chrome,modal,centerscreen',
|
|
|
|
oldTag,
|
|
|
|
dataOut
|
|
|
|
);
|
|
|
|
// If dialog was cancelled, stop
|
|
|
|
if (!dataOut.result) {
|
|
|
|
return;
|
2017-06-01 19:33:00 +00:00
|
|
|
}
|
2018-02-18 20:21:13 +00:00
|
|
|
switch (dataOut.result.op) {
|
|
|
|
case 'split':
|
|
|
|
for (let libraryID of editableLibraries) {
|
|
|
|
let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
|
2020-07-05 10:20:01 +00:00
|
|
|
yield Zotero.DB.executeTransaction(async function () {
|
2018-02-18 20:21:13 +00:00
|
|
|
for (let itemID of itemIDs) {
|
2020-07-05 10:20:01 +00:00
|
|
|
let item = await Zotero.Items.getAsync(itemID);
|
2018-02-18 20:21:13 +00:00
|
|
|
for (let tag of dataOut.result.tags) {
|
|
|
|
item.addTag(tag);
|
|
|
|
}
|
|
|
|
item.removeTag(oldTag);
|
2020-07-05 10:20:01 +00:00
|
|
|
await item.save();
|
2018-02-18 20:21:13 +00:00
|
|
|
}
|
2020-07-05 10:20:01 +00:00
|
|
|
await Zotero.Tags.purge(oldTagID);
|
2018-02-18 20:21:13 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'edit':
|
|
|
|
for (let libraryID of editableLibraries) {
|
|
|
|
let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
|
2020-07-05 10:20:01 +00:00
|
|
|
yield Zotero.DB.executeTransaction(async function () {
|
2018-02-18 20:21:13 +00:00
|
|
|
for (let itemID of itemIDs) {
|
2020-07-05 10:20:01 +00:00
|
|
|
let item = await Zotero.Items.getAsync(itemID);
|
2018-02-18 20:21:13 +00:00
|
|
|
item.replaceTag(oldTag, dataOut.result.tag);
|
2020-07-05 10:20:01 +00:00
|
|
|
await item.save();
|
2018-02-18 20:21:13 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'delete':
|
|
|
|
for (let libraryID of editableLibraries) {
|
|
|
|
yield Zotero.Tags.removeFromLibrary(libraryID, oldTagID);
|
|
|
|
}
|
|
|
|
break;
|
2017-06-01 19:33:00 +00:00
|
|
|
}
|
2016-04-21 15:08:48 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-18 20:21:13 +00:00
|
|
|
|
|
|
|
options.restartSync = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Note too long
|
|
|
|
if (object.isNote() || object.isAttachment()) {
|
|
|
|
// Throw an error that adds a button for selecting the item to the sync error dialog
|
|
|
|
if (e.message.includes('<img src="data:image')) {
|
|
|
|
e.message = Zotero.getString('sync.error.noteEmbeddedImage');
|
|
|
|
}
|
|
|
|
else if (e.message.match(/^Note '.*' too long for item/)) {
|
|
|
|
e.message = Zotero.getString(
|
|
|
|
'sync.error.noteTooLong',
|
|
|
|
Zotero.Utilities.ellipsize(object.getNoteTitle(), 40)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Field or creator too long
|
|
|
|
else if (data && data.field) {
|
|
|
|
e.message = (data.field == 'creator'
|
|
|
|
? Zotero.getString(
|
|
|
|
'sync.error.creatorTooLong',
|
|
|
|
[data.value]
|
|
|
|
)
|
|
|
|
: Zotero.getString(
|
|
|
|
'sync.error.fieldTooLong',
|
|
|
|
[data.field, data.value]
|
|
|
|
))
|
|
|
|
+ '\n\n'
|
|
|
|
+ Zotero.getString(
|
|
|
|
'sync.error.reportSiteIssuesToForums',
|
|
|
|
Zotero.clientName
|
|
|
|
);
|
2016-04-21 15:08:48 +00:00
|
|
|
}
|
2017-06-01 19:33:00 +00:00
|
|
|
|
2018-02-18 20:21:13 +00:00
|
|
|
// Include "Show Item in Library" button
|
|
|
|
e.dialogButtonText = Zotero.getString('pane.items.showItemInLibrary');
|
|
|
|
e.dialogButtonCallback = () => {
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var win = wm.getMostRecentWindow("navigator:browser");
|
|
|
|
win.ZoteroPane.selectItem(object.id);
|
|
|
|
};
|
2016-04-21 15:08:48 +00:00
|
|
|
}
|
2017-06-01 19:33:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If not a background sync, show dialog immediately
|
|
|
|
if (!options.background && e.dialogButtonCallback) {
|
|
|
|
let maybePromise = e.dialogButtonCallback();
|
|
|
|
if (maybePromise && maybePromise.then) {
|
|
|
|
yield maybePromise;
|
|
|
|
}
|
2016-04-21 15:08:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
Type/field handling overhaul
This changes the way item types, item fields, creator types, and CSL
mappings are defined and handled, in preparation for updated types and
fields.
Instead of being predefined in SQL files or code, type/field info is
read from a bundled JSON file shared with other parts of the Zotero
ecosystem [1], referred to as the "global schema". Updates to the
bundled schema file are automatically applied to the database at first
run, allowing changes to be made consistently across apps.
When syncing, invalid JSON properties are now rejected instead of being
ignored and processed later, which will allow for schema changes to be
made without causing problems in existing clients. We considered many
alternative approaches, but this approach is by far the simplest,
safest, and most transparent to the user.
For now, there are no actual changes to types and fields, since we'll
first need to do a sync cut-off for earlier versions that don't reject
invalid properties.
For third-party code, the main change is that type and field IDs should
no longer be hard-coded, since they may not be consistent in new
installs. For example, code should use `Zotero.ItemTypes.getID('note')`
instead of hard-coding `1`.
[1] https://github.com/zotero/zotero-schema
2019-05-16 08:56:46 +00:00
|
|
|
// Show warning for unknown data that couldn't be saved
|
|
|
|
else if (e.name && e.name == 'ZoteroInvalidDataError') {
|
2021-02-09 21:36:06 +00:00
|
|
|
let library = Zotero.Libraries.get(e.libraryID);
|
|
|
|
let msg = Zotero.getString(
|
|
|
|
'sync.error.invalidDataError',
|
|
|
|
[
|
|
|
|
library.name,
|
|
|
|
Zotero.clientName
|
|
|
|
]
|
|
|
|
)
|
|
|
|
+ "\n\n"
|
|
|
|
+ Zotero.getString('sync.error.invalidDataError.otherData');
|
|
|
|
|
|
|
|
// Show warning for My Library
|
|
|
|
if (library.libraryType == 'user') {
|
|
|
|
e.message = msg;
|
|
|
|
e.errorType = 'warning';
|
|
|
|
e.dialogButtonText = Zotero.getString('general.checkForUpdates');
|
|
|
|
e.dialogButtonCallback = () => {
|
|
|
|
Zotero.openCheckForUpdatesWindow();
|
|
|
|
};
|
2021-02-25 21:08:54 +00:00
|
|
|
e.dialogButton2Text = Zotero.getString('general.moreInformation');
|
|
|
|
e.dialogButton2Callback = () => {
|
|
|
|
Zotero.launchURL('https://www.zotero.org/support/kb/unknown_data_error');
|
|
|
|
};
|
2021-02-09 21:36:06 +00:00
|
|
|
}
|
|
|
|
// Otherwise just show in sync button tooltip
|
|
|
|
else {
|
|
|
|
_addTooltipMessage(msg);
|
|
|
|
e.errorType = 'ignore';
|
|
|
|
}
|
Type/field handling overhaul
This changes the way item types, item fields, creator types, and CSL
mappings are defined and handled, in preparation for updated types and
fields.
Instead of being predefined in SQL files or code, type/field info is
read from a bundled JSON file shared with other parts of the Zotero
ecosystem [1], referred to as the "global schema". Updates to the
bundled schema file are automatically applied to the database at first
run, allowing changes to be made consistently across apps.
When syncing, invalid JSON properties are now rejected instead of being
ignored and processed later, which will allow for schema changes to be
made without causing problems in existing clients. We considered many
alternative approaches, but this approach is by far the simplest,
safest, and most transparent to the user.
For now, there are no actual changes to types and fields, since we'll
first need to do a sync cut-off for earlier versions that don't reject
invalid properties.
For third-party code, the main change is that type and field IDs should
no longer be hard-coded, since they may not be consistent in new
installs. For example, code should use `Zotero.ItemTypes.getID('note')`
instead of hard-coding `1`.
[1] https://github.com/zotero/zotero-schema
2019-05-16 08:56:46 +00:00
|
|
|
}
|
2016-04-21 15:08:48 +00:00
|
|
|
});
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the sync icon and sync error icon across all windows
|
|
|
|
*
|
|
|
|
* @param {Error|Error[]|'animate'} errors - An error, an array of errors, or 'animate' to
|
|
|
|
* spin the icon. An empty array will reset the
|
|
|
|
* icons.
|
|
|
|
*/
|
|
|
|
this.updateIcons = function (errors) {
|
|
|
|
if (typeof errors == 'string') {
|
|
|
|
var state = errors;
|
|
|
|
errors = [];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (!Array.isArray(errors)) {
|
|
|
|
errors = [errors];
|
|
|
|
}
|
2021-02-09 21:36:06 +00:00
|
|
|
errors = errors.filter(o => o.errorType !== 'ignore');
|
2015-07-20 21:27:55 +00:00
|
|
|
var state = this.getPrimaryErrorType(errors);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh source list
|
|
|
|
//yield Zotero.Notifier.trigger('redraw', 'collection', []);
|
|
|
|
|
|
|
|
if (errors.length == 1 && errors[0].frontWindowOnly) {
|
|
|
|
// Fake an nsISimpleEnumerator with just the topmost window
|
|
|
|
var enumerator = {
|
|
|
|
_returned: false,
|
|
|
|
hasMoreElements: function () {
|
|
|
|
return !this._returned;
|
|
|
|
},
|
|
|
|
getNext: function () {
|
|
|
|
if (this._returned) {
|
|
|
|
throw ("No more windows to return in Zotero.Sync.Runner.updateIcons()");
|
|
|
|
}
|
|
|
|
this._returned = true;
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
return wm.getMostRecentWindow("navigator:browser");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
// Update all windows
|
|
|
|
else {
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
|
|
var enumerator = wm.getEnumerator('navigator:browser');
|
|
|
|
}
|
|
|
|
|
|
|
|
while (enumerator.hasMoreElements()) {
|
|
|
|
var win = enumerator.getNext();
|
|
|
|
if (!win.ZoteroPane) continue;
|
|
|
|
var doc = win.ZoteroPane.document;
|
|
|
|
|
|
|
|
// Update sync error icon
|
|
|
|
var icon = doc.getElementById('zotero-tb-sync-error');
|
|
|
|
this.updateErrorIcon(icon, state, errors);
|
|
|
|
|
|
|
|
// Update sync icon
|
|
|
|
var syncIcon = doc.getElementById('zotero-tb-sync');
|
2017-07-07 09:18:23 +00:00
|
|
|
var stopIcon = doc.getElementById('zotero-tb-sync-stop');
|
2015-07-20 21:27:55 +00:00
|
|
|
if (state == 'animate') {
|
|
|
|
syncIcon.setAttribute('status', state);
|
|
|
|
// Disable button while spinning
|
|
|
|
syncIcon.disabled = true;
|
2017-07-07 09:18:23 +00:00
|
|
|
stopIcon.hidden = false;
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
syncIcon.removeAttribute('status');
|
|
|
|
syncIcon.disabled = false;
|
2017-07-07 09:18:23 +00:00
|
|
|
stopIcon.hidden = true;
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear status
|
|
|
|
this.setSyncStatus();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the sync icon tooltip message
|
|
|
|
*/
|
|
|
|
this.setSyncStatus = function (msg) {
|
|
|
|
_lastSyncStatus = msg;
|
|
|
|
|
|
|
|
// If a label is registered, update it
|
|
|
|
if (_currentSyncStatusLabel) {
|
|
|
|
_updateSyncStatusLabel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.parseError = function (e) {
|
|
|
|
if (!e) {
|
|
|
|
return { parsed: true };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Already parsed
|
|
|
|
if (e.parsed) {
|
|
|
|
return e;
|
|
|
|
}
|
|
|
|
|
|
|
|
e.parsed = true;
|
|
|
|
e.errorType = e.errorType ? e.errorType : 'error';
|
|
|
|
|
|
|
|
return e;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the state of the sync error icon and add an onclick to populate
|
|
|
|
* the error panel
|
|
|
|
*/
|
|
|
|
this.updateErrorIcon = function (icon, state, errors) {
|
|
|
|
if (!errors || !errors.length) {
|
|
|
|
icon.hidden = true;
|
|
|
|
icon.onclick = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
icon.hidden = false;
|
|
|
|
icon.setAttribute('state', state);
|
|
|
|
var self = this;
|
|
|
|
icon.onclick = function () {
|
|
|
|
var panel = self.updateErrorPanel(this.ownerDocument, errors);
|
|
|
|
panel.openPopup(this, "after_end", 16, 0, false, false);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.updateErrorPanel = function (doc, errors) {
|
|
|
|
var panel = doc.getElementById('zotero-sync-error-panel');
|
|
|
|
|
|
|
|
// Clear existing panel content
|
|
|
|
while (panel.hasChildNodes()) {
|
|
|
|
panel.removeChild(panel.firstChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let e of errors) {
|
|
|
|
var box = doc.createElement('vbox');
|
|
|
|
var label = doc.createElement('label');
|
|
|
|
if (e.libraryID !== undefined) {
|
|
|
|
label.className = "zotero-sync-error-panel-library-name";
|
|
|
|
if (e.libraryID == 0) {
|
|
|
|
var libraryName = Zotero.getString('pane.collections.library');
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
let group = Zotero.Groups.getByLibraryID(e.libraryID);
|
|
|
|
var libraryName = group.name;
|
|
|
|
}
|
|
|
|
label.setAttribute('value', libraryName);
|
|
|
|
}
|
2018-08-12 06:29:58 +00:00
|
|
|
var content = doc.createElement('vbox');
|
2015-07-20 21:27:55 +00:00
|
|
|
var buttons = doc.createElement('hbox');
|
|
|
|
buttons.pack = 'end';
|
|
|
|
box.appendChild(label);
|
|
|
|
box.appendChild(content);
|
|
|
|
box.appendChild(buttons);
|
|
|
|
|
2018-08-12 06:29:58 +00:00
|
|
|
if (e.dialogHeader) {
|
|
|
|
let header = doc.createElement('description');
|
|
|
|
header.className = 'error-header';
|
|
|
|
header.textContent = e.dialogHeader;
|
|
|
|
content.appendChild(header);
|
|
|
|
}
|
|
|
|
|
2019-01-21 01:39:27 +00:00
|
|
|
// Show our own error messages directly
|
2018-08-12 06:29:58 +00:00
|
|
|
var msg;
|
2015-07-20 21:27:55 +00:00
|
|
|
if (e instanceof Zotero.Error) {
|
2018-08-12 06:29:58 +00:00
|
|
|
msg = e.message;
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
// For unexpected ones, just show a generic message
|
2018-08-12 06:29:58 +00:00
|
|
|
else if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.xmlhttp.responseText) {
|
|
|
|
msg = Zotero.Utilities.ellipsize(e.xmlhttp.responseText, 1000, true);
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
else {
|
2018-08-12 06:29:58 +00:00
|
|
|
msg = e.message;
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var desc = doc.createElement('description');
|
|
|
|
desc.textContent = msg;
|
|
|
|
// Make the text selectable
|
|
|
|
desc.setAttribute('style', '-moz-user-select: text; cursor: text');
|
|
|
|
content.appendChild(desc);
|
|
|
|
|
|
|
|
/*// If not an error and there's no explicit button text, don't show
|
|
|
|
// button to report errors
|
2016-04-21 15:08:48 +00:00
|
|
|
if (e.errorType != 'error' && e.dialogButtonText === undefined) {
|
|
|
|
e.dialogButtonText = null;
|
2015-07-20 21:27:55 +00:00
|
|
|
}*/
|
|
|
|
|
2016-04-26 22:12:27 +00:00
|
|
|
if (e.dialogButtonText !== null) {
|
2016-04-21 15:08:48 +00:00
|
|
|
if (e.dialogButtonText === undefined) {
|
2015-07-20 21:27:55 +00:00
|
|
|
var buttonText = Zotero.getString('errorReport.reportError');
|
|
|
|
var buttonCallback = function () {
|
|
|
|
doc.defaultView.ZoteroPane.reportErrors();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else {
|
2016-04-21 15:08:48 +00:00
|
|
|
var buttonText = e.dialogButtonText;
|
|
|
|
var buttonCallback = e.dialogButtonCallback;
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
2021-02-25 21:08:54 +00:00
|
|
|
let button = doc.createElement('button');
|
2015-07-20 21:27:55 +00:00
|
|
|
button.setAttribute('label', buttonText);
|
2016-04-27 07:15:29 +00:00
|
|
|
button.onclick = function () {
|
|
|
|
buttonCallback();
|
|
|
|
panel.hidePopup();
|
|
|
|
};
|
2015-07-20 21:27:55 +00:00
|
|
|
buttons.appendChild(button);
|
2021-02-25 21:08:54 +00:00
|
|
|
|
|
|
|
// Second button
|
|
|
|
if (e.dialogButton2Text) {
|
|
|
|
buttonText = e.dialogButton2Text;
|
|
|
|
buttonCallback = e.dialogButton2Callback;
|
|
|
|
|
|
|
|
let button2 = doc.createElement('button');
|
|
|
|
button2.setAttribute('label', buttonText);
|
|
|
|
button2.onclick = () => {
|
|
|
|
buttonCallback();
|
|
|
|
panel.hidePopup();
|
|
|
|
};
|
|
|
|
buttons.insertBefore(button2, button);
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
panel.appendChild(box)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return panel;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-04-08 21:03:29 +00:00
|
|
|
* Register labels in sync icon tooltip to receive updates
|
2015-07-20 21:27:55 +00:00
|
|
|
*
|
|
|
|
* If no label passed, unregister current label
|
|
|
|
*
|
2016-04-08 21:03:29 +00:00
|
|
|
* @param {Tooltip} [tooltip]
|
2015-07-20 21:27:55 +00:00
|
|
|
*/
|
2016-04-08 21:03:29 +00:00
|
|
|
this.registerSyncStatus = function (tooltip) {
|
|
|
|
if (tooltip) {
|
|
|
|
_currentSyncStatusLabel = tooltip.firstChild.nextSibling;
|
|
|
|
_currentLastSyncLabel = tooltip.firstChild.nextSibling.nextSibling;
|
2021-02-09 21:36:06 +00:00
|
|
|
_currentTooltipMessages = tooltip.querySelector('.sync-button-tooltip-messages');
|
2016-04-08 21:03:29 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
_currentSyncStatusLabel = null;
|
|
|
|
_currentLastSyncLabel = null;
|
2021-02-09 21:36:06 +00:00
|
|
|
_currentTooltipMessages = null;
|
2016-04-08 21:03:29 +00:00
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
if (_currentSyncStatusLabel) {
|
|
|
|
_updateSyncStatusLabel();
|
|
|
|
}
|
|
|
|
}
|
2015-12-02 16:13:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
this.createAPIKeyFromCredentials = Zotero.Promise.coroutine(function* (username, password) {
|
|
|
|
var client = this.getAPIClient();
|
|
|
|
var json = yield client.createAPIKeyFromCredentials(username, password);
|
|
|
|
if (!json) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sanity check
|
|
|
|
if (!json.userID) throw new Error("userID not found in key response");
|
|
|
|
if (!json.username) throw new Error("username not found in key response");
|
|
|
|
if (!json.access) throw new Error("'access' not found in key response");
|
|
|
|
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey(json.key);
|
|
|
|
|
|
|
|
return json;
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.deleteAPIKey = Zotero.Promise.coroutine(function* (){
|
2016-09-05 07:59:42 +00:00
|
|
|
var apiKey = yield Zotero.Sync.Data.Local.getAPIKey();
|
2015-12-02 16:13:29 +00:00
|
|
|
var client = this.getAPIClient({apiKey});
|
|
|
|
Zotero.Sync.Data.Local.setAPIKey();
|
|
|
|
yield client.deleteAPIKey();
|
|
|
|
})
|
2021-02-09 21:36:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
function _addTooltipMessage(msg) {
|
|
|
|
_tooltipMessages.push(msg.replace(/\n+/g, ' '));
|
|
|
|
};
|
|
|
|
|
2015-07-20 21:27:55 +00:00
|
|
|
|
|
|
|
function _updateSyncStatusLabel() {
|
|
|
|
if (_lastSyncStatus) {
|
|
|
|
_currentSyncStatusLabel.value = _lastSyncStatus;
|
|
|
|
_currentSyncStatusLabel.hidden = false;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
_currentSyncStatusLabel.hidden = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Always update last sync time
|
|
|
|
var lastSyncTime = Zotero.Sync.Data.Local.getLastSyncTime();
|
|
|
|
if (!lastSyncTime) {
|
|
|
|
try {
|
2015-10-29 07:41:54 +00:00
|
|
|
lastSyncTime = Zotero.Sync.Data.Local.getLastClassicSyncTime();
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
Zotero.debug(e, 2);
|
|
|
|
Components.utils.reportError(e);
|
|
|
|
_currentLastSyncLabel.hidden = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (lastSyncTime) {
|
|
|
|
var msg = Zotero.Date.toRelativeDate(lastSyncTime);
|
|
|
|
}
|
|
|
|
// Don't show "Not yet synced" if a sync is in progress
|
|
|
|
else if (_syncInProgress) {
|
|
|
|
_currentLastSyncLabel.hidden = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
var msg = Zotero.getString('sync.status.notYetSynced');
|
|
|
|
}
|
|
|
|
|
|
|
|
_currentLastSyncLabel.value = Zotero.getString('sync.status.lastSync') + " " + msg;
|
|
|
|
_currentLastSyncLabel.hidden = false;
|
2021-02-09 21:36:06 +00:00
|
|
|
|
|
|
|
if (_tooltipMessages.length) {
|
|
|
|
_currentTooltipMessages.textContent = '';
|
|
|
|
for (let message of _tooltipMessages) {
|
|
|
|
let elem = _currentTooltipMessages.ownerDocument.createElementNS(HTML_NS, 'p');
|
|
|
|
elem.textContent = message;
|
|
|
|
_currentTooltipMessages.appendChild(elem);
|
|
|
|
}
|
|
|
|
_currentTooltipMessages.hidden = false;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
_currentTooltipMessages.hidden = true;
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|
2015-11-02 08:22:37 +00:00
|
|
|
|
|
|
|
|
2016-09-05 07:59:42 +00:00
|
|
|
var _getAPIKey = Zotero.Promise.method(function () {
|
2016-09-05 07:20:29 +00:00
|
|
|
// Set as .apiKey on Runner in tests or set in login manager
|
|
|
|
return _apiKey || Zotero.Sync.Data.Local.getAPIKey()
|
2015-11-02 08:22:37 +00:00
|
|
|
})
|
2017-07-07 09:18:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
function _stopCheck() {
|
|
|
|
if (_stopping) {
|
|
|
|
throw new Zotero.Sync.UserCancelledException;
|
|
|
|
}
|
|
|
|
}
|
2019-09-16 05:08:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
function _cancellerReceiver(canceller) {
|
|
|
|
_canceller = canceller;
|
|
|
|
}
|
2015-07-20 21:27:55 +00:00
|
|
|
}
|