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 }