/*
***** 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 .
***** END LICENSE BLOCK *****
*/
Zotero.Sync = new function() {
// Keep in sync with syncObjectTypes table
this.__defineGetter__('syncObjects', function () {
return {
creator: {
singular: 'Creator',
plural: 'Creators'
},
item: {
singular: 'Item',
plural: 'Items'
},
collection: {
singular: 'Collection',
plural: 'Collections'
},
search: {
singular: 'Search',
plural: 'Searches'
},
tag: {
singular: 'Tag',
plural: 'Tags'
},
relation: {
singular: 'Relation',
plural: 'Relations'
},
setting: {
singular: 'Setting',
plural: 'Settings'
},
fulltext: {
singular: 'Fulltext',
plural: 'Fulltexts'
}
};
});
}
/**
* Methods for syncing with the Zotero Server
*/
Zotero.Sync.Server = new function () {
this.canAutoResetClient = true;
this.manualSyncRequired = false;
this.upgradeRequired = false;
this.nextLocalSyncDate = false;
function clear(callback) {
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
Zotero.Sync.Server.login()
.then(function () {
Zotero.Sync.Server.clear(callback);
})
.done();
return;
}
var url = _serverURL + "clear";
var body = _apiVersionComponent
+ '&' + Zotero.Sync.Server.sessionIDComponent;
Zotero.HTTP.doPost(url, body, function (xmlhttp) {
if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
Zotero.Sync.Server.login()
.then(function () {
Zotero.Sync.Server.clear(callback);
})
.done();
return;
}
_checkResponse(xmlhttp);
var response = xmlhttp.responseXML.childNodes[0];
if (response.firstChild.tagName == 'error') {
_error(response.firstChild.firstChild.nodeValue);
}
if (response.firstChild.tagName != 'cleared') {
_error('Invalid response from server', xmlhttp.responseText);
}
Zotero.Sync.Server.resetClient();
if (callback) {
callback();
}
});
}
function resetClient() {
Zotero.debug("Resetting client");
Zotero.DB.beginTransaction();
var sql = "DELETE FROM version WHERE schema IN "
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql);
var sql = "DELETE FROM version WHERE schema IN "
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql);
Zotero.DB.query("DELETE FROM syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog");
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
var sql = "UPDATE syncedSettings SET synced=0";
Zotero.DB.query(sql);
Zotero.DB.commitTransaction();
}
function _checkResponse(xmlhttp, noReloadOnFailure) {
if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] ||
xmlhttp.responseXML.childNodes[0].tagName != 'response' ||
!xmlhttp.responseXML.childNodes[0].firstChild) {
Zotero.debug(xmlhttp.responseText);
_error(Zotero.getString('general.invalidResponseServer') + Zotero.getString('general.tryAgainLater'),
xmlhttp.responseText, noReloadOnFailure);
}
var firstChild = xmlhttp.responseXML.firstChild.firstChild;
if (firstChild.localName == 'error') {
// Don't automatically retry 400 errors
if (xmlhttp.status >= 400 && xmlhttp.status < 500 && !_invalidSession(xmlhttp)) {
Zotero.debug("Server returned " + xmlhttp.status + " -- manual sync required", 2);
Zotero.Sync.Server.manualSyncRequired = true;
}
else {
Zotero.debug("Server returned " + xmlhttp.status, 3);
}
switch (firstChild.getAttribute('code')) {
case 'INVALID_UPLOAD_DATA':
// On the off-chance that this error is due to invalid characters
// in a filename, check them all (since getting a more specific
// error from the server would be difficult)
var sql = "SELECT itemID FROM itemAttachments WHERE linkMode IN (?,?)";
var ids = Zotero.DB.columnQuery(sql, [Zotero.Attachments.LINK_MODE_IMPORTED_FILE, Zotero.Attachments.LINK_MODE_IMPORTED_URL]);
if (ids) {
var items = Zotero.Items.get(ids);
var rolledBack = false;
for (let item of items) {
var file = item.getFile();
if (!file) {
continue;
}
try {
var fn = file.leafName;
// TODO: move stripping logic (copied from _xmlize()) to Utilities
var xmlfn = file.leafName.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '');
if (fn != xmlfn) {
if (!rolledBack) {
Zotero.DB.rollbackAllTransactions();
}
Zotero.debug("Changing invalid filename to " + xmlfn);
item.renameAttachmentFile(xmlfn);
}
}
catch (e) {
Zotero.debug(e);
Components.utils.reportError(e);
}
}
}
// Make sure this isn't due to relations using a local user key
//
// TEMP: This can be removed once a DB upgrade step is added
try {
var sql = "SELECT libraryID FROM relations WHERE libraryID LIKE 'local/%' LIMIT 1";
var repl = Zotero.DB.valueQuery(sql);
if (repl) {
Zotero.Relations.updateUser(repl, repl, Zotero.userID, Zotero.libraryID);
}
}
catch (e) {
Components.utils.reportError(e);
Zotero.debug(e);
}
break;
case 'FULL_SYNC_REQUIRED':
// Let current sync fail, and then do a full sync
var background = Zotero.Sync.Runner.background;
setTimeout(function () {
if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
Components.utils.reportError("Skipping automatic client reset due to debug pref");
return;
}
if (!Zotero.Sync.Server.canAutoResetClient) {
Components.utils.reportError("Client has already been auto-reset in Zotero.Sync.Server._checkResponse()");
return;
}
Zotero.Sync.Server.resetClient();
Zotero.Sync.Server.canAutoResetClient = false;
Zotero.Sync.Runner.sync({
background: background
});
}, 1);
break;
case 'LIBRARY_ACCESS_DENIED':
var background = Zotero.Sync.Runner.background;
setTimeout(function () {
var libraryID = parseInt(firstChild.getAttribute('libraryID'));
try {
var group = Zotero.Groups.getByLibraryID(libraryID);
}
catch (e) {
// Not sure how this is possible, but it's affecting some people
// TODO: Clean up in schema updates with FK check
if (!Zotero.Libraries.exists(libraryID)) {
let sql = "DELETE FROM syncedSettings WHERE libraryID=?";
Zotero.DB.query(sql, libraryID);
return;
}
}
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)
+ ps.BUTTON_DELAY_ENABLE;
var index = ps.confirmEx(
null,
Zotero.getString('general.warning'),
Zotero.getString('sync.error.writeAccessLost', group.name) + "\n\n"
+ Zotero.getString('sync.error.groupWillBeReset') + "\n\n"
+ Zotero.getString('sync.error.copyChangedItems'),
buttonFlags,
Zotero.getString('sync.resetGroupAndSync'),
null, null, null, {}
);
if (index == 0) {
group.erase();
Zotero.Sync.Server.resetClient();
Zotero.Sync.Storage.resetAllSyncStates();
Zotero.Sync.Runner.sync();
return;
}
}, 1);
break;
// We can't reproduce it, but we can fix it
case 'WRONG_LIBRARY_TAG_ITEM':
var background = Zotero.Sync.Runner.background;
setTimeout(function () {
var sql = "CREATE TEMPORARY TABLE tmpWrongLibraryTags AS "
+ "SELECT itemTags.ROWID AS tagRowID, tagID, name, itemID, "
+ "IFNULL(tags.libraryID,0) AS tagLibraryID, "
+ "IFNULL(items.libraryID,0) AS itemLibraryID FROM tags "
+ "NATURAL JOIN itemTags JOIN items USING (itemID) "
+ "WHERE IFNULL(tags.libraryID, 0)!=IFNULL(items.libraryID,0)";
Zotero.DB.query(sql);
sql = "SELECT COUNT(*) FROM tmpWrongLibraryTags";
var badTags = !!Zotero.DB.valueQuery(sql);
if (badTags) {
sql = "DELETE FROM itemTags WHERE ROWID IN (SELECT tagRowID FROM tmpWrongLibraryTags)";
Zotero.DB.query(sql);
}
Zotero.DB.query("DROP TABLE tmpWrongLibraryTags");
// If error was actually due to a missing item, do a Full Sync
if (!badTags) {
if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
Components.utils.reportError("Skipping automatic client reset due to debug pref");
return;
}
if (!Zotero.Sync.Server.canAutoResetClient) {
Components.utils.reportError("Client has already been auto-reset in Zotero.Sync.Server._checkResponse()");
return;
}
Zotero.Sync.Server.resetClient();
Zotero.Sync.Server.canAutoResetClient = false;
}
Zotero.Sync.Runner.sync({
background: background
});
}, 1);
break;
case 'INVALID_TIMESTAMP':
var validClock = Zotero.DB.valueQuery("SELECT CURRENT_TIMESTAMP BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'");
if (!validClock) {
_error(Zotero.getString('sync.error.invalidClock'));
}
setTimeout(function () {
Zotero.DB.beginTransaction();
var types = ['collections', 'creators', 'items', 'savedSearches', 'tags'];
for (let type of types) {
var sql = "UPDATE " + type + " SET dateAdded=CURRENT_TIMESTAMP "
+ "WHERE dateAdded NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
Zotero.DB.query(sql);
var sql = "UPDATE " + type + " SET dateModified=CURRENT_TIMESTAMP "
+ "WHERE dateModified NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
Zotero.DB.query(sql);
var sql = "UPDATE " + type + " SET clientDateModified=CURRENT_TIMESTAMP "
+ "WHERE clientDateModified NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
Zotero.DB.query(sql);
}
Zotero.DB.commitTransaction();
}, 1);
break;
case 'UPGRADE_REQUIRED':
Zotero.Sync.Server.upgradeRequired = true;
break;
}
}
}
/**
* @private
* @param {DOMElement} response
* @param {Function} callback
*/
function _checkServerLock(response, callback) {
_checkTimer = null;
var mode;
switch (response.firstChild.localName) {
case 'queued':
mode = 'queued';
break;
case 'locked':
mode = 'locked';
break;
default:
return false;
}
if (mode == 'queued') {
var msg = "Upload queued";
}
else {
var msg = "Associated libraries are locked";
}
var wait = parseInt(response.firstChild.getAttribute('wait'));
if (!wait || isNaN(wait)) {
wait = 5000;
}
Zotero.debug(msg + " -- waiting " + wait + "ms before next check");
_checkTimer = setTimeout(function () { callback(mode); }, wait);
return true;
}
}
Zotero.Sync.Server.Data = new function() {
/**
* @param {String} itemTypes
* @param {String} localName
* @param {String} remoteName
* @param {Boolean} [remoteMoreRecent=false]
*/
function _generateAutoChangeAlertMessage(itemTypes, localName, remoteName, remoteMoreRecent) {
if (localName === null) {
var localDelete = true;
}
else if (remoteName === null) {
var remoteDelete = true;
}
var msg = Zotero.getString('sync.conflict.autoChange.alert', itemTypes) + " ";
if (localDelete) {
msg += Zotero.getString('sync.conflict.remoteVersionsKept');
}
else if (remoteDelete) {
msg += Zotero.getString('sync.conflict.localVersionsKept');
}
else {
msg += Zotero.getString('sync.conflict.recentVersionsKept');
}
msg += "\n\n" + Zotero.getString('sync.conflict.viewErrorConsole',
(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
return msg;
}
/**
* @param {String} itemType
* @param {String} localName
* @param {String} remoteName
* @param {Boolean} [remoteMoreRecent=false]
*/
function _generateAutoChangeLogMessage(itemType, localName, remoteName, remoteMoreRecent) {
if (localName === null) {
localName = Zotero.getString('sync.conflict.deleted');
var localDelete = true;
}
else if (remoteName === null) {
remoteName = Zotero.getString('sync.conflict.deleted');
var remoteDelete = true;
}
var msg = Zotero.getString('sync.conflict.autoChange.log', itemType) + "\n\n";
msg += Zotero.getString('sync.conflict.localVersion', localName) + "\n";
msg += Zotero.getString('sync.conflict.remoteVersion', remoteName);
msg += "\n\n";
if (localDelete) {
msg += Zotero.getString('sync.conflict.remoteVersionKept');
}
else if (remoteDelete) {
msg += Zotero.getString('sync.conflict.localVersionKept');
}
else {
var moreRecent = remoteMoreRecent ? remoteName : localName;
msg += Zotero.getString('sync.conflict.recentVersionKept', moreRecent);
}
return msg;
}
function _generateCollectionItemMergeAlertMessage() {
var msg = Zotero.getString('sync.conflict.collectionItemMerge.alert') + "\n\n"
+ Zotero.getString('sync.conflict.viewErrorConsole',
(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
return msg;
}
/**
* @param {String} collectionName
* @param {Integer[]} addedItemIDs
*/
function _generateCollectionItemMergeLogMessage(collectionName, addedItemIDs) {
var introMsg = Zotero.getString('sync.conflict.collectionItemMerge.log', collectionName);
var itemText = [];
var max = addedItemIDs.length;
for (var i=0; i 20) {
itemText.push(" \u2022 ...");
break;
}
}
return introMsg + "\n\n" + itemText.join("\n");
}
function _generateTagItemMergeAlertMessage() {
var msg = Zotero.getString('sync.conflict.tagItemMerge.alert') + "\n\n"
+ Zotero.getString('sync.conflict.viewErrorConsole',
(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
return msg;
}
/**
* @param {String} tagName
* @param {Integer[]} addedItemIDs
* @param {Boolean} remoteIsTarget
*/
function _generateTagItemMergeLogMessage(tagName, addedItemIDs, remoteIsTarget) {
var introMsg = Zotero.getString('sync.conflict.tagItemMerge.log', tagName) + " ";
if (remoteIsTarget) {
introMsg += Zotero.getString('sync.conflict.tag.addedToRemote');
}
else {
introMsg += Zotero.getString('sync.conflict.tag.addedToLocal');
}
var itemText = [];
for (let id of addedItemIDs) {
var item = Zotero.Items.get(id);
var title = item.getField('title');
var text = " - " + title;
var firstCreator = item.getField('firstCreator');
if (firstCreator) {
text += " (" + firstCreator + ")";
}
itemText.push(text);
}
return introMsg + "\n\n" + itemText.join("\n");
}
function _xmlize(str) {
return str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
}
}