www

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

sync.js (16468B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 
     27 Zotero.Sync = new function() {
     28 	// Keep in sync with syncObjectTypes table
     29 	this.__defineGetter__('syncObjects', function () {
     30 		return {
     31 			creator: {
     32 				singular: 'Creator',
     33 				plural: 'Creators'
     34 			},
     35 			item: {
     36 				singular: 'Item',
     37 				plural: 'Items'
     38 			},
     39 			collection: {
     40 				singular: 'Collection',
     41 				plural: 'Collections'
     42 			},
     43 			search: {
     44 				singular: 'Search',
     45 				plural: 'Searches'
     46 			},
     47 			tag: {
     48 				singular: 'Tag',
     49 				plural: 'Tags'
     50 			},
     51 			relation: {
     52 				singular: 'Relation',
     53 				plural: 'Relations'
     54 			},
     55 			setting: {
     56 				singular: 'Setting',
     57 				plural: 'Settings'
     58 			},
     59 			fulltext: {
     60 				singular: 'Fulltext',
     61 				plural: 'Fulltexts'
     62 			}
     63 		};
     64 	});
     65 }
     66 
     67 
     68 /**
     69  * Methods for syncing with the Zotero Server
     70  */
     71 Zotero.Sync.Server = new function () {
     72 	this.canAutoResetClient = true;
     73 	this.manualSyncRequired = false;
     74 	this.upgradeRequired = false;
     75 	this.nextLocalSyncDate = false;
     76 	
     77 	function clear(callback) {
     78 		if (!_sessionID) {
     79 			Zotero.debug("Session ID not available -- logging in");
     80 			Zotero.Sync.Server.login()
     81 			.then(function () {
     82 				Zotero.Sync.Server.clear(callback);
     83 			})
     84 			.done();
     85 			return;
     86 		}
     87 		
     88 		var url = _serverURL + "clear";
     89 		var body = _apiVersionComponent
     90 					+ '&' + Zotero.Sync.Server.sessionIDComponent;
     91 		
     92 		Zotero.HTTP.doPost(url, body, function (xmlhttp) {
     93 			if (_invalidSession(xmlhttp)) {
     94 				Zotero.debug("Invalid session ID -- logging in");
     95 				_sessionID = false;
     96 				Zotero.Sync.Server.login()
     97 				.then(function () {
     98 					Zotero.Sync.Server.clear(callback);
     99 				})
    100 				.done();
    101 				return;
    102 			}
    103 			
    104 			_checkResponse(xmlhttp);
    105 			
    106 			var response = xmlhttp.responseXML.childNodes[0];
    107 			
    108 			if (response.firstChild.tagName == 'error') {
    109 				_error(response.firstChild.firstChild.nodeValue);
    110 			}
    111 			
    112 			if (response.firstChild.tagName != 'cleared') {
    113 				_error('Invalid response from server', xmlhttp.responseText);
    114 			}
    115 			
    116 			Zotero.Sync.Server.resetClient();
    117 			
    118 			if (callback) {
    119 				callback();
    120 			}
    121 		});
    122 	}
    123 	
    124 	
    125 	function resetClient() {
    126 		Zotero.debug("Resetting client");
    127 		
    128 		Zotero.DB.beginTransaction();
    129 		
    130 		var sql = "DELETE FROM version WHERE schema IN "
    131 			+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
    132 		Zotero.DB.query(sql);
    133 		
    134 		var sql = "DELETE FROM version WHERE schema IN "
    135 			+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
    136 		Zotero.DB.query(sql);
    137 		
    138 		Zotero.DB.query("DELETE FROM syncDeleteLog");
    139 		Zotero.DB.query("DELETE FROM storageDeleteLog");
    140 		
    141 		sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
    142 		Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
    143 		
    144 		var sql = "UPDATE syncedSettings SET synced=0";
    145 		Zotero.DB.query(sql);
    146 		
    147 		Zotero.DB.commitTransaction();
    148 	}
    149 	
    150 	
    151 	function _checkResponse(xmlhttp, noReloadOnFailure) {
    152 		
    153 		
    154 		if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] ||
    155 				xmlhttp.responseXML.childNodes[0].tagName != 'response' ||
    156 				!xmlhttp.responseXML.childNodes[0].firstChild) {
    157 			Zotero.debug(xmlhttp.responseText);
    158 			_error(Zotero.getString('general.invalidResponseServer') + Zotero.getString('general.tryAgainLater'),
    159 				xmlhttp.responseText, noReloadOnFailure);
    160 		}
    161 		
    162 		var firstChild = xmlhttp.responseXML.firstChild.firstChild;
    163 		
    164 		if (firstChild.localName == 'error') {
    165 			// Don't automatically retry 400 errors
    166 			if (xmlhttp.status >= 400 && xmlhttp.status < 500 && !_invalidSession(xmlhttp)) {
    167 				Zotero.debug("Server returned " + xmlhttp.status + " -- manual sync required", 2);
    168 				Zotero.Sync.Server.manualSyncRequired = true;
    169 			}
    170 			else {
    171 				Zotero.debug("Server returned " + xmlhttp.status, 3);
    172 			}
    173 			
    174 			switch (firstChild.getAttribute('code')) {
    175 				case 'INVALID_UPLOAD_DATA':
    176 					// On the off-chance that this error is due to invalid characters
    177 					// in a filename, check them all (since getting a more specific
    178 					// error from the server would be difficult)
    179 					var sql = "SELECT itemID FROM itemAttachments WHERE linkMode IN (?,?)";
    180 					var ids = Zotero.DB.columnQuery(sql, [Zotero.Attachments.LINK_MODE_IMPORTED_FILE, Zotero.Attachments.LINK_MODE_IMPORTED_URL]);
    181 					if (ids) {
    182 						var items = Zotero.Items.get(ids);
    183 						var rolledBack = false;
    184 						for (let item of items) {
    185 							var file = item.getFile();
    186 							if (!file) {
    187 								continue;
    188 							}
    189 							try {
    190 								var fn = file.leafName;
    191 								// TODO: move stripping logic (copied from _xmlize()) to Utilities
    192 								var xmlfn = file.leafName.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '');
    193 								if (fn != xmlfn) {
    194 									if (!rolledBack) {
    195 										Zotero.DB.rollbackAllTransactions();
    196 									}
    197 									Zotero.debug("Changing invalid filename to " + xmlfn);
    198 									item.renameAttachmentFile(xmlfn);
    199 								}
    200 							}
    201 							catch (e) {
    202 								Zotero.debug(e);
    203 								Components.utils.reportError(e);
    204 							}
    205 						}
    206 					}
    207 					
    208 					// Make sure this isn't due to relations using a local user key
    209 					//
    210 					// TEMP: This can be removed once a DB upgrade step is added
    211 					try {
    212 						var sql = "SELECT libraryID FROM relations WHERE libraryID LIKE 'local/%' LIMIT 1";
    213 						var repl = Zotero.DB.valueQuery(sql);
    214 						if (repl) {
    215 							Zotero.Relations.updateUser(repl, repl, Zotero.userID, Zotero.libraryID);
    216 						}
    217 					}
    218 					catch (e) {
    219 						Components.utils.reportError(e);
    220 						Zotero.debug(e);
    221 					}
    222 					break;
    223 				
    224 				case 'FULL_SYNC_REQUIRED':
    225 					// Let current sync fail, and then do a full sync
    226 					var background = Zotero.Sync.Runner.background;
    227 					setTimeout(function () {
    228 						if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
    229 							Components.utils.reportError("Skipping automatic client reset due to debug pref");
    230 							return;
    231 						}
    232 						if (!Zotero.Sync.Server.canAutoResetClient) {
    233 							Components.utils.reportError("Client has already been auto-reset in Zotero.Sync.Server._checkResponse()");
    234 							return;
    235 						}
    236 						
    237 						Zotero.Sync.Server.resetClient();
    238 						Zotero.Sync.Server.canAutoResetClient = false;
    239 						Zotero.Sync.Runner.sync({
    240 							background: background
    241 						});
    242 					}, 1);
    243 					break;
    244 				
    245 				case 'LIBRARY_ACCESS_DENIED':
    246 					var background = Zotero.Sync.Runner.background;
    247 					setTimeout(function () {
    248 						var libraryID = parseInt(firstChild.getAttribute('libraryID'));
    249 						
    250 						try {
    251 							var group = Zotero.Groups.getByLibraryID(libraryID);
    252 						}
    253 						catch (e) {
    254 							// Not sure how this is possible, but it's affecting some people
    255 							// TODO: Clean up in schema updates with FK check
    256 							if (!Zotero.Libraries.exists(libraryID)) {
    257 								let sql = "DELETE FROM syncedSettings WHERE libraryID=?";
    258 								Zotero.DB.query(sql, libraryID);
    259 								return;
    260 							}
    261 						}
    262 						
    263 						var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    264 												.getService(Components.interfaces.nsIPromptService);
    265 						var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    266 										+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    267 										+ ps.BUTTON_DELAY_ENABLE;
    268 						var index = ps.confirmEx(
    269 							null,
    270 							Zotero.getString('general.warning'),
    271 							Zotero.getString('sync.error.writeAccessLost', group.name) + "\n\n"
    272 								+ Zotero.getString('sync.error.groupWillBeReset') + "\n\n"
    273 								+ Zotero.getString('sync.error.copyChangedItems'),
    274 							buttonFlags,
    275 							Zotero.getString('sync.resetGroupAndSync'),
    276 							null, null, null, {}
    277 						);
    278 						
    279 						if (index == 0) {
    280 							group.erase();
    281 							Zotero.Sync.Server.resetClient();
    282 							Zotero.Sync.Storage.resetAllSyncStates();
    283 							Zotero.Sync.Runner.sync();
    284 							return;
    285 						}
    286 					}, 1);
    287 					break;
    288 				
    289 				
    290 				// We can't reproduce it, but we can fix it
    291 				case 'WRONG_LIBRARY_TAG_ITEM':
    292 					var background = Zotero.Sync.Runner.background;
    293 					setTimeout(function () {
    294 						var sql = "CREATE TEMPORARY TABLE tmpWrongLibraryTags AS "
    295 							+ "SELECT itemTags.ROWID AS tagRowID, tagID, name, itemID, "
    296 							+ "IFNULL(tags.libraryID,0) AS tagLibraryID, "
    297 							+ "IFNULL(items.libraryID,0) AS itemLibraryID FROM tags "
    298 							+ "NATURAL JOIN itemTags JOIN items USING (itemID) "
    299 							+ "WHERE IFNULL(tags.libraryID, 0)!=IFNULL(items.libraryID,0)";
    300 						Zotero.DB.query(sql);
    301 						
    302 						sql = "SELECT COUNT(*) FROM tmpWrongLibraryTags";
    303 						var badTags = !!Zotero.DB.valueQuery(sql);
    304 						
    305 						if (badTags) {
    306 							sql = "DELETE FROM itemTags WHERE ROWID IN (SELECT tagRowID FROM tmpWrongLibraryTags)";
    307 							Zotero.DB.query(sql);
    308 						}
    309 						
    310 						Zotero.DB.query("DROP TABLE tmpWrongLibraryTags");
    311 						
    312 						// If error was actually due to a missing item, do a Full Sync
    313 						if (!badTags) {
    314 							if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
    315 								Components.utils.reportError("Skipping automatic client reset due to debug pref");
    316 								return;
    317 							}
    318 							if (!Zotero.Sync.Server.canAutoResetClient) {
    319 								Components.utils.reportError("Client has already been auto-reset in Zotero.Sync.Server._checkResponse()");
    320 								return;
    321 							}
    322 							
    323 							Zotero.Sync.Server.resetClient();
    324 							Zotero.Sync.Server.canAutoResetClient = false;
    325 						}
    326 						
    327 						Zotero.Sync.Runner.sync({
    328 							background: background
    329 						});
    330 					}, 1);
    331 					break;
    332 				
    333 				case 'INVALID_TIMESTAMP':
    334 					var validClock = Zotero.DB.valueQuery("SELECT CURRENT_TIMESTAMP BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'");
    335 					if (!validClock) {
    336 						_error(Zotero.getString('sync.error.invalidClock'));
    337 					}
    338 					
    339 					setTimeout(function () {
    340 						Zotero.DB.beginTransaction();
    341 						
    342 						var types = ['collections', 'creators', 'items', 'savedSearches', 'tags'];
    343 						for (let type of types) {
    344 							var sql = "UPDATE " + type + " SET dateAdded=CURRENT_TIMESTAMP "
    345 									+ "WHERE dateAdded NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
    346 							Zotero.DB.query(sql);
    347 							var sql = "UPDATE " + type + " SET dateModified=CURRENT_TIMESTAMP "
    348 									+ "WHERE dateModified NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
    349 							Zotero.DB.query(sql);
    350 							var sql = "UPDATE " + type + " SET clientDateModified=CURRENT_TIMESTAMP "
    351 									+ "WHERE clientDateModified NOT BETWEEN '1970-01-01 00:00:01' AND '2038-01-19 03:14:07'";
    352 							Zotero.DB.query(sql);
    353 						}
    354 						
    355 						Zotero.DB.commitTransaction();
    356 					}, 1);
    357 					break;
    358 				
    359 				case 'UPGRADE_REQUIRED':
    360 					Zotero.Sync.Server.upgradeRequired = true;
    361 					break;
    362 			}
    363 		}
    364 	}
    365 	
    366 	
    367 	/**
    368 	 * @private
    369 	 * @param	{DOMElement}	response
    370 	 * @param	{Function}		callback
    371 	 */
    372 	function _checkServerLock(response, callback) {
    373 		_checkTimer = null;
    374 		
    375 		var mode;
    376 		
    377 		switch (response.firstChild.localName) {
    378 			case 'queued':
    379 				mode = 'queued';
    380 				break;
    381 			
    382 			case 'locked':
    383 				mode = 'locked';
    384 				break;
    385 				
    386 			default:
    387 				return false;
    388 		}
    389 		
    390 		if (mode == 'queued') {
    391 			var msg = "Upload queued";
    392 		}
    393 		else {
    394 			var msg = "Associated libraries are locked";
    395 		}
    396 		
    397 		var wait = parseInt(response.firstChild.getAttribute('wait'));
    398 		if (!wait || isNaN(wait)) {
    399 			wait = 5000;
    400 		}
    401 		Zotero.debug(msg + " -- waiting " + wait + "ms before next check");
    402 		_checkTimer = setTimeout(function () { callback(mode); }, wait);
    403 		return true;
    404 	}
    405 }
    406 
    407 
    408 Zotero.Sync.Server.Data = new function() {
    409 	/**
    410 	 * @param	{String}	itemTypes
    411 	 * @param	{String}	localName
    412 	 * @param	{String}	remoteName
    413 	 * @param	{Boolean}	[remoteMoreRecent=false]
    414 	 */
    415 	function _generateAutoChangeAlertMessage(itemTypes, localName, remoteName, remoteMoreRecent) {
    416 		if (localName === null) {
    417 			var localDelete = true;
    418 		}
    419 		else if (remoteName === null) {
    420 			var remoteDelete = true;
    421 		}
    422 		
    423 		var msg = Zotero.getString('sync.conflict.autoChange.alert', itemTypes) + " ";
    424 		if (localDelete) {
    425 			msg += Zotero.getString('sync.conflict.remoteVersionsKept');
    426 		}
    427 		else if (remoteDelete) {
    428 			msg += Zotero.getString('sync.conflict.localVersionsKept');
    429 		}
    430 		else {
    431 			msg += Zotero.getString('sync.conflict.recentVersionsKept');
    432 		}
    433 		msg += "\n\n" + Zotero.getString('sync.conflict.viewErrorConsole',
    434 				(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
    435 		return msg;
    436 	}
    437 	
    438 	
    439 	/**
    440 	 * @param	{String}	itemType
    441 	 * @param	{String}	localName
    442 	 * @param	{String}	remoteName
    443 	 * @param	{Boolean}	[remoteMoreRecent=false]
    444 	 */
    445 	function _generateAutoChangeLogMessage(itemType, localName, remoteName, remoteMoreRecent) {
    446 		if (localName === null) {
    447 			localName = Zotero.getString('sync.conflict.deleted');
    448 			var localDelete = true;
    449 		}
    450 		else if (remoteName === null) {
    451 			remoteName = Zotero.getString('sync.conflict.deleted');
    452 			var remoteDelete = true;
    453 		}
    454 		
    455 		var msg = Zotero.getString('sync.conflict.autoChange.log', itemType) + "\n\n";
    456 		msg += Zotero.getString('sync.conflict.localVersion', localName) + "\n";
    457 		msg += Zotero.getString('sync.conflict.remoteVersion', remoteName);
    458 		msg += "\n\n";
    459 		if (localDelete) {
    460 			msg += Zotero.getString('sync.conflict.remoteVersionKept');
    461 		}
    462 		else if (remoteDelete) {
    463 			msg += Zotero.getString('sync.conflict.localVersionKept');
    464 		}
    465 		else {
    466 			var moreRecent = remoteMoreRecent ? remoteName : localName;
    467 			msg += Zotero.getString('sync.conflict.recentVersionKept', moreRecent);
    468 		}
    469 		return msg;
    470 	}
    471 	
    472 	
    473 	function _generateCollectionItemMergeAlertMessage() {
    474 		var msg = Zotero.getString('sync.conflict.collectionItemMerge.alert') + "\n\n"
    475 			+ Zotero.getString('sync.conflict.viewErrorConsole',
    476 				(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
    477 		return msg;
    478 	}
    479 	
    480 	
    481 	/**
    482 	 * @param	{String}		collectionName
    483 	 * @param	{Integer[]}		addedItemIDs
    484 	 */
    485 	function _generateCollectionItemMergeLogMessage(collectionName, addedItemIDs) {
    486 		var introMsg = Zotero.getString('sync.conflict.collectionItemMerge.log', collectionName);
    487 		var itemText = [];
    488 		var max = addedItemIDs.length;
    489 		for (var i=0; i<max; i++) {
    490 			var id = addedItemIDs[i];
    491 			var item = Zotero.Items.get(id);
    492 			var title = item.getDisplayTitle();
    493 			var text = " \u2022 " + title;
    494 			var firstCreator = item.getField('firstCreator');
    495 			if (firstCreator) {
    496 				text += " (" + firstCreator + ")";
    497 			}
    498 			itemText.push(text);
    499 			
    500 			if (i == 19 && max > 20) {
    501 				itemText.push(" \u2022 ...");
    502 				break;
    503 			}
    504 		}
    505 		return introMsg + "\n\n" + itemText.join("\n");
    506 	}
    507 	
    508 	
    509 	function _generateTagItemMergeAlertMessage() {
    510 		var msg = Zotero.getString('sync.conflict.tagItemMerge.alert') + "\n\n"
    511 			+ Zotero.getString('sync.conflict.viewErrorConsole',
    512 				(Zotero.isStandalone ? "" : "Firefox")).replace(/\s+/, " ");
    513 		return msg;
    514 	}
    515 	
    516 	
    517 	/**
    518 	 * @param	{String}		tagName
    519 	 * @param	{Integer[]}		addedItemIDs
    520 	 * @param	{Boolean}		remoteIsTarget
    521 	 */
    522 	function _generateTagItemMergeLogMessage(tagName, addedItemIDs, remoteIsTarget) {
    523 		var introMsg = Zotero.getString('sync.conflict.tagItemMerge.log', tagName) + " ";
    524 		
    525 		if (remoteIsTarget) {
    526 			introMsg += Zotero.getString('sync.conflict.tag.addedToRemote');
    527 		}
    528 		else {
    529 			introMsg += Zotero.getString('sync.conflict.tag.addedToLocal');
    530 		}
    531 		var itemText = [];
    532 		for (let id of addedItemIDs) {
    533 			var item = Zotero.Items.get(id);
    534 			var title = item.getField('title');
    535 			var text = " - " + title;
    536 			var firstCreator = item.getField('firstCreator');
    537 			if (firstCreator) {
    538 				text += " (" + firstCreator + ")";
    539 			}
    540 			itemText.push(text);
    541 		}
    542 		return introMsg + "\n\n" + itemText.join("\n");
    543 	}
    544 	
    545 	
    546 	function _xmlize(str) {
    547 		return str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
    548 	}
    549 }