schema.js (127018B)
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 Zotero.Schema = new function(){ 27 this.dbInitialized = false; 28 this.goToChangeLog = false; 29 30 this.REPO_UPDATE_MANUAL = 1; 31 this.REPO_UPDATE_UPGRADE = 2; 32 this.REPO_UPDATE_STARTUP = 3; 33 this.REPO_UPDATE_NOTIFICATION = 4; 34 35 var _schemaUpdateDeferred = Zotero.Promise.defer(); 36 this.schemaUpdatePromise = _schemaUpdateDeferred.promise; 37 38 // If updating from this userdata version or later, don't show "Upgrading database…" and don't make 39 // DB backup first. This should be set to false when breaking compatibility or making major changes. 40 const minorUpdateFrom = 95; 41 42 var _dbVersions = []; 43 var _schemaVersions = []; 44 // Update when adding _updateCompatibility() line to schema update step 45 var _maxCompatibility = 5; 46 47 var _repositoryTimerID; 48 var _repositoryNotificationTimerID; 49 var _nextRepositoryUpdate; 50 var _remoteUpdateInProgress = false; 51 var _localUpdateInProgress = false; 52 53 var self = this; 54 55 /* 56 * Retrieve the DB schema version 57 */ 58 this.getDBVersion = function (schema) { 59 if (_dbVersions[schema]){ 60 return Zotero.Promise.resolve(_dbVersions[schema]); 61 } 62 63 var sql = "SELECT version FROM version WHERE schema='" + schema + "'"; 64 return Zotero.DB.valueQueryAsync(sql) 65 .then(function (dbVersion) { 66 if (dbVersion) { 67 dbVersion = parseInt(dbVersion); 68 _dbVersions[schema] = dbVersion; 69 } 70 return dbVersion; 71 }) 72 .catch(function (e) { 73 return Zotero.DB.tableExists('version') 74 .then(function (exists) { 75 if (exists) { 76 throw e; 77 } 78 return false; 79 }); 80 }); 81 } 82 83 84 /* 85 * Checks if the DB schema exists and is up-to-date, updating if necessary 86 */ 87 this.updateSchema = Zotero.Promise.coroutine(function* (options = {}) { 88 // TODO: Check database integrity first with Zotero.DB.integrityCheck() 89 90 // 'userdata' is the last upgrade step run in _migrateUserDataSchema() based on the 91 // version in the schema file. Upgrade steps may or may not break DB compatibility. 92 // 93 // 'compatibility' is incremented manually by upgrade steps in order to break DB 94 // compatibility with older versions. 95 var versions = yield Zotero.Promise.all([ 96 this.getDBVersion('userdata'), this.getDBVersion('compatibility') 97 ]); 98 var [userdata, compatibility] = versions; 99 if (!userdata) { 100 Zotero.debug('Database does not exist -- creating\n'); 101 return _initializeSchema() 102 .then(function() { 103 (Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise) 104 .then(1000) 105 .then(async function () { 106 await this.updateBundledFiles(); 107 if (Zotero.Prefs.get('automaticScraperUpdates')) { 108 try { 109 await this.updateFromRepository(this.REPO_UPDATE_UPGRADE); 110 } 111 catch (e) { 112 Zotero.logError(e); 113 } 114 } 115 _schemaUpdateDeferred.resolve(true); 116 }.bind(this)) 117 }.bind(this)); 118 } 119 120 // We don't handle upgrades from pre-Zotero 2.1 databases 121 if (userdata < 76) { 122 let msg = Zotero.getString('upgrade.nonupgradeableDB1') 123 + "\n\n" + Zotero.getString('upgrade.nonupgradeableDB2', "4.0"); 124 throw new Error(msg); 125 } 126 127 if (compatibility > _maxCompatibility) { 128 let dbClientVersion = yield Zotero.DB.valueQueryAsync( 129 "SELECT value FROM settings " 130 + "WHERE setting='client' AND key='lastCompatibleVersion'" 131 ); 132 let msg = "Database is incompatible with this Zotero version " 133 + `(${compatibility} > ${_maxCompatibility})` 134 throw new Zotero.DB.IncompatibleVersionException(msg, dbClientVersion); 135 } 136 137 // Check if DB is coming from the DB Repair Tool and should be checked 138 var integrityCheck = yield Zotero.DB.valueQueryAsync( 139 "SELECT value FROM settings WHERE setting='db' AND key='integrityCheck'" 140 ); 141 142 var schemaVersion = yield _getSchemaSQLVersion('userdata'); 143 options.minor = minorUpdateFrom && userdata >= minorUpdateFrom; 144 145 // If non-minor userdata upgrade, make backup of database first 146 if (userdata < schemaVersion && !options.minor) { 147 yield Zotero.DB.backupDatabase(userdata, true); 148 } 149 else if (integrityCheck) { 150 yield Zotero.DB.backupDatabase(false, true); 151 } 152 153 yield Zotero.DB.queryAsync("PRAGMA foreign_keys = false"); 154 try { 155 var updated = yield Zotero.DB.executeTransaction(function* (conn) { 156 var updated = yield _updateSchema('system'); 157 158 // Update custom tables if they exist so that changes are in 159 // place before user data migration 160 if (Zotero.DB.tableExists('customItemTypes')) { 161 yield _updateCustomTables(updated); 162 } 163 164 // Auto-repair databases coming from the DB Repair Tool 165 if (integrityCheck) { 166 yield this.integrityCheck(true); 167 yield Zotero.DB.queryAsync( 168 "DELETE FROM settings WHERE setting='db' AND key='integrityCheck'" 169 ); 170 } 171 172 updated = yield _migrateUserDataSchema(userdata, options); 173 yield _updateSchema('triggers'); 174 175 // Populate combined tables for custom types and fields -- this is likely temporary 176 // 177 // We do this again in case custom fields were changed during user data migration 178 yield _updateCustomTables() 179 180 return updated; 181 }.bind(this)); 182 } 183 finally { 184 yield Zotero.DB.queryAsync("PRAGMA foreign_keys = true"); 185 } 186 187 if (updated) { 188 // Upgrade seems to have been a success -- delete any previous backups 189 var maxPrevious = userdata - 1; 190 var file = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 191 var toDelete = []; 192 try { 193 var files = file.directoryEntries; 194 while (files.hasMoreElements()) { 195 var file = files.getNext(); 196 file.QueryInterface(Components.interfaces.nsIFile); 197 if (file.isDirectory()) { 198 continue; 199 } 200 var matches = file.leafName.match(/zotero\.sqlite\.([0-9]{2,})\.bak/); 201 if (!matches) { 202 continue; 203 } 204 if (matches[1]>=28 && matches[1]<=maxPrevious) { 205 toDelete.push(file); 206 } 207 } 208 for (let file of toDelete) { 209 Zotero.debug('Removing previous backup file ' + file.leafName); 210 file.remove(false); 211 } 212 } 213 catch (e) { 214 Zotero.debug(e); 215 } 216 } 217 218 // Reset sync queue tries if new version 219 yield _checkClientVersion(); 220 221 // In Standalone, don't load bundled files until after UI is ready. In Firefox, load them as 222 // soon initialization is done so that translation works before the Zotero pane is opened. 223 (Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise) 224 .then(1000) 225 .then(async function () { 226 await this.updateBundledFiles(); 227 if (Zotero.Prefs.get('automaticScraperUpdates')) { 228 try { 229 await this.updateFromRepository(this.REPO_UPDATE_STARTUP); 230 } 231 catch (e) { 232 Zotero.logError(e); 233 } 234 } 235 _schemaUpdateDeferred.resolve(true); 236 }.bind(this)); 237 238 return updated; 239 }); 240 241 242 // https://www.zotero.org/support/nsf 243 // 244 // This is mostly temporary 245 // TEMP - NSF 246 this.importSchema = Zotero.Promise.coroutine(function* (str, uri) { 247 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 248 .getService(Components.interfaces.nsIPromptService); 249 250 if (!uri.match(/https?:\/\/([^\.]+\.)?zotero.org\//)) { 251 Zotero.debug("Ignoring schema file from non-zotero.org domain"); 252 return; 253 } 254 255 str = str.trim(); 256 257 Zotero.debug(str); 258 259 if (str == "%%%ZOTERO_NSF_TEMP_INSTALL%%%") { 260 Zotero.debug(Zotero.ItemTypes.getID("nsfReviewer")); 261 if (Zotero.ItemTypes.getID("nsfReviewer")) { 262 ps.alert(null, "Zotero Item Type Already Exists", "The 'NSF Reviewer' item type already exists in Zotero."); 263 Zotero.debug("nsfReviewer item type already exists"); 264 return; 265 } 266 267 Zotero.debug("Installing nsfReviewer item type"); 268 269 var itemTypeID = Zotero.ID.get('customItemTypes'); 270 271 yield Zotero.DB.executeTransaction(function* () { 272 yield Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID); 273 274 var fields = [ 275 ['name', 'Name'], 276 ['institution', 'Institution'], 277 ['address', 'Address'], 278 ['telephone', 'Telephone'], 279 ['email', 'Email'], 280 ['homepage', 'Webpage'], 281 ['discipline', 'Discipline'], 282 ['nsfID', 'NSF ID'], 283 ['dateSent', 'Date Sent'], 284 ['dateDue', 'Date Due'], 285 ['accepted', 'Accepted'], 286 ['programDirector', 'Program Director'] 287 ]; 288 for (var i=0; i<fields.length; i++) { 289 var fieldID = Zotero.ItemFields.getID(fields[i][0]); 290 if (!fieldID) { 291 var fieldID = Zotero.ID.get('customFields'); 292 yield Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]); 293 yield Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]); 294 } 295 else { 296 yield Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]); 297 } 298 299 switch (fields[i][0]) { 300 case 'name': 301 var baseFieldID = 110; // title 302 break; 303 304 case 'dateSent': 305 var baseFieldID = 14; // date 306 break; 307 308 case 'homepage': 309 var baseFieldID = 1; // URL 310 break; 311 312 default: 313 var baseFieldID = null; 314 } 315 316 if (baseFieldID) { 317 yield Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]); 318 } 319 } 320 321 yield _reloadSchema(); 322 }); 323 324 var s = new Zotero.Search; 325 s.name = "Overdue NSF Reviewers"; 326 s.addCondition('itemType', 'is', 'nsfReviewer'); 327 s.addCondition('dateDue', 'isBefore', 'today'); 328 s.addCondition('tag', 'isNot', 'Completed'); 329 yield s.saveTx(); 330 331 ps.alert(null, "Zotero Item Type Added", "The 'NSF Reviewer' item type and 'Overdue NSF Reviewers' saved search have been installed."); 332 } 333 else if (str == "%%%ZOTERO_NSF_TEMP_UNINSTALL%%%") { 334 var itemTypeID = Zotero.ItemTypes.getID('nsfReviewer'); 335 if (!itemTypeID) { 336 ps.alert(null, "Zotero Item Type Does Not Exist", "The 'NSF Reviewer' item type does not exist in Zotero."); 337 Zotero.debug("nsfReviewer item types doesn't exist", 2); 338 return; 339 } 340 341 var s = new Zotero.Search; 342 s.addCondition('itemType', 'is', 'nsfReviewer'); 343 var s2 = new Zotero.Search; 344 s2.addCondition('itemType', 'is', 'nsfReviewer'); 345 s2.addCondition('deleted', 'true'); 346 if ((yield s.search()).length || (yield s2.search()).length) { 347 ps.alert(null, "Error", "All 'NSF Reviewer' items must be deleted before the item type can be removed from Zotero."); 348 return; 349 } 350 351 Zotero.debug("Uninstalling nsfReviewer item type"); 352 yield Zotero.DB.executeTransaction(function* () { 353 yield Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset); 354 yield Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset); 355 var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID); 356 for (let fieldID of fields) { 357 if (Zotero.ItemFields.isCustom(fieldID)) { 358 yield Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset); 359 } 360 } 361 yield Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset); 362 363 var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID); 364 for (let search of searches) { 365 if (search.name == 'Overdue NSF Reviewers') { 366 yield search.erase(); 367 } 368 } 369 370 yield _reloadSchema(); 371 }.bind(this)); 372 373 ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled."); 374 } 375 }); 376 377 var _reloadSchema = Zotero.Promise.coroutine(function* () { 378 yield _updateCustomTables(); 379 yield Zotero.ItemTypes.init(); 380 yield Zotero.ItemFields.init(); 381 yield Zotero.SearchConditions.init(); 382 383 // Update item type menus in every open window 384 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 385 .getService(Components.interfaces.nsIWindowMediator); 386 var enumerator = wm.getEnumerator("navigator:browser"); 387 while (enumerator.hasMoreElements()) { 388 var win = enumerator.getNext(); 389 win.ZoteroPane.buildItemTypeSubMenu(); 390 win.document.getElementById('zotero-editpane-item-box').buildItemTypeMenu(); 391 } 392 }); 393 394 395 var _updateCustomTables = Zotero.Promise.coroutine(function* (skipDelete, skipSystem) { 396 Zotero.debug("Updating custom tables"); 397 398 Zotero.DB.requireTransaction(); 399 400 if (!skipDelete) { 401 yield Zotero.DB.queryAsync("DELETE FROM itemTypesCombined"); 402 yield Zotero.DB.queryAsync("DELETE FROM fieldsCombined WHERE fieldID NOT IN (SELECT fieldID FROM itemData)"); 403 yield Zotero.DB.queryAsync("DELETE FROM itemTypeFieldsCombined"); 404 yield Zotero.DB.queryAsync("DELETE FROM baseFieldMappingsCombined"); 405 } 406 407 var offset = Zotero.ItemTypes.customIDOffset; 408 yield Zotero.DB.queryAsync( 409 "INSERT INTO itemTypesCombined " 410 + ( 411 skipSystem 412 ? "" 413 : "SELECT itemTypeID, typeName, display, 0 AS custom FROM itemTypes UNION " 414 ) 415 + "SELECT customItemTypeID + " + offset + " AS itemTypeID, typeName, display, 1 AS custom FROM customItemTypes" 416 ); 417 yield Zotero.DB.queryAsync( 418 "INSERT OR IGNORE INTO fieldsCombined " 419 + ( 420 skipSystem 421 ? "" 422 : "SELECT fieldID, fieldName, NULL AS label, fieldFormatID, 0 AS custom FROM fields UNION " 423 ) 424 + "SELECT customFieldID + " + offset + " AS fieldID, fieldName, label, NULL, 1 AS custom FROM customFields" 425 ); 426 yield Zotero.DB.queryAsync( 427 "INSERT INTO itemTypeFieldsCombined " 428 + ( 429 skipSystem 430 ? "" 431 : "SELECT itemTypeID, fieldID, hide, orderIndex FROM itemTypeFields UNION " 432 ) 433 + "SELECT customItemTypeID + " + offset + " AS itemTypeID, " 434 + "COALESCE(fieldID, customFieldID + " + offset + ") AS fieldID, hide, orderIndex FROM customItemTypeFields" 435 ); 436 yield Zotero.DB.queryAsync( 437 "INSERT INTO baseFieldMappingsCombined " 438 + ( 439 skipSystem 440 ? "" 441 : "SELECT itemTypeID, baseFieldID, fieldID FROM baseFieldMappings UNION " 442 ) 443 + "SELECT customItemTypeID + " + offset + " AS itemTypeID, baseFieldID, " 444 + "customFieldID + " + offset + " AS fieldID FROM customBaseFieldMappings" 445 ); 446 }); 447 448 449 /** 450 * Update styles and translators in data directory with versions from 451 * ZIP file (XPI) or directory (source) in extension directory 452 * 453 * @param {String} [mode] - 'translators' or 'styles' 454 * @return {Promise} 455 */ 456 this.updateBundledFiles = Zotero.Promise.coroutine(function* (mode) { 457 if (Zotero.skipBundledFiles) { 458 Zotero.debug("Skipping bundled file installation"); 459 return; 460 } 461 462 if (_localUpdateInProgress) { 463 Zotero.debug("Bundled file update already in progress", 2); 464 return; 465 } 466 467 _localUpdateInProgress = true; 468 469 try { 470 yield Zotero.proxyAuthComplete.delay(1000); 471 472 Zotero.debug("Updating bundled " + (mode || "files")); 473 474 // Get path to add-on 475 476 // Synchronous in Standalone 477 if (Zotero.isStandalone) { 478 var installLocation = Components.classes["@mozilla.org/file/directory_service;1"] 479 .getService(Components.interfaces.nsIProperties) 480 .get("AChrom", Components.interfaces.nsIFile).parent; 481 installLocation.append("zotero.jar"); 482 } 483 // Asynchronous in Firefox 484 else { 485 let resolve, reject; 486 let promise = new Zotero.Promise(function () { 487 resolve = arguments[0]; 488 reject = arguments[1]; 489 }); 490 Components.utils.import("resource://gre/modules/AddonManager.jsm"); 491 AddonManager.getAddonByID( 492 ZOTERO_CONFIG.GUID, 493 function (addon) { 494 try { 495 installLocation = addon.getResourceURI() 496 .QueryInterface(Components.interfaces.nsIFileURL).file; 497 } 498 catch (e) { 499 reject(e); 500 return; 501 } 502 resolve(); 503 } 504 ); 505 yield promise; 506 } 507 installLocation = installLocation.path; 508 509 let initOpts = { fromSchemaUpdate: true }; 510 511 // Update files 512 switch (mode) { 513 case 'styles': 514 yield Zotero.Styles.init(initOpts); 515 var updated = yield _updateBundledFilesAtLocation(installLocation, mode); 516 break; 517 518 case 'translators': 519 yield Zotero.Translators.init(initOpts); 520 var updated = yield _updateBundledFilesAtLocation(installLocation, mode); 521 break; 522 523 default: 524 yield Zotero.Translators.init(initOpts); 525 let up1 = yield _updateBundledFilesAtLocation(installLocation, 'translators', true); 526 yield Zotero.Styles.init(initOpts); 527 let up2 = yield _updateBundledFilesAtLocation(installLocation, 'styles'); 528 var updated = up1 || up2; 529 } 530 } 531 finally { 532 _localUpdateInProgress = false; 533 } 534 535 return updated; 536 }); 537 538 /** 539 * Update bundled files in a given location 540 * 541 * @param {String} installLocation - Path to XPI or source dir 542 * @param {'translators','styles'} mode 543 * @param {Boolean} [skipVersionUpdates=false] 544 */ 545 var _updateBundledFilesAtLocation = Zotero.Promise.coroutine(function* (installLocation, mode, skipVersionUpdates) { 546 Components.utils.import("resource://gre/modules/FileUtils.jsm"); 547 548 var isUnpacked = (yield OS.File.stat(installLocation)).isDir; 549 if(!isUnpacked) { 550 var xpiZipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"] 551 .createInstance(Components.interfaces.nsIZipReader); 552 xpiZipReader.open(new FileUtils.File(installLocation)); 553 } 554 555 switch (mode) { 556 case "translators": 557 var titleField = 'label'; 558 var fileExt = ".js"; 559 var destDir = Zotero.getTranslatorsDirectory().path; 560 break; 561 562 case "styles": 563 var titleField = 'title'; 564 var fileExt = ".csl"; 565 var destDir = Zotero.getStylesDirectory().path; 566 var hiddenDir = OS.Path.join(destDir, 'hidden'); 567 break; 568 569 default: 570 throw new Error("Invalid mode '" + mode + "'"); 571 } 572 573 var modeType = mode.substr(0, mode.length - 1); 574 var ModeType = Zotero.Utilities.capitalize(modeType); 575 var Mode = Zotero.Utilities.capitalize(mode); 576 577 var repotime = yield Zotero.File.getContentsFromURLAsync("resource://zotero/schema/repotime.txt"); 578 var date = Zotero.Date.sqlToDate(repotime.trim(), true); 579 repotime = Zotero.Date.toUnixTimestamp(date); 580 581 var fileNameRE = new RegExp("^[^\.].+\\" + fileExt + "$"); 582 583 // If directory is empty, force reinstall 584 var forceReinstall = true; 585 let iterator = new OS.File.DirectoryIterator(destDir); 586 try { 587 outer: 588 while (true) { 589 let entries = yield iterator.nextBatch(10); 590 if (!entries.length) break; 591 for (let i = 0; i < entries.length; i++) { 592 let entry = entries[i]; 593 if (!entry.name.match(fileNameRE) || entry.isDir) { 594 continue; 595 } 596 // Not empty 597 forceReinstall = false; 598 break outer; 599 } 600 } 601 } 602 finally { 603 iterator.close(); 604 } 605 606 // 607 // Delete obsolete files 608 // 609 var sql = "SELECT version FROM version WHERE schema='delete'"; 610 var lastVersion = yield Zotero.DB.valueQueryAsync(sql); 611 612 if(isUnpacked) { 613 var deleted = OS.Path.join(installLocation, 'deleted.txt'); 614 // In source builds, deleted.txt is in the translators directory 615 if (!(yield OS.File.exists(deleted))) { 616 deleted = OS.Path.join(installLocation, 'translators', 'deleted.txt'); 617 if (!(yield OS.File.exists(deleted))) { 618 deleted = false; 619 } 620 } 621 } else { 622 var deleted = xpiZipReader.getInputStream("deleted.txt"); 623 } 624 625 let deletedVersion; 626 if (deleted) { 627 deleted = yield Zotero.File.getContentsAsync(deleted); 628 deleted = deleted.match(/^([^\s]+)/gm); 629 deletedVersion = deleted.shift(); 630 } 631 632 if (!lastVersion || lastVersion < deletedVersion) { 633 var toDelete = []; 634 let iterator = new OS.File.DirectoryIterator(destDir); 635 try { 636 while (true) { 637 let entries = yield iterator.nextBatch(10); 638 if (!entries.length) break; 639 for (let i = 0; i < entries.length; i++) { 640 let entry = entries[i]; 641 642 if ((entry.isSymLink && !(yield OS.File.exists(entry.path))) // symlink to non-existent file 643 || entry.isDir) { 644 continue; 645 } 646 647 if (mode == 'styles') { 648 switch (entry.name) { 649 // Remove update script (included with 3.0 accidentally) 650 case 'update': 651 652 // Delete renamed/obsolete files 653 case 'chicago-note.csl': 654 case 'mhra_note_without_bibliography.csl': 655 case 'mhra.csl': 656 case 'mla.csl': 657 toDelete.push(entry.path); 658 continue; 659 660 // Be a little more careful with this one, in case someone 661 // created a custom 'aaa' style 662 case 'aaa.csl': 663 let str = yield Zotero.File.getContentsAsync(entry.path, false, 300); 664 if (str.indexOf("<title>American Anthropological Association</title>") != -1) { 665 toDelete.push(entry.path); 666 } 667 continue; 668 } 669 } 670 671 if (forceReinstall || !entry.name.match(fileNameRE)) { 672 continue; 673 } 674 675 if (mode == 'translators') { 676 // TODO: Change if the APIs change 677 let newObj = new Zotero[Mode].loadFromFile(entry.path); 678 if (deleted.indexOf(newObj[modeType + "ID"]) == -1) { 679 continue; 680 } 681 toDelete.push(entry.path); 682 } 683 } 684 } 685 } 686 finally { 687 iterator.close(); 688 } 689 690 for (let i = 0; i < toDelete.length; i++) { 691 let path = toDelete[i]; 692 Zotero.debug("Deleting " + path); 693 try { 694 yield OS.File.remove(path); 695 } 696 catch (e) { 697 Components.utils.reportError(e); 698 Zotero.debug(e, 1); 699 } 700 } 701 702 if (!skipVersionUpdates) { 703 let sql = "REPLACE INTO version (schema, version) VALUES ('delete', ?)"; 704 yield Zotero.DB.queryAsync(sql, deletedVersion); 705 } 706 } 707 708 // 709 // Update files 710 // 711 var sql = "SELECT version FROM version WHERE schema=?"; 712 var lastModTime = yield Zotero.DB.valueQueryAsync(sql, mode); 713 // Fix millisecond times (possible in 4.0?) 714 if (lastModTime > 9999999999) { 715 lastModTime = Math.round(lastModTime / 1000); 716 } 717 var cache = {}; 718 719 // XPI installation 720 if (!isUnpacked) { 721 var modTime = Math.round( 722 (yield OS.File.stat(installLocation)).lastModificationDate.getTime() / 1000 723 ); 724 725 if (!forceReinstall && lastModTime && modTime <= lastModTime) { 726 Zotero.debug("Installed " + mode + " are up-to-date with XPI"); 727 return false; 728 } 729 730 Zotero.debug("Updating installed " + mode + " from XPI"); 731 732 let tmpDir = Zotero.getTempDirectory().path; 733 734 if (mode == 'translators') { 735 // Parse translators.json 736 if (!xpiZipReader.hasEntry("translators.json")) { 737 Zotero.logError("translators.json not found"); 738 return false; 739 } 740 let index = JSON.parse(yield Zotero.File.getContentsAsync( 741 xpiZipReader.getInputStream("translators.json")) 742 ); 743 for (let id in index) { 744 index[id].extract = true; 745 } 746 747 let sql = "SELECT rowid, fileName, metadataJSON FROM translatorCache"; 748 let rows = yield Zotero.DB.queryAsync(sql); 749 // If there's anything in the cache, see what we actually need to extract 750 for (let i = 0; i < rows.length; i++) { 751 let json = rows[i].metadataJSON; 752 let metadata; 753 try { 754 metadata = JSON.parse(json); 755 } 756 catch (e) { 757 Zotero.logError(e); 758 Zotero.debug(json, 1); 759 760 // If JSON is invalid, clear from cache 761 yield Zotero.DB.queryAsync( 762 "DELETE FROM translatorCache WHERE rowid=?", 763 rows[i].rowid 764 ); 765 continue; 766 } 767 let id = metadata.translatorID; 768 if (index[id] && index[id].lastUpdated <= metadata.lastUpdated) { 769 index[id].extract = false; 770 } 771 } 772 773 for (let translatorID in index) { 774 // Use index file and DB cache for translator entries, 775 // extracting only what's necessary 776 let entry = index[translatorID]; 777 if (!entry.extract) { 778 //Zotero.debug("Not extracting '" + entry.label + "' -- same version already in cache"); 779 continue; 780 } 781 782 let tmpFile = OS.Path.join(tmpDir, entry.fileName) 783 yield Zotero.File.removeIfExists(tmpFile); 784 xpiZipReader.extract("translators/" + entry.fileName, new FileUtils.File(tmpFile)); 785 786 var existingObj = Zotero.Translators.get(translatorID); 787 if (!existingObj) { 788 Zotero.debug("Installing translator '" + entry.label + "'"); 789 } 790 else { 791 Zotero.debug("Updating translator '" + existingObj.label + "'"); 792 yield Zotero.File.removeIfExists(existingObj.path); 793 } 794 795 let destFile = OS.Path.join(destDir, entry.fileName); 796 try { 797 yield OS.File.move(tmpFile, destFile, { 798 noOverwrite: true 799 }); 800 } 801 catch (e) { 802 if (e instanceof OS.File.Error && e.becauseExists) { 803 // Could overwrite automatically, but we want to log this 804 Zotero.warn("Overwriting translator with same filename '" 805 + entry.fileName + "'"); 806 yield OS.File.move(tmpFile, destFile); 807 } 808 else { 809 throw e; 810 } 811 } 812 } 813 } 814 // Styles 815 else { 816 let entries = xpiZipReader.findEntries('styles/*.csl'); 817 while (entries.hasMore()) { 818 let entry = entries.getNext(); 819 let fileName = entry.substr(7); // strip 'styles/' 820 821 let tmpFile = OS.Path.join(tmpDir, fileName); 822 yield Zotero.File.removeIfExists(tmpFile); 823 xpiZipReader.extract(entry, new FileUtils.File(tmpFile)); 824 let code = yield Zotero.File.getContentsAsync(tmpFile); 825 let newObj = new Zotero.Style(code); 826 827 let existingObj = Zotero.Styles.get(newObj[modeType + "ID"]); 828 if (!existingObj) { 829 Zotero.debug("Installing style '" + newObj[titleField] + "'"); 830 } 831 else { 832 Zotero.debug("Updating " 833 + (existingObj.hidden ? "hidden " : "") 834 + "style '" + existingObj[titleField] + "'"); 835 yield Zotero.File.removeIfExists(existingObj.path); 836 } 837 838 if (!existingObj || !existingObj.hidden) { 839 yield OS.File.move(tmpFile, OS.Path.join(destDir, fileName)); 840 } 841 else { 842 yield OS.File.move(tmpFile, OS.Path.join(hiddenDir, fileName)); 843 } 844 } 845 } 846 847 if(xpiZipReader) xpiZipReader.close(); 848 } 849 // Source installation 850 else { 851 let sourceDir = OS.Path.join(installLocation, mode); 852 853 var modTime = 0; 854 let sourceFilesExist = false; 855 let iterator; 856 try { 857 iterator = new OS.File.DirectoryIterator(sourceDir); 858 } 859 catch (e) { 860 if (e instanceof OS.File.Error && e.becauseNoSuchFile) { 861 let msg = "No " + mode + " directory"; 862 Zotero.debug(msg, 1); 863 Components.utils.reportError(msg); 864 return false; 865 } 866 throw e; 867 } 868 try { 869 while (true) { 870 let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary 871 if (!entries.length) break; 872 for (let i = 0; i < entries.length; i++) { 873 let entry = entries[i]; 874 if (!entry.name.match(fileNameRE) || entry.isDir) { 875 continue; 876 } 877 sourceFilesExist = true; 878 let d; 879 if ('winLastWriteDate' in entry) { 880 d = entry.winLastWriteDate; 881 } 882 else { 883 d = (yield OS.File.stat(entry.path)).lastModificationDate; 884 } 885 let fileModTime = Math.round(d.getTime() / 1000); 886 if (fileModTime > modTime) { 887 modTime = fileModTime; 888 } 889 } 890 } 891 } 892 finally { 893 iterator.close(); 894 } 895 896 // Don't attempt installation for source build with missing styles 897 if (!sourceFilesExist) { 898 Zotero.debug("No source " + modeType + " files exist -- skipping update"); 899 return false; 900 } 901 902 if (!forceReinstall && lastModTime && modTime <= lastModTime) { 903 Zotero.debug("Installed " + mode + " are up-to-date with " + mode + " directory"); 904 return false; 905 } 906 907 Zotero.debug("Updating installed " + mode + " from " + mode + " directory"); 908 909 iterator = new OS.File.DirectoryIterator(sourceDir); 910 try { 911 while (true) { 912 let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary 913 if (!entries.length) break; 914 915 for (let i = 0; i < entries.length; i++) { 916 let entry = entries[i]; 917 if (!entry.name.match(fileNameRE) || entry.isDir) { 918 continue; 919 } 920 let newObj; 921 if (mode == 'styles') { 922 let code = yield Zotero.File.getContentsAsync(entry.path); 923 newObj = new Zotero.Style(code); 924 } 925 else if (mode == 'translators') { 926 newObj = yield Zotero.Translators.loadFromFile(entry.path); 927 } 928 else { 929 throw new Error("Invalid mode '" + mode + "'"); 930 } 931 let existingObj = Zotero[Mode].get(newObj[modeType + "ID"]); 932 if (!existingObj) { 933 Zotero.debug("Installing " + modeType + " '" + newObj[titleField] + "'"); 934 } 935 else { 936 Zotero.debug("Updating " 937 + (existingObj.hidden ? "hidden " : "") 938 + modeType + " '" + existingObj[titleField] + "'"); 939 yield Zotero.File.removeIfExists(existingObj.path); 940 } 941 942 let fileName; 943 if (mode == 'translators') { 944 fileName = Zotero.Translators.getFileNameFromLabel( 945 newObj[titleField], newObj.translatorID 946 ); 947 } 948 else if (mode == 'styles') { 949 fileName = entry.name; 950 } 951 952 try { 953 let destFile; 954 if (!existingObj || !existingObj.hidden) { 955 destFile = OS.Path.join(destDir, fileName); 956 } 957 else { 958 destFile = OS.Path.join(hiddenDir, fileName) 959 } 960 961 try { 962 yield OS.File.copy(entry.path, destFile, { noOverwrite: true }); 963 } 964 catch (e) { 965 if (e instanceof OS.File.Error && e.becauseExists) { 966 // Could overwrite automatically, but we want to log this 967 Zotero.warn("Overwriting " + modeType + " with same filename " 968 + "'" + fileName + "'", 1); 969 yield OS.File.copy(entry.path, destFile); 970 } 971 else { 972 throw e; 973 } 974 } 975 976 if (mode == 'translators') { 977 cache[fileName] = newObj.metadata; 978 } 979 } 980 catch (e) { 981 Components.utils.reportError("Error copying file " + fileName + ": " + e); 982 } 983 } 984 } 985 } 986 finally { 987 iterator.close(); 988 } 989 } 990 991 yield Zotero.DB.executeTransaction(function* () { 992 var sql = "REPLACE INTO version VALUES (?, ?)"; 993 yield Zotero.DB.queryAsync(sql, [mode, modTime]); 994 995 if (!skipVersionUpdates) { 996 sql = "REPLACE INTO version VALUES ('repository', ?)"; 997 yield Zotero.DB.queryAsync(sql, repotime); 998 } 999 }); 1000 1001 yield Zotero[Mode].reinit({ 1002 metadataCache: cache, 1003 fromSchemaUpdate: true 1004 }); 1005 1006 return true; 1007 }); 1008 1009 1010 this.onUpdateNotification = async function (delay) { 1011 if (!Zotero.Prefs.get('automaticScraperUpdates')) { 1012 return; 1013 } 1014 1015 // If another repository check -- either from notification or daily check -- is scheduled 1016 // before delay, just wait for that one 1017 if (_nextRepositoryUpdate) { 1018 if (_nextRepositoryUpdate <= (Date.now() + delay)) { 1019 Zotero.debug("Next scheduled update from repository is in " 1020 + Math.round((_nextRepositoryUpdate - Date.now()) / 1000) + " seconds " 1021 + "-- ignoring notification"); 1022 return; 1023 } 1024 if (_repositoryNotificationTimerID) { 1025 clearTimeout(_repositoryNotificationTimerID); 1026 } 1027 } 1028 1029 _nextRepositoryUpdate = Date.now() + delay; 1030 Zotero.debug(`Updating from repository in ${Math.round(delay / 1000)} seconds`); 1031 _repositoryNotificationTimerID = setTimeout(() => { 1032 this.updateFromRepository(this.REPO_UPDATE_NOTIFICATION) 1033 }, delay); 1034 }; 1035 1036 1037 /** 1038 * Send XMLHTTP request for updated translators and styles to the central repository 1039 * 1040 * @param {Integer} [force=0] - If non-zero, force a repository query regardless of how long it's 1041 * been since the last check. Should be a REPO_UPDATE_* constant. 1042 */ 1043 this.updateFromRepository = Zotero.Promise.coroutine(function* (force = 0) { 1044 if (Zotero.skipBundledFiles) { 1045 Zotero.debug("No bundled files -- skipping repository update"); 1046 return; 1047 } 1048 1049 if (_remoteUpdateInProgress) { 1050 Zotero.debug("A remote update is already in progress -- not checking repository"); 1051 return false; 1052 } 1053 1054 if (!force) { 1055 // Check user preference for automatic updates 1056 if (!Zotero.Prefs.get('automaticScraperUpdates')) { 1057 Zotero.debug('Automatic repository updating disabled -- not checking repository', 4); 1058 return false; 1059 } 1060 1061 // Determine the earliest local time that we'd query the repository again 1062 let lastCheck = yield this.getDBVersion('lastcheck'); 1063 let nextCheck = new Date(); 1064 nextCheck.setTime((lastCheck + ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL) * 1000); 1065 1066 // If enough time hasn't passed, don't update 1067 var now = new Date(); 1068 if (now < nextCheck) { 1069 Zotero.debug('Not enough time since last update -- not checking repository', 4); 1070 // Set the repository timer to the remaining time 1071 _setRepositoryTimer(Math.round((nextCheck.getTime() - now.getTime()) / 1000)); 1072 return false; 1073 } 1074 } 1075 1076 if (_localUpdateInProgress) { 1077 Zotero.debug('A local update is already in progress -- delaying repository check', 4); 1078 _setRepositoryTimer(600); 1079 return false; 1080 } 1081 1082 if (Zotero.locked) { 1083 Zotero.debug('Zotero is locked -- delaying repository check', 4); 1084 _setRepositoryTimer(600); 1085 return false; 1086 } 1087 1088 // If an update from a notification is queued, stop it, since we're updating now 1089 if (_repositoryNotificationTimerID) { 1090 clearTimeout(_repositoryNotificationTimerID); 1091 _repositoryNotificationTimerID = null; 1092 _nextRepositoryUpdate = null; 1093 } 1094 1095 if (Zotero.DB.inTransaction()) { 1096 yield Zotero.DB.waitForTransaction(); 1097 } 1098 1099 // Get the last timestamp we got from the server 1100 var lastUpdated = yield this.getDBVersion('repository'); 1101 var updated = false; 1102 1103 try { 1104 var url = ZOTERO_CONFIG.REPOSITORY_URL + 'updated?' 1105 + (lastUpdated ? 'last=' + lastUpdated + '&' : '') 1106 + 'version=' + Zotero.version; 1107 1108 Zotero.debug('Checking repository for updates'); 1109 1110 _remoteUpdateInProgress = true; 1111 1112 if (force) { 1113 url += '&m=' + force; 1114 } 1115 1116 // Send list of installed styles 1117 var styles = Zotero.Styles.getAll(); 1118 var styleTimestamps = []; 1119 for (let id in styles) { 1120 let styleUpdated = Zotero.Date.sqlToDate(styles[id].updated); 1121 styleUpdated = styleUpdated ? styleUpdated.getTime() / 1000 : 0; 1122 var selfLink = styles[id].url; 1123 var data = { 1124 id: id, 1125 updated: styleUpdated 1126 }; 1127 if (selfLink) { 1128 data.url = selfLink; 1129 } 1130 styleTimestamps.push(data); 1131 } 1132 var body = 'styles=' + encodeURIComponent(JSON.stringify(styleTimestamps)); 1133 1134 try { 1135 var xmlhttp = yield Zotero.HTTP.request("POST", url, { body: body }); 1136 updated = yield _handleRepositoryResponse(xmlhttp, force); 1137 } 1138 catch (e) { 1139 if (!force) { 1140 if (e instanceof Zotero.HTTP.UnexpectedStatusException 1141 || e instanceof Zotero.HTTP.BrowserOfflineException) { 1142 let msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL 1143 if (e instanceof Zotero.HTTP.BrowserOfflineException) { 1144 Zotero.debug("Browser is offline" + msg, 2); 1145 } 1146 else { 1147 Zotero.logError(e); 1148 Zotero.debug(e.status, 1); 1149 Zotero.debug(e.xmlhttp.responseText, 1); 1150 Zotero.debug("Error updating from repository " + msg, 1); 1151 } 1152 // TODO: instead, add an observer to start and stop timer on online state change 1153 _setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL); 1154 return; 1155 } 1156 } 1157 if (xmlhttp) { 1158 Zotero.debug(xmlhttp.status, 1); 1159 Zotero.debug(xmlhttp.responseText, 1); 1160 } 1161 throw e; 1162 }; 1163 } 1164 finally { 1165 if (!force) { 1166 _setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL); 1167 } 1168 _remoteUpdateInProgress = false; 1169 } 1170 1171 return updated; 1172 }); 1173 1174 1175 this.stopRepositoryTimer = function () { 1176 if (_repositoryTimerID) { 1177 Zotero.debug('Stopping repository check timer'); 1178 clearTimeout(_repositoryTimerID); 1179 _repositoryTimerID = null; 1180 } 1181 if (_repositoryNotificationTimerID) { 1182 Zotero.debug('Stopping repository notification update timer'); 1183 clearTimeout(_repositoryNotificationTimerID); 1184 _repositoryNotificationTimerID = null 1185 } 1186 _nextRepositoryUpdate = null; 1187 } 1188 1189 1190 this.resetTranslatorsAndStyles = Zotero.Promise.coroutine(function* () { 1191 Zotero.debug("Resetting translators and styles"); 1192 1193 var sql = "DELETE FROM version WHERE schema IN " 1194 + "('translators', 'styles', 'repository', 'lastcheck')"; 1195 yield Zotero.DB.queryAsync(sql); 1196 _dbVersions.repository = null; 1197 _dbVersions.lastcheck = null; 1198 1199 var translatorsDir = Zotero.getTranslatorsDirectory(); 1200 var stylesDir = Zotero.getStylesDirectory(); 1201 1202 translatorsDir.remove(true); 1203 stylesDir.remove(true); 1204 1205 // Recreate directories 1206 Zotero.getTranslatorsDirectory(); 1207 Zotero.getStylesDirectory(); 1208 1209 yield Zotero.Promise.all(Zotero.Translators.reinit(), Zotero.Styles.reinit()); 1210 var updated = yield this.updateBundledFiles(); 1211 if (updated && Zotero.Prefs.get('automaticScraperUpdates')) { 1212 yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL); 1213 } 1214 return updated; 1215 }); 1216 1217 1218 this.resetTranslators = Zotero.Promise.coroutine(function* () { 1219 Zotero.debug("Resetting translators"); 1220 1221 var sql = "DELETE FROM version WHERE schema IN " 1222 + "('translators', 'repository', 'lastcheck')"; 1223 yield Zotero.DB.queryAsync(sql); 1224 _dbVersions.repository = null; 1225 _dbVersions.lastcheck = null; 1226 1227 var translatorsDir = Zotero.getTranslatorsDirectory(); 1228 translatorsDir.remove(true); 1229 Zotero.getTranslatorsDirectory(); // recreate directory 1230 yield Zotero.Translators.reinit(); 1231 var updated = yield this.updateBundledFiles('translators'); 1232 if (updated && Zotero.Prefs.get('automaticScraperUpdates')) { 1233 yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL); 1234 } 1235 return updated; 1236 }); 1237 1238 1239 this.resetStyles = Zotero.Promise.coroutine(function* () { 1240 Zotero.debug("Resetting styles"); 1241 1242 var sql = "DELETE FROM version WHERE schema IN " 1243 + "('styles', 'repository', 'lastcheck')"; 1244 yield Zotero.DB.queryAsync(sql); 1245 _dbVersions.repository = null; 1246 _dbVersions.lastcheck = null; 1247 1248 var stylesDir = Zotero.getStylesDirectory(); 1249 stylesDir.remove(true); 1250 Zotero.getStylesDirectory(); // recreate directory 1251 yield Zotero.Styles.reinit() 1252 var updated = yield this.updateBundledFiles('styles'); 1253 if (updated && Zotero.Prefs.get('automaticScraperUpdates')) { 1254 yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL); 1255 } 1256 return updated; 1257 }); 1258 1259 1260 this.integrityCheck = Zotero.Promise.coroutine(function* (fix) { 1261 Zotero.debug("Checking database integrity"); 1262 1263 // Just as a sanity check, make sure combined field tables are populated, 1264 // so that we don't try to wipe out all data 1265 if (!(yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM fieldsCombined")) 1266 || !(yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM itemTypeFieldsCombined"))) { 1267 return false; 1268 } 1269 1270 // Check foreign keys 1271 var rows = yield Zotero.DB.queryAsync("PRAGMA foreign_key_check"); 1272 if (rows.length && !fix) { 1273 let suffix1 = rows.length == 1 ? '' : 's'; 1274 let suffix2 = rows.length == 1 ? 's' : ''; 1275 Zotero.debug(`Found ${rows.length} row${suffix1} that violate${suffix2} foreign key constraints`, 1); 1276 return false; 1277 } 1278 // If fixing, delete rows that violate FK constraints 1279 for (let row of rows) { 1280 try { 1281 yield Zotero.DB.queryAsync(`DELETE FROM ${row.table} WHERE ROWID=?`, row.rowid); 1282 } 1283 catch (e) { 1284 Zotero.logError(e); 1285 } 1286 } 1287 1288 1289 // Non-foreign key checks 1290 // 1291 // The first position is for testing and the second is for repairing. Can be either SQL 1292 // statements or promise-returning functions. For statements, the repair entry can be either a 1293 // string or an array with multiple statements. Functions should avoid assuming any global state 1294 // (e.g., loaded data). 1295 var checks = [ 1296 // Can't be a FK with itemTypesCombined 1297 [ 1298 "SELECT COUNT(*) > 0 FROM items WHERE itemTypeID IS NULL", 1299 "DELETE FROM items WHERE itemTypeID IS NULL", 1300 ], 1301 // Attachments row with itemTypeID != 14 1302 [ 1303 "SELECT COUNT(*) > 0 FROM itemAttachments JOIN items USING (itemID) WHERE itemTypeID != 14", 1304 "UPDATE items SET itemTypeID=14, clientDateModified=CURRENT_TIMESTAMP WHERE itemTypeID != 14 AND itemID IN (SELECT itemID FROM itemAttachments)", 1305 ], 1306 // Fields not in type 1307 [ 1308 "SELECT COUNT(*) > 0 FROM itemData WHERE fieldID NOT IN (SELECT fieldID FROM itemTypeFieldsCombined WHERE itemTypeID=(SELECT itemTypeID FROM items WHERE itemID=itemData.itemID))", 1309 "DELETE FROM itemData WHERE fieldID NOT IN (SELECT fieldID FROM itemTypeFieldsCombined WHERE itemTypeID=(SELECT itemTypeID FROM items WHERE itemID=itemData.itemID))", 1310 ], 1311 // Missing itemAttachments row 1312 [ 1313 "SELECT COUNT(*) > 0 FROM items WHERE itemTypeID=14 AND itemID NOT IN (SELECT itemID FROM itemAttachments)", 1314 "INSERT INTO itemAttachments (itemID, linkMode) SELECT itemID, 0 FROM items WHERE itemTypeID=14 AND itemID NOT IN (SELECT itemID FROM itemAttachments)", 1315 ], 1316 // Note/child parents 1317 [ 1318 "SELECT COUNT(*) > 0 FROM itemAttachments WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))", 1319 "UPDATE itemAttachments SET parentItemID=NULL WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))", 1320 ], 1321 [ 1322 "SELECT COUNT(*) > 0 FROM itemNotes WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))", 1323 "UPDATE itemNotes SET parentItemID=NULL WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))", 1324 ], 1325 1326 // Delete empty creators 1327 // This may cause itemCreator gaps, but that's better than empty creators 1328 [ 1329 "SELECT COUNT(*) > 0 FROM creators WHERE firstName='' AND lastName=''", 1330 "DELETE FROM creators WHERE firstName='' AND lastName=''" 1331 ], 1332 1333 // Non-attachment items in the full-text index 1334 [ 1335 "SELECT COUNT(*) > 0 FROM fulltextItemWords WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)", 1336 "DELETE FROM fulltextItemWords WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)" 1337 ], 1338 // Full-text items must be attachments 1339 [ 1340 "SELECT COUNT(*) > 0 FROM fulltextItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)", 1341 "DELETE FROM fulltextItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)" 1342 ], 1343 // Invalid link mode -- set to imported url 1344 [ 1345 "SELECT COUNT(*) > 0 FROM itemAttachments WHERE linkMode NOT IN (0,1,2,3)", 1346 "UPDATE itemAttachments SET linkMode=1 WHERE linkMode NOT IN (0,1,2,3)" 1347 ], 1348 // Creators with first name can't be fieldMode 1 1349 [ 1350 "SELECT COUNT(*) > 0 FROM creators WHERE fieldMode = 1 AND firstName != ''", 1351 function () { 1352 return Zotero.DB.executeTransaction(function* () { 1353 var rows = yield Zotero.DB.queryAsync("SELECT * FROM creators WHERE fieldMode = 1 AND firstName != ''"); 1354 for (let row of rows) { 1355 // Find existing fieldMode 0 row and use that if available 1356 let newID = yield Zotero.DB.valueQueryAsync("SELECT creatorID FROM creators WHERE firstName=? AND lastName=? AND fieldMode=0", [row.firstName, row.lastName]); 1357 if (newID) { 1358 yield Zotero.DB.queryAsync("UPDATE itemCreators SET creatorID=? WHERE creatorID=?", [newID, row.creatorID]); 1359 yield Zotero.DB.queryAsync("DELETE FROM creators WHERE creatorID=?", row.creatorID); 1360 } 1361 // Otherwise convert this one to fieldMode 0 1362 else { 1363 yield Zotero.DB.queryAsync("UPDATE creators SET fieldMode=0 WHERE creatorID=?", row.creatorID); 1364 } 1365 } 1366 }); 1367 } 1368 ] 1369 ]; 1370 1371 for (let check of checks) { 1372 let errorsFound = false; 1373 // SQL statement 1374 if (typeof check[0] == 'string') { 1375 errorsFound = yield Zotero.DB.valueQueryAsync(check[0]); 1376 } 1377 // Function 1378 else { 1379 errorsFound = yield check[0](); 1380 } 1381 if (!errorsFound) { 1382 continue; 1383 } 1384 1385 Zotero.debug("Test failed!", 1); 1386 1387 if (fix) { 1388 try { 1389 // Single query 1390 if (typeof check[1] == 'string') { 1391 yield Zotero.DB.queryAsync(check[1]); 1392 } 1393 // Multiple queries 1394 else if (Array.isArray(check[1])) { 1395 for (let s of check[1]) { 1396 yield Zotero.DB.queryAsync(s); 1397 } 1398 } 1399 // Function 1400 else { 1401 yield check[1](); 1402 } 1403 continue; 1404 } 1405 catch (e) { 1406 Zotero.logError(e); 1407 } 1408 } 1409 1410 return false; 1411 } 1412 1413 return true; 1414 }); 1415 1416 1417 ///////////////////////////////////////////////////////////////// 1418 // 1419 // Private methods 1420 // 1421 ///////////////////////////////////////////////////////////////// 1422 1423 /** 1424 * Retrieve the version from the top line of the schema SQL file 1425 * 1426 * @return {Promise:String} A promise for the SQL file's version number 1427 */ 1428 function _getSchemaSQLVersion(schema){ 1429 return _getSchemaSQL(schema) 1430 .then(function (sql) { 1431 // Fetch the schema version from the first line of the file 1432 var schemaVersion = parseInt(sql.match(/^-- ([0-9]+)/)[1]); 1433 _schemaVersions[schema] = schemaVersion; 1434 return schemaVersion; 1435 }); 1436 } 1437 1438 1439 /** 1440 * Load in SQL schema 1441 * 1442 * @return {Promise:String} A promise for the contents of a schema SQL file 1443 */ 1444 function _getSchemaSQL(schema){ 1445 if (!schema){ 1446 throw ('Schema type not provided to _getSchemaSQL()'); 1447 } 1448 1449 return Zotero.File.getContentsFromURLAsync("resource://zotero/schema/" + schema + ".sql"); 1450 } 1451 1452 1453 /* 1454 * Determine the SQL statements necessary to drop the tables and indexed 1455 * in a given schema file 1456 * 1457 * NOTE: This is not currently used. 1458 * 1459 * Returns the SQL statements as a string for feeding into query() 1460 */ 1461 function _getDropCommands(schema){ 1462 var sql = _getSchemaSQL(schema); // FIXME: now a promise 1463 1464 const re = /(?:[\r\n]|^)CREATE (TABLE|INDEX) IF NOT EXISTS ([^\s]+)/; 1465 var m, str=""; 1466 while(matches = re.exec(sql)) { 1467 str += "DROP " + matches[1] + " IF EXISTS " + matches[2] + ";\n"; 1468 } 1469 1470 return str; 1471 } 1472 1473 1474 /* 1475 * Create new DB schema 1476 */ 1477 function _initializeSchema(){ 1478 return Zotero.DB.executeTransaction(function* (conn) { 1479 var userLibraryID = 1; 1480 1481 // Enable auto-vacuuming 1482 yield Zotero.DB.queryAsync("PRAGMA page_size = 4096"); 1483 yield Zotero.DB.queryAsync("PRAGMA encoding = 'UTF-8'"); 1484 yield Zotero.DB.queryAsync("PRAGMA auto_vacuum = 1"); 1485 1486 yield _getSchemaSQL('system').then(function (sql) { 1487 return Zotero.DB.executeSQLFile(sql); 1488 }); 1489 yield _getSchemaSQL('userdata').then(function (sql) { 1490 return Zotero.DB.executeSQLFile(sql); 1491 }); 1492 yield _getSchemaSQL('triggers').then(function (sql) { 1493 return Zotero.DB.executeSQLFile(sql); 1494 }); 1495 yield _updateCustomTables(true); 1496 1497 yield _getSchemaSQLVersion('system').then(function (version) { 1498 return _updateDBVersion('system', version); 1499 }); 1500 yield _getSchemaSQLVersion('userdata').then(function (version) { 1501 return _updateDBVersion('userdata', version); 1502 }); 1503 yield _getSchemaSQLVersion('triggers').then(function (version) { 1504 return _updateDBVersion('triggers', version); 1505 }); 1506 yield _updateDBVersion('compatibility', _maxCompatibility); 1507 1508 var sql = "INSERT INTO libraries (libraryID, type, editable, filesEditable) " 1509 + "VALUES " 1510 + "(?, 'user', 1, 1)"; 1511 yield Zotero.DB.queryAsync(sql, userLibraryID); 1512 1513 yield _updateLastClientVersion(); 1514 1515 self.dbInitialized = true; 1516 }) 1517 .catch(function (e) { 1518 Zotero.debug(e, 1); 1519 Components.utils.reportError(e); 1520 let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 1521 .getService(Components.interfaces.nsIPromptService); 1522 ps.alert(null, Zotero.getString('general.error'), Zotero.getString('startupError')); 1523 throw e; 1524 }); 1525 } 1526 1527 1528 /* 1529 * Update a DB schema version tag in an existing database 1530 */ 1531 function _updateDBVersion(schema, version) { 1532 _dbVersions[schema] = version; 1533 var sql = "REPLACE INTO version (schema,version) VALUES (?,?)"; 1534 return Zotero.DB.queryAsync(sql, [schema, parseInt(version)]); 1535 } 1536 1537 1538 /** 1539 * Requires a transaction 1540 */ 1541 var _updateSchema = Zotero.Promise.coroutine(function* (schema) { 1542 var [dbVersion, schemaVersion] = yield Zotero.Promise.all( 1543 [Zotero.Schema.getDBVersion(schema), _getSchemaSQLVersion(schema)] 1544 ); 1545 if (dbVersion == schemaVersion) { 1546 return false; 1547 } 1548 if (dbVersion > schemaVersion) { 1549 let dbClientVersion = yield Zotero.DB.valueQueryAsync( 1550 "SELECT value FROM settings WHERE setting='client' AND key='lastCompatibleVersion'" 1551 ); 1552 throw new Zotero.DB.IncompatibleVersionException( 1553 `Zotero '${schema}' DB version (${dbVersion}) is newer than SQL file (${schemaVersion})`, 1554 dbClientVersion 1555 ); 1556 } 1557 let sql = yield _getSchemaSQL(schema); 1558 yield Zotero.DB.executeSQLFile(sql); 1559 return _updateDBVersion(schema, schemaVersion); 1560 }); 1561 1562 1563 var _updateCompatibility = Zotero.Promise.coroutine(function* (version) { 1564 if (version > _maxCompatibility) { 1565 throw new Error("Can't set compatibility greater than _maxCompatibility"); 1566 } 1567 1568 yield Zotero.DB.queryAsync( 1569 "REPLACE INTO settings VALUES ('client', 'lastCompatibleVersion', ?)", [Zotero.version] 1570 ); 1571 yield _updateDBVersion('compatibility', version); 1572 }); 1573 1574 1575 function _checkClientVersion() { 1576 return Zotero.DB.executeTransaction(function* () { 1577 var lastVersion = yield _getLastClientVersion(); 1578 var currentVersion = Zotero.version; 1579 1580 if (currentVersion == lastVersion) { 1581 return false; 1582 } 1583 1584 Zotero.debug(`Client version has changed from ${lastVersion} to ${currentVersion}`); 1585 1586 // Retry all queued objects immediately on upgrade 1587 yield Zotero.Sync.Data.Local.resetSyncQueueTries(); 1588 1589 // Update version 1590 yield _updateLastClientVersion(); 1591 1592 return true; 1593 }.bind(this)); 1594 } 1595 1596 1597 function _getLastClientVersion() { 1598 var sql = "SELECT value FROM settings WHERE setting='client' AND key='lastVersion'"; 1599 return Zotero.DB.valueQueryAsync(sql); 1600 } 1601 1602 1603 function _updateLastClientVersion() { 1604 var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)"; 1605 return Zotero.DB.queryAsync(sql, Zotero.version); 1606 } 1607 1608 1609 /** 1610 * Process the response from the repository 1611 * 1612 * @return {Promise:Boolean} A promise for whether the update suceeded 1613 **/ 1614 async function _handleRepositoryResponse(xmlhttp, force) { 1615 if (!xmlhttp.responseXML){ 1616 try { 1617 if (xmlhttp.status>1000){ 1618 Zotero.debug('No network connection', 2); 1619 } 1620 else { 1621 Zotero.debug(xmlhttp.status); 1622 Zotero.debug(xmlhttp.responseText); 1623 Zotero.debug('Invalid response from repository', 2); 1624 } 1625 } 1626 catch (e){ 1627 Zotero.debug('Repository cannot be contacted'); 1628 } 1629 return false; 1630 } 1631 1632 var currentTime = xmlhttp.responseXML. 1633 getElementsByTagName('currentTime')[0].firstChild.nodeValue; 1634 var lastCheckTime = Math.round(new Date().getTime()/1000); 1635 var translatorUpdates = xmlhttp.responseXML.getElementsByTagName('translator'); 1636 var styleUpdates = xmlhttp.responseXML.getElementsByTagName('style'); 1637 1638 if (!translatorUpdates.length && !styleUpdates.length){ 1639 await Zotero.DB.executeTransaction(function* (conn) { 1640 // Store the timestamp provided by the server 1641 yield _updateDBVersion('repository', currentTime); 1642 1643 // And the local timestamp of the update time 1644 yield _updateDBVersion('lastcheck', lastCheckTime); 1645 }); 1646 1647 Zotero.debug('All translators and styles are up-to-date'); 1648 if (!force) { 1649 _setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL); 1650 } 1651 return true; 1652 } 1653 1654 var updated = false; 1655 try { 1656 for (var i=0, len=translatorUpdates.length; i<len; i++){ 1657 await _translatorXMLToFile(translatorUpdates[i]); 1658 } 1659 1660 for (var i=0, len=styleUpdates.length; i<len; i++){ 1661 await _styleXMLToFile(styleUpdates[i]); 1662 } 1663 1664 // Rebuild caches 1665 await Zotero.Translators.reinit({ fromSchemaUpdate: force != 1 }); 1666 await Zotero.Styles.reinit({ fromSchemaUpdate: force != 1 }); 1667 1668 updated = true; 1669 } 1670 catch (e) { 1671 Zotero.logError(e); 1672 } 1673 1674 if (updated) { 1675 await Zotero.DB.executeTransaction(function* (conn) { 1676 // Store the timestamp provided by the server 1677 yield _updateDBVersion('repository', currentTime); 1678 1679 // And the local timestamp of the update time 1680 yield _updateDBVersion('lastcheck', lastCheckTime); 1681 }); 1682 } 1683 1684 return updated; 1685 } 1686 1687 1688 /** 1689 * Set the interval between repository queries 1690 * 1691 * We add an additional two seconds to avoid race conditions 1692 **/ 1693 function _setRepositoryTimer(delay) { 1694 var fudge = 2; // two seconds 1695 var displayInterval = delay + fudge; 1696 delay = (delay + fudge) * 1000; // convert to ms 1697 1698 if (_repositoryTimerID) { 1699 clearTimeout(_repositoryTimerID); 1700 _repositoryTimerID = null; 1701 } 1702 if (_repositoryNotificationTimerID) { 1703 clearTimeout(_repositoryNotificationTimerID); 1704 _repositoryNotificationTimerID = null; 1705 } 1706 1707 Zotero.debug('Scheduling next repository check in ' + displayInterval + ' seconds'); 1708 _repositoryTimerID = setTimeout(() => Zotero.Schema.updateFromRepository(), delay); 1709 _nextRepositoryUpdate = Date.now() + delay; 1710 } 1711 1712 1713 /** 1714 * Traverse an XML translator node from the repository and 1715 * update the local translators folder with the translator data 1716 * 1717 * @return {Promise} 1718 */ 1719 var _translatorXMLToFile = Zotero.Promise.coroutine(function* (xmlnode) { 1720 // Don't split >4K chunks into multiple nodes 1721 // https://bugzilla.mozilla.org/show_bug.cgi?id=194231 1722 xmlnode.normalize(); 1723 var translatorID = xmlnode.getAttribute('id'); 1724 var translator = Zotero.Translators.get(translatorID); 1725 1726 // Delete local version of remote translators with priority 0 1727 if (xmlnode.getElementsByTagName('priority')[0].firstChild.nodeValue === "0") { 1728 if (translator && (yield OS.File.exists(translator.path))) { 1729 Zotero.debug("Deleting translator '" + translator.label + "'"); 1730 yield OS.File.remove(translator.path); 1731 } 1732 return false; 1733 } 1734 1735 var metadata = { 1736 translatorID: translatorID, 1737 translatorType: parseInt(xmlnode.getAttribute('type')), 1738 label: xmlnode.getElementsByTagName('label')[0].firstChild.nodeValue, 1739 creator: xmlnode.getElementsByTagName('creator')[0].firstChild.nodeValue, 1740 target: (xmlnode.getElementsByTagName('target').item(0) && 1741 xmlnode.getElementsByTagName('target')[0].firstChild) 1742 ? xmlnode.getElementsByTagName('target')[0].firstChild.nodeValue 1743 : null, 1744 minVersion: xmlnode.getAttribute('minVersion'), 1745 maxVersion: xmlnode.getAttribute('maxVersion'), 1746 priority: parseInt( 1747 xmlnode.getElementsByTagName('priority')[0].firstChild.nodeValue 1748 ), 1749 inRepository: true, 1750 }; 1751 1752 var browserSupport = xmlnode.getAttribute('browserSupport'); 1753 if (browserSupport) { 1754 metadata.browserSupport = browserSupport; 1755 } 1756 1757 for (let attr of ["configOptions", "displayOptions", "hiddenPrefs"]) { 1758 try { 1759 var tags = xmlnode.getElementsByTagName(attr); 1760 if(tags.length && tags[0].firstChild) { 1761 metadata[attr] = JSON.parse(tags[0].firstChild.nodeValue); 1762 } 1763 } catch(e) { 1764 Zotero.logError("Invalid JSON for "+attr+" in new version of "+metadata.label+" ("+translatorID+") from repository"); 1765 return; 1766 } 1767 } 1768 1769 metadata.lastUpdated = xmlnode.getAttribute('lastUpdated'); 1770 1771 // detectCode can not exist or be empty 1772 var detectCode = (xmlnode.getElementsByTagName('detectCode').item(0) && 1773 xmlnode.getElementsByTagName('detectCode')[0].firstChild) 1774 ? xmlnode.getElementsByTagName('detectCode')[0].firstChild.nodeValue 1775 : null; 1776 var code = xmlnode.getElementsByTagName('code')[0].firstChild.nodeValue; 1777 code = (detectCode ? detectCode + "\n\n" : "") + code; 1778 1779 return Zotero.Translators.save(metadata, code); 1780 }); 1781 1782 1783 /** 1784 * Traverse an XML style node from the repository and 1785 * update the local styles folder with the style data 1786 */ 1787 var _styleXMLToFile = Zotero.Promise.coroutine(function* (xmlnode) { 1788 // Don't split >4K chunks into multiple nodes 1789 // https://bugzilla.mozilla.org/show_bug.cgi?id=194231 1790 xmlnode.normalize(); 1791 1792 var uri = xmlnode.getAttribute('id'); 1793 var shortName = uri.replace("http://www.zotero.org/styles/", ""); 1794 1795 // Delete local style if CSL code is empty 1796 if (!xmlnode.firstChild) { 1797 var style = Zotero.Styles.get(uri); 1798 if (style) { 1799 yield OS.File.remove(style.path); 1800 } 1801 return; 1802 } 1803 1804 // Remove renamed styles, as instructed by the server 1805 var oldID = xmlnode.getAttribute('oldID'); 1806 if (oldID) { 1807 var style = Zotero.Styles.get(oldID, true); 1808 if (style && (yield OS.File.exists(style.path))) { 1809 Zotero.debug("Deleting renamed style '" + oldID + "'"); 1810 yield OS.File.remove(style.path); 1811 } 1812 } 1813 1814 var str = xmlnode.firstChild.nodeValue; 1815 var style = Zotero.Styles.get(uri); 1816 if (style) { 1817 yield Zotero.File.removeIfExists(style.path); 1818 var destFile = style.path; 1819 } 1820 else { 1821 // Get last part of URI for filename 1822 var matches = uri.match(/([^\/]+)$/); 1823 if (!matches) { 1824 throw ("Invalid style URI '" + uri + "' from repository"); 1825 } 1826 var destFile = OS.Path.join( 1827 Zotero.getStylesDirectory().path, 1828 matches[1] + ".csl" 1829 ); 1830 if (yield OS.File.exists(destFile)) { 1831 throw new Error("Different style with filename '" + matches[1] 1832 + "' already exists"); 1833 } 1834 } 1835 1836 Zotero.debug("Saving style '" + uri + "'"); 1837 return Zotero.File.putContentsAsync(destFile, str); 1838 }); 1839 1840 1841 // TODO 1842 // 1843 // If libraryID set, make sure no relations still use a local user key, and then remove on-error code in sync.js 1844 1845 var _migrateUserDataSchema = Zotero.Promise.coroutine(function* (fromVersion, options = {}) { 1846 var toVersion = yield _getSchemaSQLVersion('userdata'); 1847 1848 if (fromVersion >= toVersion) { 1849 return false; 1850 } 1851 1852 Zotero.debug('Updating user data tables from version ' + fromVersion + ' to ' + toVersion); 1853 1854 if (options.onBeforeUpdate) { 1855 let maybePromise = options.onBeforeUpdate({ minor: options.minor }); 1856 if (maybePromise && maybePromise.then) { 1857 yield maybePromise; 1858 } 1859 } 1860 1861 Zotero.DB.requireTransaction(); 1862 1863 // Step through version changes until we reach the current version 1864 // 1865 // Each block performs the changes necessary to move from the 1866 // previous revision to that one. 1867 for (let i = fromVersion + 1; i <= toVersion; i++) { 1868 if (i == 80) { 1869 yield _updateCompatibility(1); 1870 1871 // Delete 'libraries' rows not in 'groups', which shouldn't exist 1872 yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID != 0 AND libraryID NOT IN (SELECT libraryID FROM groups)"); 1873 1874 yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld"); 1875 yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n libraryID INTEGER PRIMARY KEY,\n type TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n lastSync INT NOT NULL DEFAULT 0,\n lastStorageSync INT NOT NULL DEFAULT 0\n)"); 1876 yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (1, 'user', 1, 1)"); 1877 yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (4, 'publications', 1, 1)"); 1878 yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, libraryType, editable, filesEditable, 0, 0, 0 FROM librariesOld JOIN groups USING (libraryID)"); 1879 1880 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')"); 1881 yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema IN ('userdata2', 'userdata3')"); 1882 1883 yield Zotero.DB.queryAsync("CREATE TABLE syncCache (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n syncObjectTypeID INT NOT NULL,\n version INT NOT NULL,\n data TEXT,\n PRIMARY KEY (libraryID, key, syncObjectTypeID, version),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n)"); 1884 1885 yield Zotero.DB.queryAsync("DROP TABLE translatorCache"); 1886 yield Zotero.DB.queryAsync("CREATE TABLE translatorCache (\n fileName TEXT PRIMARY KEY,\n metadataJSON TEXT,\n lastModifiedTime INT\n);"); 1887 1888 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_annotations_itemID_itemAttachments_itemID"); 1889 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_annotations_itemID_itemAttachments_itemID"); 1890 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_annotations_itemID_itemAttachments_itemID"); 1891 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_annotations_itemID"); 1892 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collections_parentCollectionID_collections_collectionID"); 1893 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_parentCollectionID_collections_collectionID"); 1894 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collections_parentCollectionID_collections_collectionID"); 1895 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_collectionID_collections_parentCollectionID"); 1896 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collectionItems_collectionID_collections_collectionID"); 1897 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collectionItems_collectionID_collections_collectionID"); 1898 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collectionItems_collectionID_collections_collectionID"); 1899 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_collectionID_collectionItems_collectionID"); 1900 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collectionItems_itemID_items_itemID"); 1901 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collectionItems_itemID_items_itemID"); 1902 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collectionItems_itemID_items_itemID"); 1903 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_collectionItems_itemID"); 1904 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_creators_creatorDataID_creatorData_creatorDataID"); 1905 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creators_creatorDataID_creatorData_creatorDataID"); 1906 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_creators_creatorDataID_creatorData_creatorDataID"); 1907 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creatorData_creatorDataID_creators_creatorDataID"); 1908 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID"); 1909 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID"); 1910 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID"); 1911 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypes_customItemTypeID_customBaseFieldMappings_customItemTypeID"); 1912 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_baseFieldID_fields_fieldID"); 1913 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_baseFieldID_fields_fieldID"); 1914 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_customFieldID_customFields_customFieldID"); 1915 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_customFieldID_customFields_customFieldID"); 1916 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customBaseFieldMappings_customFieldID_customFields_customFieldID"); 1917 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customFields_customFieldID_customBaseFieldMappings_customFieldID"); 1918 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID"); 1919 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID"); 1920 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID"); 1921 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypes_customItemTypeID_customItemTypeFields_customItemTypeID"); 1922 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customItemTypeFields_fieldID_fields_fieldID"); 1923 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_fieldID_fields_fieldID"); 1924 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_customFieldID_customFields_customFieldID"); 1925 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customItemTypeFields_customFieldID_customFields_customFieldID"); 1926 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customFields_customFieldID_customItemTypeFields_customFieldID"); 1927 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItems_itemID_items_itemID"); 1928 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItems_itemID_items_itemID"); 1929 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItems_itemID_items_itemID"); 1930 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_fulltextItems_itemID"); 1931 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItemWords_wordID_fulltextWords_wordID"); 1932 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItemWords_wordID_fulltextWords_wordID"); 1933 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItemWords_wordID_fulltextWords_wordID"); 1934 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextWords_wordID_fulltextItemWords_wordID"); 1935 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItemWords_itemID_items_itemID"); 1936 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItemWords_itemID_items_itemID"); 1937 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItemWords_itemID_items_itemID"); 1938 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_fulltextItemWords_itemID"); 1939 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groups_libraryID_libraries_libraryID"); 1940 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groups_libraryID_libraries_libraryID"); 1941 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groups_libraryID_libraries_libraryID"); 1942 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_libraries_libraryID_groups_libraryID"); 1943 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groupItems_createdByUserID_users_userID"); 1944 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groupItems_createdByUserID_users_userID"); 1945 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groupItems_createdByUserID_users_userID"); 1946 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_users_userID_groupItems_createdByUserID"); 1947 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groupItems_lastModifiedByUserID_users_userID"); 1948 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groupItems_lastModifiedByUserID_users_userID"); 1949 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groupItems_lastModifiedByUserID_users_userID"); 1950 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_users_userID_groupItems_lastModifiedByUserID"); 1951 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_highlights_itemID_itemAttachments_itemID"); 1952 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_highlights_itemID_itemAttachments_itemID"); 1953 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_highlights_itemID_itemAttachments_itemID"); 1954 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_highlights_itemID"); 1955 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemAttachments_itemID_items_itemID"); 1956 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_items_itemID"); 1957 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemAttachments_itemID_items_itemID"); 1958 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemAttachments_itemID"); 1959 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemAttachments_sourceItemID_items_itemID"); 1960 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_sourceItemID_items_itemID"); 1961 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemAttachments_sourceItemID_items_itemID"); 1962 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemAttachments_sourceItemID"); 1963 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_itemID_items_itemID"); 1964 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_itemID_items_itemID"); 1965 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_itemID_items_itemID"); 1966 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemCreators_itemID"); 1967 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_creatorID_creators_creatorID"); 1968 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_creatorID_creators_creatorID"); 1969 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_creatorID_creators_creatorID"); 1970 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creators_creatorID_itemCreators_creatorID"); 1971 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"); 1972 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"); 1973 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"); 1974 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creatorTypes_creatorTypeID_itemCreators_creatorTypeID"); 1975 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_libraryID"); 1976 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_libraryID"); 1977 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_itemID_items_itemID"); 1978 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_itemID_items_itemID"); 1979 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemData_itemID_items_itemID"); 1980 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemData_itemID"); 1981 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_fieldID_fields_fieldID"); 1982 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_fieldID_fields_fieldID"); 1983 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_valueID_itemDataValues_valueID"); 1984 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_valueID_itemDataValues_valueID"); 1985 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemData_valueID_itemDataValues_valueID"); 1986 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemDataValues_valueID_itemData_valueID"); 1987 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemNotes_itemID_items_itemID"); 1988 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemNotes_itemID_items_itemID"); 1989 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemNotes_itemID_items_itemID"); 1990 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemNotes_itemID"); 1991 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemNotes_sourceItemID_items_itemID"); 1992 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemNotes_sourceItemID_items_itemID"); 1993 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemNotes_sourceItemID_items_itemID"); 1994 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemNotes_sourceItemID"); 1995 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_items_libraryID_libraries_libraryID"); 1996 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_libraryID_libraries_libraryID"); 1997 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_items_libraryID_libraries_libraryID"); 1998 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_libraries_libraryID_items_libraryID"); 1999 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemSeeAlso_itemID_items_itemID"); 2000 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemSeeAlso_itemID_items_itemID"); 2001 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemSeeAlso_itemID_items_itemID"); 2002 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemSeeAlso_itemID"); 2003 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemSeeAlso_linkedItemID_items_itemID"); 2004 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemSeeAlso_linkedItemID_items_itemID"); 2005 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemSeeAlso_linkedItemID_items_itemID"); 2006 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemSeeAlso_linkedItemID"); 2007 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemTags_itemID_items_itemID"); 2008 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemTags_itemID_items_itemID"); 2009 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemTags_itemID_items_itemID"); 2010 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_items_itemID_itemTags_itemID"); 2011 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemTags_tagID_tags_tagID"); 2012 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemTags_tagID_tags_tagID"); 2013 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemTags_tagID_tags_tagID"); 2014 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_tags_tagID_itemTags_tagID"); 2015 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"); 2016 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"); 2017 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"); 2018 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_savedSearches_savedSearchID_savedSearchConditions_savedSearchID"); 2019 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_deletedItems_itemID_items_itemID"); 2020 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_deletedItems_itemID_items_itemID"); 2021 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_deletedItems_itemID_items_itemID"); 2022 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_deletedItems_itemID"); 2023 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"); 2024 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"); 2025 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"); 2026 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_syncObjectTypes_syncObjectTypeID_syncDeleteLog_syncObjectTypeID"); 2027 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_proxyHosts_proxyID_proxies_proxyID"); 2028 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_proxyHosts_proxyID_proxies_proxyID"); 2029 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_proxyHosts_proxyID_proxies_proxyID"); 2030 yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_proxies_proxyID_proxyHosts_proxyID"); 2031 2032 yield Zotero.DB.queryAsync("ALTER TABLE collections RENAME TO collectionsOld"); 2033 yield Zotero.DB.queryAsync("CREATE TABLE collections (\n collectionID INTEGER PRIMARY KEY,\n collectionName TEXT NOT NULL,\n parentCollectionID INT DEFAULT NULL,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID) ON DELETE CASCADE\n)"); 2034 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO collections SELECT collectionID, collectionName, parentCollectionID, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM collectionsOld ORDER BY collectionID DESC"); 2035 yield Zotero.DB.queryAsync("CREATE INDEX collections_synced ON collections(synced)"); 2036 2037 yield Zotero.DB.queryAsync("ALTER TABLE items RENAME TO itemsOld"); 2038 yield Zotero.DB.queryAsync("CREATE TABLE items (\n itemID INTEGER PRIMARY KEY,\n itemTypeID INT NOT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2039 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO items SELECT itemID, itemTypeID, dateAdded, dateModified, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM itemsOld ORDER BY dateAdded DESC"); 2040 yield Zotero.DB.queryAsync("CREATE INDEX items_synced ON items(synced)"); 2041 2042 let rows = yield Zotero.DB.queryAsync("SELECT firstName, lastName, fieldMode, COUNT(*) FROM creatorData GROUP BY firstName, lastName, fieldMode HAVING COUNT(*) > 1"); 2043 for (let row of rows) { 2044 let ids = yield Zotero.DB.columnQueryAsync("SELECT creatorDataID FROM creatorData WHERE firstName=? AND lastName=? AND fieldMode=?", [row.firstName, row.lastName, row.fieldMode]); 2045 yield Zotero.DB.queryAsync("UPDATE creators SET creatorDataID=" + ids[0] + " WHERE creatorDataID IN (" + ids.slice(1).join(", ") + ")"); 2046 } 2047 yield Zotero.DB.queryAsync("DELETE FROM creatorData WHERE creatorDataID NOT IN (SELECT creatorDataID FROM creators)"); 2048 yield Zotero.DB.queryAsync("ALTER TABLE creators RENAME TO creatorsOld"); 2049 yield Zotero.DB.queryAsync("CREATE TABLE creators (\n creatorID INTEGER PRIMARY KEY,\n firstName TEXT,\n lastName TEXT,\n fieldMode INT,\n UNIQUE (lastName, firstName, fieldMode)\n)"); 2050 yield Zotero.DB.queryAsync("INSERT INTO creators SELECT creatorDataID, firstName, lastName, fieldMode FROM creatorData"); 2051 yield Zotero.DB.queryAsync("ALTER TABLE itemCreators RENAME TO itemCreatorsOld"); 2052 yield Zotero.DB.queryAsync("CREATE TABLE itemCreators (\n itemID INT NOT NULL,\n creatorID INT NOT NULL,\n creatorTypeID INT NOT NULL DEFAULT 1,\n orderIndex INT NOT NULL DEFAULT 0,\n PRIMARY KEY (itemID, creatorID, creatorTypeID, orderIndex),\n UNIQUE (itemID, orderIndex),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (creatorID) REFERENCES creators(creatorID) ON DELETE CASCADE,\n FOREIGN KEY (creatorTypeID) REFERENCES creatorTypes(creatorTypeID)\n)"); 2053 yield Zotero.DB.queryAsync("CREATE INDEX itemCreators_creatorTypeID ON itemCreators(creatorTypeID)"); 2054 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemCreators SELECT itemID, C.creatorID, creatorTypeID, orderIndex FROM itemCreatorsOld ICO JOIN creatorsOld CO USING (creatorID) JOIN creators C ON (CO.creatorDataID=C.creatorID)"); 2055 2056 yield Zotero.DB.queryAsync("ALTER TABLE savedSearches RENAME TO savedSearchesOld"); 2057 yield Zotero.DB.queryAsync("CREATE TABLE savedSearches (\n savedSearchID INTEGER PRIMARY KEY,\n savedSearchName TEXT NOT NULL,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n UNIQUE (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2058 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO savedSearches SELECT savedSearchID, savedSearchName, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM savedSearchesOld ORDER BY savedSearchID DESC"); 2059 yield Zotero.DB.queryAsync("CREATE INDEX savedSearches_synced ON savedSearches(synced)"); 2060 2061 yield Zotero.DB.queryAsync("ALTER TABLE tags RENAME TO tagsOld"); 2062 yield Zotero.DB.queryAsync("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n name TEXT NOT NULL UNIQUE\n)"); 2063 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tags SELECT tagID, name FROM tagsOld"); 2064 yield Zotero.DB.queryAsync("ALTER TABLE itemTags RENAME TO itemTagsOld"); 2065 yield Zotero.DB.queryAsync("CREATE TABLE itemTags (\n itemID INT NOT NULL,\n tagID INT NOT NULL,\n type INT NOT NULL,\n PRIMARY KEY (itemID, tagID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (tagID) REFERENCES tags(tagID) ON DELETE CASCADE\n)"); 2066 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemTags SELECT itemID, T.tagID, TOld.type FROM itemTagsOld ITO JOIN tagsOld TOld USING (tagID) JOIN tags T ON (TOld.name=T.name COLLATE BINARY)"); 2067 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemTags_tagID"); 2068 yield Zotero.DB.queryAsync("CREATE INDEX itemTags_tagID ON itemTags(tagID)"); 2069 2070 yield Zotero.DB.queryAsync("CREATE TABLE IF NOT EXISTS syncedSettings (\n setting TEXT NOT NULL,\n libraryID INT NOT NULL,\n value NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (setting, libraryID)\n)"); 2071 yield Zotero.DB.queryAsync("ALTER TABLE syncedSettings RENAME TO syncedSettingsOld"); 2072 yield Zotero.DB.queryAsync("CREATE TABLE syncedSettings (\n setting TEXT NOT NULL,\n libraryID INT NOT NULL,\n value NOT NULL,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n PRIMARY KEY (setting, libraryID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2073 yield Zotero.DB.queryAsync("UPDATE syncedSettingsOld SET libraryID=1 WHERE libraryID=0"); 2074 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncedSettings SELECT * FROM syncedSettingsOld"); 2075 2076 yield Zotero.DB.queryAsync("ALTER TABLE itemData RENAME TO itemDataOld"); 2077 yield Zotero.DB.queryAsync("CREATE TABLE itemData (\n itemID INT,\n fieldID INT,\n valueID,\n PRIMARY KEY (itemID, fieldID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (fieldID) REFERENCES fieldsCombined(fieldID),\n FOREIGN KEY (valueID) REFERENCES itemDataValues(valueID)\n)"); 2078 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemData SELECT * FROM itemDataOld"); 2079 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemData_fieldID"); 2080 yield Zotero.DB.queryAsync("CREATE INDEX itemData_fieldID ON itemData(fieldID)"); 2081 2082 yield Zotero.DB.queryAsync("ALTER TABLE itemNotes RENAME TO itemNotesOld"); 2083 yield Zotero.DB.queryAsync("CREATE TABLE itemNotes (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT,\n note TEXT,\n title TEXT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2084 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemNotes SELECT * FROM itemNotesOld"); 2085 yield Zotero.DB.queryAsync("CREATE INDEX itemNotes_parentItemID ON itemNotes(parentItemID)"); 2086 2087 yield Zotero.DB.queryAsync("CREATE TEMPORARY TABLE charsetsOld (charsetID INT, charset UNIQUE, canonical, PRIMARY KEY (charsetID))"); 2088 yield Zotero.DB.queryAsync("INSERT INTO charsetsOld VALUES (1,'utf-8','utf-8'), (2,'ascii','windows-1252'), (3,'windows-1250','windows-1250'), (4,'windows-1251','windows-1251'), (5,'windows-1252','windows-1252'), (6,'windows-1253','windows-1253'), (7,'windows-1254','windows-1254'), (8,'windows-1257','windows-1257'), (9,'us',NULL), (10,'us-ascii','windows-1252'), (11,'utf-7',NULL), (12,'iso8859-1','windows-1252'), (13,'iso8859-15','iso-8859-15'), (14,'iso_646.irv:1991',NULL), (15,'iso_8859-1','windows-1252'), (16,'iso_8859-1:1987','windows-1252'), (17,'iso_8859-2','iso-8859-2'), (18,'iso_8859-2:1987','iso-8859-2'), (19,'iso_8859-4','iso-8859-4'), (20,'iso_8859-4:1988','iso-8859-4'), (21,'iso_8859-5','iso-8859-5'), (22,'iso_8859-5:1988','iso-8859-5'), (23,'iso_8859-7','iso-8859-7'), (24,'iso_8859-7:1987','iso-8859-7'), (25,'iso-8859-1','windows-1252'), (26,'iso-8859-1-windows-3.0-latin-1',NULL), (27,'iso-8859-1-windows-3.1-latin-1',NULL), (28,'iso-8859-15','iso-8859-15'), (29,'iso-8859-2','iso-8859-2'), (30,'iso-8859-2-windows-latin-2',NULL), (31,'iso-8859-3','iso-8859-3'), (32,'iso-8859-4','iso-8859-4'), (33,'iso-8859-5','iso-8859-5'), (34,'iso-8859-5-windows-latin-5',NULL), (35,'iso-8859-6','iso-8859-6'), (36,'iso-8859-7','iso-8859-7'), (37,'iso-8859-8','iso-8859-8'), (38,'iso-8859-9','windows-1254'), (39,'l1','windows-1252'), (40,'l2','iso-8859-2'), (41,'l4','iso-8859-4'), (42,'latin1','windows-1252'), (43,'latin2','iso-8859-2'), (44,'latin4','iso-8859-4'), (45,'x-mac-ce',NULL), (46,'x-mac-cyrillic','x-mac-cyrillic'), (47,'x-mac-greek',NULL), (48,'x-mac-roman','macintosh'), (49,'x-mac-turkish',NULL), (50,'adobe-symbol-encoding',NULL), (51,'ansi_x3.4-1968','windows-1252'), (52,'ansi_x3.4-1986',NULL), (53,'big5','big5'), (54,'chinese','gbk'), (55,'cn-big5','big5'), (56,'cn-gb',NULL), (57,'cn-gb-isoir165',NULL), (58,'cp367',NULL), (59,'cp819','windows-1252'), (60,'cp850',NULL), (61,'cp852',NULL), (62,'cp855',NULL), (63,'cp857',NULL), (64,'cp862',NULL), (65,'cp864',NULL), (66,'cp866','ibm866'), (67,'csascii',NULL), (68,'csbig5','big5'), (69,'cseuckr','euc-kr'), (70,'cseucpkdfmtjapanese','euc-jp'), (71,'csgb2312','gbk'), (72,'cshalfwidthkatakana',NULL), (73,'cshppsmath',NULL), (74,'csiso103t618bit',NULL), (75,'csiso159jisx02121990',NULL), (76,'csiso2022jp','iso-2022-jp'), (77,'csiso2022jp2',NULL), (78,'csiso2022kr','replacement'), (79,'csiso58gb231280','gbk'), (80,'csisolatin4','iso-8859-4'), (81,'csisolatincyrillic','iso-8859-5'), (82,'csisolatingreek','iso-8859-7'), (83,'cskoi8r','koi8-r'), (84,'csksc56011987','euc-kr'), (85,'csshiftjis','shift_jis'), (86,'csunicode11',NULL), (87,'csunicode11utf7',NULL), (88,'csunicodeascii',NULL), (89,'csunicodelatin1',NULL), (90,'cswindows31latin5',NULL), (91,'cyrillic','iso-8859-5'), (92,'ecma-118','iso-8859-7'), (93,'elot_928','iso-8859-7'), (94,'euc-jp','euc-jp'), (95,'euc-kr','euc-kr'), (96,'extended_unix_code_packed_format_for_japanese',NULL), (97,'gb2312','gbk'), (98,'gb_2312-80','gbk'), (99,'greek','iso-8859-7'), (100,'greek8','iso-8859-7'), (101,'hz-gb-2312','replacement'), (102,'ibm367',NULL), (103,'ibm819','windows-1252'), (104,'ibm850',NULL), (105,'ibm852',NULL), (106,'ibm855',NULL), (107,'ibm857',NULL), (108,'ibm862',NULL), (109,'ibm864',NULL), (110,'ibm866','ibm866'), (111,'iso-10646',NULL), (112,'iso-10646-j-1',NULL), (113,'iso-10646-ucs-2',NULL), (114,'iso-10646-ucs-4',NULL), (115,'iso-10646-ucs-basic',NULL), (116,'iso-10646-unicode-latin1',NULL), (117,'iso-2022-jp','iso-2022-jp'), (118,'iso-2022-jp-2',NULL), (119,'iso-2022-kr','replacement'), (120,'iso-ir-100','windows-1252'), (121,'iso-ir-101','iso-8859-2'), (122,'iso-ir-103',NULL), (123,'iso-ir-110','iso-8859-4'), (124,'iso-ir-126','iso-8859-7'), (125,'iso-ir-144','iso-8859-5'), (126,'iso-ir-149','euc-kr'), (127,'iso-ir-159',NULL), (128,'iso-ir-58','gbk'), (129,'iso-ir-6',NULL), (130,'iso646-us',NULL), (131,'jis_x0201',NULL), (132,'jis_x0208-1983',NULL), (133,'jis_x0212-1990',NULL), (134,'koi8-r','koi8-r'), (135,'korean','euc-kr'), (136,'ks_c_5601',NULL), (137,'ks_c_5601-1987','euc-kr'), (138,'ks_c_5601-1989','euc-kr'), (139,'ksc5601','euc-kr'), (140,'ksc_5601','euc-kr'), (141,'ms_kanji','shift_jis'), (142,'shift_jis','shift_jis'), (143,'t.61',NULL), (144,'t.61-8bit',NULL), (145,'unicode-1-1-utf-7',NULL), (146,'unicode-1-1-utf-8','utf-8'), (147,'unicode-2-0-utf-7',NULL), (148,'windows-31j','shift_jis'), (149,'x-cns11643-1',NULL), (150,'x-cns11643-1110',NULL), (151,'x-cns11643-2',NULL), (152,'x-cp1250','windows-1250'), (153,'x-cp1251','windows-1251'), (154,'x-cp1253','windows-1253'), (155,'x-dectech',NULL), (156,'x-dingbats',NULL), (157,'x-euc-jp','euc-jp'), (158,'x-euc-tw',NULL), (159,'x-gb2312-11',NULL), (160,'x-imap4-modified-utf7',NULL), (161,'x-jisx0208-11',NULL), (162,'x-ksc5601-11',NULL), (163,'x-sjis','shift_jis'), (164,'x-tis620',NULL), (165,'x-unicode-2-0-utf-7',NULL), (166,'x-x-big5','big5'), (167,'x0201',NULL), (168,'x0212',NULL)"); 2089 yield Zotero.DB.queryAsync("CREATE INDEX charsetsOld_canonical ON charsetsOld(canonical)"); 2090 2091 yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments RENAME TO itemAttachmentsOld"); 2092 yield Zotero.DB.queryAsync("CREATE TABLE itemAttachments (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT,\n linkMode INT,\n contentType TEXT,\n charsetID INT,\n path TEXT,\n syncState INT DEFAULT 0,\n storageModTime INT,\n storageHash TEXT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (charsetID) REFERENCES charsets(charsetID) ON DELETE SET NULL\n)"); 2093 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemAttachments SELECT itemID, sourceItemID, linkMode, mimeType, C.charsetID, path, syncState, storageModTime, storageHash FROM itemAttachmentsOld IA LEFT JOIN charsetsOld CO ON (IA.charsetID=CO.charsetID) LEFT JOIN charsets C ON (CO.canonical=C.charset)"); 2094 yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_parentItemID ON itemAttachments(parentItemID)"); 2095 yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID)"); 2096 yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType)"); 2097 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemAttachments_syncState"); 2098 yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState)"); 2099 2100 yield _migrateUserData_80_filePaths(); 2101 2102 yield Zotero.DB.queryAsync("ALTER TABLE collectionItems RENAME TO collectionItemsOld"); 2103 yield Zotero.DB.queryAsync("CREATE TABLE collectionItems (\n collectionID INT NOT NULL,\n itemID INT NOT NULL,\n orderIndex INT NOT NULL DEFAULT 0,\n PRIMARY KEY (collectionID, itemID),\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2104 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO collectionItems SELECT * FROM collectionItemsOld"); 2105 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemID"); // incorrect old name 2106 yield Zotero.DB.queryAsync("CREATE INDEX collectionItems_itemID ON collectionItems(itemID)"); 2107 2108 yield Zotero.DB.queryAsync("ALTER TABLE savedSearchConditions RENAME TO savedSearchConditionsOld"); 2109 yield Zotero.DB.queryAsync("CREATE TABLE savedSearchConditions (\n savedSearchID INT NOT NULL,\n searchConditionID INT NOT NULL,\n condition TEXT NOT NULL,\n operator TEXT,\n value TEXT,\n required NONE,\n PRIMARY KEY (savedSearchID, searchConditionID),\n FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE\n)"); 2110 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO savedSearchConditions SELECT * FROM savedSearchConditionsOld"); 2111 yield Zotero.DB.queryAsync("DROP TABLE savedSearchConditionsOld"); 2112 2113 yield Zotero.DB.queryAsync("ALTER TABLE deletedItems RENAME TO deletedItemsOld"); 2114 yield Zotero.DB.queryAsync("CREATE TABLE deletedItems (\n itemID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2115 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO deletedItems SELECT * FROM deletedItemsOld"); 2116 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS deletedItems_dateDeleted"); 2117 yield Zotero.DB.queryAsync("CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted)"); 2118 2119 yield _migrateUserData_80_relations(); 2120 2121 yield Zotero.DB.queryAsync("ALTER TABLE groups RENAME TO groupsOld"); 2122 yield Zotero.DB.queryAsync("CREATE TABLE groups (\n groupID INTEGER PRIMARY KEY,\n libraryID INT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n version INT NOT NULL,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2123 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO groups SELECT groupID, libraryID, name, description, 0 FROM groupsOld"); 2124 2125 yield Zotero.DB.queryAsync("ALTER TABLE groupItems RENAME TO groupItemsOld"); 2126 yield Zotero.DB.queryAsync("CREATE TABLE groupItems (\n itemID INTEGER PRIMARY KEY,\n createdByUserID INT,\n lastModifiedByUserID INT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (createdByUserID) REFERENCES users(userID) ON DELETE SET NULL,\n FOREIGN KEY (lastModifiedByUserID) REFERENCES users(userID) ON DELETE SET NULL\n)"); 2127 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO groupItems SELECT * FROM groupItemsOld"); 2128 2129 let cols = yield Zotero.DB.getColumns('fulltextItems'); 2130 if (cols.indexOf("synced") == -1) { 2131 Zotero.DB.queryAsync("ALTER TABLE fulltextItems ADD COLUMN synced INT DEFAULT 0"); 2132 } 2133 yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='fulltext'"); 2134 yield Zotero.DB.queryAsync("ALTER TABLE fulltextItems RENAME TO fulltextItemsOld"); 2135 yield Zotero.DB.queryAsync("CREATE TABLE fulltextItems (\n itemID INTEGER PRIMARY KEY,\n indexedPages INT,\n totalPages INT,\n indexedChars INT,\n totalChars INT,\n version INT NOT NULL DEFAULT 0,\n synced INT NOT NULL DEFAULT 0,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2136 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO fulltextItems SELECT itemID, indexedPages, totalPages, indexedChars, totalChars, version, synced FROM fulltextItemsOld"); 2137 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS fulltextItems_version"); 2138 yield Zotero.DB.queryAsync("CREATE INDEX fulltextItems_synced ON fulltextItems(synced)"); 2139 yield Zotero.DB.queryAsync("CREATE INDEX fulltextItems_version ON fulltextItems(version)"); 2140 2141 yield Zotero.DB.queryAsync("ALTER TABLE fulltextItemWords RENAME TO fulltextItemWordsOld"); 2142 yield Zotero.DB.queryAsync("CREATE TABLE fulltextItemWords (\n wordID INT,\n itemID INT,\n PRIMARY KEY (wordID, itemID),\n FOREIGN KEY (wordID) REFERENCES fulltextWords(wordID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2143 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO fulltextItemWords SELECT * FROM fulltextItemWordsOld"); 2144 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS fulltextItemWords_itemID"); 2145 yield Zotero.DB.queryAsync("CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID)"); 2146 2147 yield Zotero.DB.queryAsync("UPDATE syncDeleteLog SET libraryID=1 WHERE libraryID=0"); 2148 yield Zotero.DB.queryAsync("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld"); 2149 yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n UNIQUE (syncObjectTypeID, libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2150 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, libraryID, key, timestamp FROM syncDeleteLogOld"); 2151 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS syncDeleteLog_timestamp"); 2152 // TODO: Something special for tag deletions? 2153 //yield Zotero.DB.queryAsync("DELETE FROM syncDeleteLog WHERE syncObjectTypeID IN (2, 5, 6)"); 2154 //yield Zotero.DB.queryAsync("DELETE FROM syncObjectTypes WHERE syncObjectTypeID IN (2, 5, 6)"); 2155 2156 yield Zotero.DB.queryAsync("UPDATE storageDeleteLog SET libraryID=1 WHERE libraryID=0"); 2157 yield Zotero.DB.queryAsync("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld"); 2158 yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (libraryID, key),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2159 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT libraryID, key, timestamp FROM storageDeleteLogOld"); 2160 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS storageDeleteLog_timestamp"); 2161 2162 yield Zotero.DB.queryAsync("ALTER TABLE annotations RENAME TO annotationsOld"); 2163 yield Zotero.DB.queryAsync("CREATE TABLE annotations (\n annotationID INTEGER PRIMARY KEY,\n itemID INT NOT NULL,\n parent TEXT,\n textNode INT,\n offset INT,\n x INT,\n y INT,\n cols INT,\n rows INT,\n text TEXT,\n collapsed BOOL,\n dateModified DATE,\n FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)"); 2164 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO annotations SELECT * FROM annotationsOld"); 2165 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS annotations_itemID"); 2166 yield Zotero.DB.queryAsync("CREATE INDEX annotations_itemID ON annotations(itemID)"); 2167 2168 yield Zotero.DB.queryAsync("ALTER TABLE highlights RENAME TO highlightsOld"); 2169 yield Zotero.DB.queryAsync("CREATE TABLE highlights (\n highlightID INTEGER PRIMARY KEY,\n itemID INT NOT NULL,\n startParent TEXT,\n startTextNode INT,\n startOffset INT,\n endParent TEXT,\n endTextNode INT,\n endOffset INT,\n dateModified DATE,\n FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)"); 2170 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO highlights SELECT * FROM highlightsOld"); 2171 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS highlights_itemID"); 2172 yield Zotero.DB.queryAsync("CREATE INDEX highlights_itemID ON highlights(itemID)"); 2173 2174 yield Zotero.DB.queryAsync("ALTER TABLE customBaseFieldMappings RENAME TO customBaseFieldMappingsOld"); 2175 yield Zotero.DB.queryAsync("CREATE TABLE customBaseFieldMappings (\n customItemTypeID INT,\n baseFieldID INT,\n customFieldID INT,\n PRIMARY KEY (customItemTypeID, baseFieldID, customFieldID),\n FOREIGN KEY (customItemTypeID) REFERENCES customItemTypes(customItemTypeID),\n FOREIGN KEY (baseFieldID) REFERENCES fields(fieldID),\n FOREIGN KEY (customFieldID) REFERENCES customFields(customFieldID)\n)"); 2176 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO customBaseFieldMappings SELECT * FROM customBaseFieldMappingsOld"); 2177 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS customBaseFieldMappings_baseFieldID"); 2178 yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS customBaseFieldMappings_customFieldID"); 2179 yield Zotero.DB.queryAsync("CREATE INDEX customBaseFieldMappings_baseFieldID ON customBaseFieldMappings(baseFieldID)"); 2180 yield Zotero.DB.queryAsync("CREATE INDEX customBaseFieldMappings_customFieldID ON customBaseFieldMappings(customFieldID)"); 2181 2182 yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='account' AND key='libraryID'"); 2183 2184 yield Zotero.DB.queryAsync("DROP TABLE annotationsOld"); 2185 yield Zotero.DB.queryAsync("DROP TABLE collectionItemsOld"); 2186 yield Zotero.DB.queryAsync("DROP TABLE charsetsOld"); 2187 yield Zotero.DB.queryAsync("DROP TABLE customBaseFieldMappingsOld"); 2188 yield Zotero.DB.queryAsync("DROP TABLE deletedItemsOld"); 2189 yield Zotero.DB.queryAsync("DROP TABLE fulltextItemWordsOld"); 2190 yield Zotero.DB.queryAsync("DROP TABLE fulltextItemsOld"); 2191 yield Zotero.DB.queryAsync("DROP TABLE groupItemsOld"); 2192 yield Zotero.DB.queryAsync("DROP TABLE groupsOld"); 2193 yield Zotero.DB.queryAsync("DROP TABLE highlightsOld"); 2194 yield Zotero.DB.queryAsync("DROP TABLE itemAttachmentsOld"); 2195 yield Zotero.DB.queryAsync("DROP TABLE itemCreatorsOld"); 2196 yield Zotero.DB.queryAsync("DROP TABLE itemDataOld"); 2197 yield Zotero.DB.queryAsync("DROP TABLE itemNotesOld"); 2198 yield Zotero.DB.queryAsync("DROP TABLE itemTagsOld"); 2199 yield Zotero.DB.queryAsync("DROP TABLE savedSearchesOld"); 2200 yield Zotero.DB.queryAsync("DROP TABLE storageDeleteLogOld"); 2201 yield Zotero.DB.queryAsync("DROP TABLE syncDeleteLogOld"); 2202 yield Zotero.DB.queryAsync("DROP TABLE syncedSettingsOld"); 2203 yield Zotero.DB.queryAsync("DROP TABLE collectionsOld"); 2204 yield Zotero.DB.queryAsync("DROP TABLE creatorsOld"); 2205 yield Zotero.DB.queryAsync("DROP TABLE creatorData"); 2206 yield Zotero.DB.queryAsync("DROP TABLE itemsOld"); 2207 yield Zotero.DB.queryAsync("DROP TABLE tagsOld"); 2208 yield Zotero.DB.queryAsync("DROP TABLE librariesOld"); 2209 2210 } 2211 2212 else if (i == 81) { 2213 yield _updateCompatibility(2); 2214 2215 yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld"); 2216 yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n libraryID INTEGER PRIMARY KEY,\n type TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n version INT NOT NULL DEFAULT 0,\n storageVersion INT NOT NULL DEFAULT 0,\n lastSync INT NOT NULL DEFAULT 0\n)"); 2217 yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, type, editable, filesEditable, version, 0, lastSync FROM librariesOld"); 2218 yield Zotero.DB.queryAsync("DROP TABLE librariesOld"); 2219 2220 yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema LIKE ?", "storage_%"); 2221 } 2222 2223 else if (i == 82) { 2224 yield Zotero.DB.queryAsync("DELETE FROM itemTypeFields WHERE itemTypeID=17 AND orderIndex BETWEEN 3 AND 9"); 2225 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 44, NULL, 3)"); 2226 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 96, NULL, 4)"); 2227 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 117, NULL, 5)"); 2228 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 43, NULL, 6)"); 2229 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 97, NULL, 7)"); 2230 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 98, NULL, 8)"); 2231 yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 42, NULL, 9)"); 2232 } 2233 2234 else if (i == 83) { 2235 // Feeds 2236 yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feeds"); 2237 yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feedItems"); 2238 yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n libraryID INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n url TEXT NOT NULL UNIQUE,\n lastUpdate TIMESTAMP,\n lastCheck TIMESTAMP,\n lastCheckError TEXT,\n cleanupAfter INT,\n refreshInterval INT,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2239 yield Zotero.DB.queryAsync("CREATE TABLE feedItems (\n itemID INTEGER PRIMARY KEY,\n guid TEXT NOT NULL UNIQUE,\n readTime TIMESTAMP,\n translatedTime TIMESTAMP,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)"); 2240 } 2241 2242 else if (i == 84) { 2243 yield Zotero.DB.queryAsync("CREATE TABLE syncQueue (\n libraryID INT NOT NULL,\n key TEXT NOT NULL,\n syncObjectTypeID INT NOT NULL,\n lastCheck TIMESTAMP,\n tries INT,\n PRIMARY KEY (libraryID, key, syncObjectTypeID),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID) ON DELETE CASCADE\n)"); 2244 } 2245 2246 else if (i == 85) { 2247 yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema IN ('sync', 'syncdeletelog')"); 2248 } 2249 2250 else if (i == 86) { 2251 let rows = yield Zotero.DB.queryAsync("SELECT ROWID AS id, * FROM itemRelations WHERE SUBSTR(object, 1, 18)='http://zotero.org/' AND NOT INSTR(object, 'item')"); 2252 for (let i = 0; i < rows.length; i++) { 2253 // http://zotero.org/users/local/aFeGasdGSdH/8QZ36WQ3 -> http://zotero.org/users/local/aFeGasdGSdH/items/8QZ36WQ3 2254 // http://zotero.org/users/12341/8QZ36WQ3 -> http://zotero.org/users/12341/items/8QZ36WQ3 2255 // http://zotero.org/groups/12341/8QZ36WQ3 -> http://zotero.org/groups/12341/items/8QZ36WQ3 2256 let newObject = rows[i].object.replace(/^(http:\/\/zotero.org\/(?:(?:users|groups)\/\d+|users\/local\/[^\/]+))\/([A-Z0-9]{8})$/, '$1/items/$2'); 2257 yield Zotero.DB.queryAsync("UPDATE itemRelations SET object=? WHERE ROWID=?", [newObject, rows[i].id]); 2258 } 2259 } 2260 2261 else if (i == 87) { 2262 yield _updateCompatibility(3); 2263 let rows = yield Zotero.DB.queryAsync("SELECT valueID, value FROM itemDataValues WHERE TYPEOF(value) = 'integer'"); 2264 for (let i = 0; i < rows.length; i++) { 2265 let row = rows[i]; 2266 let valueID = yield Zotero.DB.valueQueryAsync("SELECT valueID FROM itemDataValues WHERE value=?", "" + row.value); 2267 if (valueID) { 2268 yield Zotero.DB.queryAsync("UPDATE itemData SET valueID=? WHERE valueID=?", [valueID, row.valueID]); 2269 yield Zotero.DB.queryAsync("DELETE FROM itemDataValues WHERE valueID=?", row.valueID); 2270 } 2271 else { 2272 yield Zotero.DB.queryAsync("UPDATE itemDataValues SET value=? WHERE valueID=?", ["" + row.value, row.valueID]); 2273 } 2274 } 2275 } 2276 2277 else if (i == 89) { 2278 let groupLibraryMap = {}; 2279 let libraryGroupMap = {}; 2280 let resolveLibrary = Zotero.Promise.coroutine(function* (usersOrGroups, id) { 2281 if (usersOrGroups == 'users') return 1; 2282 if (groupLibraryMap[id] !== undefined) return groupLibraryMap[id]; 2283 return groupLibraryMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT libraryID FROM groups WHERE groupID=?", id)); 2284 }); 2285 let resolveGroup = Zotero.Promise.coroutine(function* (id) { 2286 if (libraryGroupMap[id] !== undefined) return libraryGroupMap[id]; 2287 return libraryGroupMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT groupID FROM groups WHERE libraryID=?", id)); 2288 }); 2289 2290 let userSegment = yield Zotero.DB.valueQueryAsync("SELECT IFNULL((SELECT value FROM settings WHERE setting='account' AND key='userID'), 'local/' || (SELECT value FROM settings WHERE setting='account' AND key='localUserKey'))"); 2291 2292 let predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate='dc:relation'"); 2293 if (!predicateID) continue; 2294 let rows = yield Zotero.DB.queryAsync("SELECT ROWID AS id, * FROM itemRelations WHERE predicateID=?", predicateID); 2295 for (let i = 0; i < rows.length; i++) { 2296 let row = rows[i]; 2297 let newSubjectlibraryID, newSubjectKey, newObjectKey; 2298 2299 let object = row.object; 2300 if (!object.startsWith('http://zotero.org/')) continue; 2301 object = object.substr(18); 2302 let newObjectURI = 'http://zotero.org/'; 2303 2304 // Fix missing 'local' from 80 2305 let matches = object.match(/^users\/([a-zA-Z0-9]{8})\/items\/([A-Z0-9]{8})$/); 2306 // http://zotero.org/users/aFeGasdG/items/8QZ36WQ3 -> http://zotero.org/users/local/aFeGasdG/items/8QZ36WQ3 2307 if (matches) { 2308 object = `users/local/${matches[1]}/items/${matches[2]}`; 2309 let uri = `http://zotero.org/users/local/${matches[1]}/items/${matches[2]}`; 2310 yield Zotero.DB.queryAsync("UPDATE itemRelations SET object=? WHERE ROWID=?", [uri, row.id]); 2311 } 2312 2313 // Add missing bidirectional from 80 2314 if (object.startsWith('users')) { 2315 matches = object.match(/^users\/(local\/\w+|\d+)\/items\/([A-Z0-9]{8})$/); 2316 if (!matches) continue; 2317 newSubjectlibraryID = 1; 2318 newSubjectKey = matches[2]; 2319 } 2320 else if (object.startsWith('groups')) { 2321 matches = object.match(/^groups\/(\d+)\/items\/([A-Z0-9]{8})$/); 2322 if (!matches) continue; 2323 newSubjectlibraryID = yield resolveLibrary('groups', matches[1]); 2324 newSubjectKey = matches[2]; 2325 } 2326 else { 2327 continue; 2328 } 2329 let newSubjectID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [newSubjectlibraryID, newSubjectKey]); 2330 if (!newSubjectID) continue; 2331 let { libraryID, key } = yield Zotero.DB.rowQueryAsync("SELECT libraryID, key FROM items WHERE itemID=?", row.itemID); 2332 if (libraryID == 1) { 2333 newObjectURI += `users/${userSegment}/items/${key}`; 2334 } 2335 else { 2336 let groupID = yield resolveGroup(libraryID); 2337 newObjectURI += `groups/${groupID}/items/${key}`; 2338 } 2339 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemRelations VALUES (?, ?, ?)", [newSubjectID, predicateID, newObjectURI]); 2340 } 2341 } 2342 2343 else if (i == 90) { 2344 yield _updateCompatibility(4); 2345 yield Zotero.DB.queryAsync("ALTER TABLE feeds RENAME TO feedsOld"); 2346 yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n libraryID INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n url TEXT NOT NULL UNIQUE,\n lastUpdate TIMESTAMP,\n lastCheck TIMESTAMP,\n lastCheckError TEXT,\n cleanupReadAfter INT,\n cleanupUnreadAfter INT,\n refreshInterval INT,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)"); 2347 yield Zotero.DB.queryAsync("INSERT INTO feeds SELECT libraryID, name, url, lastUpdate, lastCheck, lastCheckError, 30, cleanupAfter, refreshInterval FROM feedsOld"); 2348 yield Zotero.DB.queryAsync("DROP TABLE feedsOld"); 2349 } 2350 2351 else if (i == 91) { 2352 yield Zotero.DB.queryAsync("ALTER TABLE libraries ADD COLUMN archived INT NOT NULL DEFAULT 0"); 2353 } 2354 2355 else if (i == 92) { 2356 let userID = yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'"); 2357 if (userID) { 2358 yield Zotero.DB.queryAsync("UPDATE itemRelations SET object='http://zotero.org/users/' || ? || SUBSTR(object, 39) WHERE object LIKE ?", [userID, 'http://zotero.org/users/local/%']); 2359 } 2360 } 2361 2362 else if (i == 93) { 2363 yield _updateCompatibility(5); 2364 yield Zotero.DB.queryAsync("CREATE TABLE publicationsItems (\n itemID INTEGER PRIMARY KEY\n);"); 2365 yield Zotero.DB.queryAsync("INSERT INTO publicationsItems SELECT itemID FROM items WHERE libraryID=4"); 2366 yield Zotero.DB.queryAsync("UPDATE OR IGNORE items SET libraryID=1, synced=0 WHERE libraryID=4"); 2367 yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE object LIKE ? AND object LIKE ?", ['http://zotero.org/users/%', '%/publications/items%']); 2368 yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID=4"); 2369 2370 let rows = yield Zotero.DB.queryAsync("SELECT itemID, data FROM syncCache JOIN items USING (libraryID, key, version) WHERE syncObjectTypeID=3"); 2371 let ids = []; 2372 for (let row of rows) { 2373 let json = JSON.parse(row.data); 2374 if (json.data && json.data.inPublications) { 2375 ids.push(row.itemID); 2376 } 2377 } 2378 if (ids.length) { 2379 yield Zotero.DB.queryAsync("INSERT INTO publicationsItems (itemID) VALUES " 2380 + ids.map(id => `(${id})`).join(', ')); 2381 } 2382 } 2383 2384 else if (i == 94) { 2385 let ids = yield Zotero.DB.columnQueryAsync("SELECT itemID FROM publicationsItems WHERE itemID IN (SELECT itemID FROM items JOIN itemAttachments USING (itemID) WHERE linkMode=2)"); 2386 for (let id of ids) { 2387 yield Zotero.DB.queryAsync("UPDATE items SET synced=0, clientDateModified=CURRENT_TIMESTAMP WHERE itemID=?", id); 2388 } 2389 yield Zotero.DB.queryAsync("DELETE FROM publicationsItems WHERE itemID IN (SELECT itemID FROM items JOIN itemAttachments USING (itemID) WHERE linkMode=2)"); 2390 } 2391 2392 else if (i == 95) { 2393 yield Zotero.DB.queryAsync("DELETE FROM publicationsItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE libraryID=1)"); 2394 } 2395 2396 else if (i == 96) { 2397 yield Zotero.DB.queryAsync("REPLACE INTO fileTypeMIMETypes VALUES(7, 'application/vnd.ms-powerpoint')"); 2398 } 2399 2400 else if (i == 97) { 2401 let where = "WHERE predicate IN (" + Array.from(Array(20).keys()).map(i => `'${i}'`).join(', ') + ")"; 2402 let rows = yield Zotero.DB.queryAsync("SELECT * FROM relationPredicates " + where); 2403 for (let row of rows) { 2404 yield Zotero.DB.columnQueryAsync("UPDATE items SET synced=0 WHERE itemID IN (SELECT itemID FROM itemRelations WHERE predicateID=?)", row.predicateID); 2405 yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=?", row.predicateID); 2406 } 2407 yield Zotero.DB.queryAsync("DELETE FROM relationPredicates " + where); 2408 } 2409 2410 else if (i == 98) { 2411 yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=(SELECT predicateID FROM relationPredicates WHERE predicate='owl:sameAs') AND object LIKE ?", 'http://www.archive.org/%'); 2412 } 2413 2414 else if (i == 99) { 2415 yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=(SELECT predicateID FROM relationPredicates WHERE predicate='dc:isReplacedBy')"); 2416 yield Zotero.DB.queryAsync("DELETE FROM relationPredicates WHERE predicate='dc:isReplacedBy'"); 2417 } 2418 2419 else if (i == 100) { 2420 let userID = yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'"); 2421 let predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate='dc:relation'"); 2422 if (userID && predicateID) { 2423 let rows = yield Zotero.DB.queryAsync("SELECT itemID, object FROM items JOIN itemRelations IR USING (itemID) WHERE libraryID=? AND predicateID=?", [1, predicateID]); 2424 for (let row of rows) { 2425 let matches = row.object.match(/^http:\/\/zotero.org\/users\/(\d+)\/items\/([A-Z0-9]+)$/); 2426 if (matches) { 2427 // Wrong libraryID 2428 if (matches[1] != userID) { 2429 yield Zotero.DB.queryAsync(`UPDATE OR REPLACE itemRelations SET object='http://zotero.org/users/${userID}/items/${matches[2]}' WHERE itemID=? AND predicateID=?`, [row.itemID, predicateID]); 2430 } 2431 } 2432 } 2433 } 2434 } 2435 2436 else if (i == 101) { 2437 Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js"); 2438 let importer = new Zotero_Import_Mendeley(); 2439 if (yield importer.hasImportedFiles()) { 2440 yield importer.queueFileCleanup(); 2441 } 2442 } 2443 2444 // If breaking compatibility or doing anything dangerous, clear minorUpdateFrom 2445 } 2446 2447 yield _updateDBVersion('userdata', toVersion); 2448 return true; 2449 }); 2450 2451 2452 // 2453 // Longer functions for specific upgrade steps 2454 // 2455 2456 /** 2457 * Convert Mozilla-specific relative descriptors below storage and base directories to UTF-8 2458 * paths using '/' separators 2459 */ 2460 var _migrateUserData_80_filePaths = Zotero.Promise.coroutine(function* () { 2461 var rows = yield Zotero.DB.queryAsync("SELECT itemID, libraryID, key, linkMode, path FROM items JOIN itemAttachments USING (itemID) WHERE path != ''"); 2462 var tmpDirFile = Zotero.getTempDirectory(); 2463 var tmpFilePath = OS.Path.normalize(tmpDirFile.path) 2464 // Since relative paths can be applied on different platforms, 2465 // just use "/" everywhere for oonsistency, and convert on use 2466 .replace(/\\/g, '/'); 2467 2468 for (let i = 0; i < rows.length; i++) { 2469 let row = rows[i]; 2470 let libraryKey = row.libraryID + "/" + row.key; 2471 let path = row.path; 2472 let prefix = path.match(/^(attachments|storage):/); 2473 if (prefix) { 2474 prefix = prefix[0]; 2475 let relPath = path.substr(prefix.length) 2476 let file = tmpDirFile.clone(); 2477 file.setRelativeDescriptor(file, relPath); 2478 path = OS.Path.normalize(file.path).replace(/\\/g, '/'); 2479 2480 // setRelativeDescriptor() silently uses the parent directory on Windows 2481 // if the filename contains certain characters, so strip them — 2482 // but don't skip characters outside of XML range, since they may be 2483 // correct in the opaque relative descriptor string 2484 // 2485 // This is a bad place for this, since the change doesn't make it 2486 // back up to the sync server, but we do it to make sure we don't 2487 // accidentally use the parent dir. 2488 if (path == tmpFilePath) { 2489 file.setRelativeDescriptor(file, Zotero.File.getValidFileName(relPath, true)); 2490 path = OS.Path.normalize(file.path); 2491 if (path == tmpFilePath) { 2492 Zotero.logError("Cannot fix relative descriptor for item " + libraryKey + " -- not converting path"); 2493 continue; 2494 } 2495 else { 2496 Zotero.logError("Filtered relative descriptor for item " + libraryKey); 2497 } 2498 } 2499 2500 if (!path.startsWith(tmpFilePath)) { 2501 Zotero.logError(path + " does not start with " + tmpFilePath 2502 + " -- not converting relative path for item " + libraryKey); 2503 continue; 2504 } 2505 path = prefix + path.substr(tmpFilePath.length + 1); 2506 } 2507 else { 2508 let file = Components.classes["@mozilla.org/file/local;1"] 2509 .createInstance(Components.interfaces.nsILocalFile); 2510 try { 2511 file.persistentDescriptor = path; 2512 } 2513 catch (e) { 2514 Zotero.logError("Invalid persistent descriptor for item " + libraryKey + " -- not converting path"); 2515 continue; 2516 } 2517 path = file.path; 2518 } 2519 2520 yield Zotero.DB.queryAsync("UPDATE itemAttachments SET path=? WHERE itemID=?", [path, row.itemID]); 2521 } 2522 }) 2523 2524 var _migrateUserData_80_relations = Zotero.Promise.coroutine(function* () { 2525 yield Zotero.DB.queryAsync("CREATE TABLE relationPredicates (\n predicateID INTEGER PRIMARY KEY,\n predicate TEXT UNIQUE\n)"); 2526 2527 yield Zotero.DB.queryAsync("CREATE TABLE collectionRelations (\n collectionID INT NOT NULL,\n predicateID INT NOT NULL,\n object TEXT NOT NULL,\n PRIMARY KEY (collectionID, predicateID, object),\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)"); 2528 yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID)"); 2529 yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_object ON collectionRelations(object);"); 2530 yield Zotero.DB.queryAsync("CREATE TABLE itemRelations (\n itemID INT NOT NULL,\n predicateID INT NOT NULL,\n object TEXT NOT NULL,\n PRIMARY KEY (itemID, predicateID, object),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)"); 2531 yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_predicateID ON itemRelations(predicateID)"); 2532 yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_object ON itemRelations(object);"); 2533 2534 yield Zotero.DB.queryAsync("UPDATE relations SET subject=object, predicate='dc:replaces', object=subject WHERE predicate='dc:isReplacedBy'"); 2535 2536 var start = 0; 2537 var limit = 100; 2538 var collectionSQL = "INSERT OR IGNORE INTO collectionRelations (collectionID, predicateID, object) VALUES "; 2539 var itemSQL = "INSERT OR IGNORE INTO itemRelations (itemID, predicateID, object) VALUES "; 2540 // 1 2 1 2 3 4 2541 var objectRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/(collections|items)\/([A-Z0-9]{8})/; 2542 // 1 2 1 2 3 2543 var itemRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/items\/([A-Z0-9]{8})/; 2544 var report = ""; 2545 var groupLibraryIDMap = {}; 2546 var resolveLibrary = Zotero.Promise.coroutine(function* (usersOrGroups, id) { 2547 if (usersOrGroups == 'users') return 1; 2548 if (groupLibraryIDMap[id] !== undefined) return groupLibraryIDMap[id]; 2549 return groupLibraryIDMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT libraryID FROM groups WHERE groupID=?", id)); 2550 }); 2551 var predicateMap = {}; 2552 var resolvePredicate = Zotero.Promise.coroutine(function* (predicate) { 2553 if (predicateMap[predicate]) return predicateMap[predicate]; 2554 yield Zotero.DB.queryAsync("INSERT INTO relationPredicates (predicateID, predicate) VALUES (NULL, ?)", predicate); 2555 return predicateMap[predicate] = Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", predicate); 2556 }); 2557 while (true) { 2558 let rows = yield Zotero.DB.queryAsync("SELECT subject, predicate, object FROM relations LIMIT ?, ?", [start, limit]); 2559 if (!rows.length) { 2560 break; 2561 } 2562 2563 let collectionRels = []; 2564 let itemRels = []; 2565 2566 for (let i = 0; i < rows.length; i++) { 2567 let row = rows[i]; 2568 let concat = row.subject + " - " + row.predicate + " - " + row.object; 2569 2570 try { 2571 switch (row.predicate) { 2572 case 'owl:sameAs': 2573 let subjectMatch = row.subject.match(objectRE); 2574 let objectMatch = row.object.match(objectRE); 2575 if (!subjectMatch && !objectMatch) { 2576 Zotero.debug("No match for relation subject or object: " + concat, 2); 2577 report += concat + "\n"; 2578 continue; 2579 } 2580 // Remove empty captured groups 2581 subjectMatch = subjectMatch ? subjectMatch.filter(x => x) : false; 2582 objectMatch = objectMatch ? objectMatch.filter(x => x) : false; 2583 let subjectLibraryID = false; 2584 let subjectType = false; 2585 let subject = false; 2586 let objectLibraryID = false; 2587 let objectType = false; 2588 let object = false; 2589 if (subjectMatch) { 2590 subjectLibraryID = (yield resolveLibrary(subjectMatch[1], subjectMatch[2])) || false; 2591 subjectType = subjectMatch[3]; 2592 } 2593 if (objectMatch) { 2594 objectLibraryID = (yield resolveLibrary(objectMatch[1], objectMatch[2])) || false; 2595 objectType = objectMatch[3]; 2596 } 2597 // Use subject if it's a user library or it isn't but neither is object, and if object can be found 2598 if (subjectLibraryID && (subjectLibraryID == 1 || objectLibraryID != 1)) { 2599 let key = subjectMatch[4]; 2600 if (subjectType == 'collection') { 2601 let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [subjectLibraryID, key]); 2602 if (collectionID) { 2603 collectionRels.push([collectionID, row.predicate, row.object]); 2604 continue; 2605 } 2606 } 2607 else { 2608 let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [subjectLibraryID, key]); 2609 if (itemID) { 2610 itemRels.push([itemID, row.predicate, row.object]); 2611 continue; 2612 } 2613 } 2614 } 2615 2616 // Otherwise use object if it can be found 2617 if (objectLibraryID) { 2618 let key = objectMatch[4]; 2619 if (objectType == 'collection') { 2620 let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [objectLibraryID, key]); 2621 if (collectionID) { 2622 collectionRels.push([collectionID, row.predicate, row.subject]); 2623 continue; 2624 } 2625 } 2626 else { 2627 let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [objectLibraryID, key]); 2628 if (itemID) { 2629 itemRels.push([itemID, row.predicate, row.subject]); 2630 continue; 2631 } 2632 } 2633 Zotero.debug("Neither subject nor object found: " + concat, 2); 2634 report += concat + "\n"; 2635 } 2636 break; 2637 2638 case 'dc:replaces': 2639 let match = row.subject.match(itemRE); 2640 if (!match) { 2641 Zotero.debug("Unrecognized subject: " + concat, 2); 2642 report += concat + "\n"; 2643 continue; 2644 } 2645 // Remove empty captured groups 2646 match = match.filter(x => x); 2647 let libraryID; 2648 // Users 2649 if (match[1] == 'users') { 2650 let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [1, match[3]]); 2651 if (!itemID) { 2652 Zotero.debug("Subject not found: " + concat, 2); 2653 report += concat + "\n"; 2654 continue; 2655 } 2656 itemRels.push([itemID, row.predicate, row.object]); 2657 } 2658 // Groups 2659 else { 2660 let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items JOIN groups USING (libraryID) WHERE groupID=? AND key=?", [match[2], match[3]]); 2661 if (!itemID) { 2662 Zotero.debug("Subject not found: " + concat, 2); 2663 report += concat + "\n"; 2664 continue; 2665 } 2666 itemRels.push([itemID, row.predicate, row.object]); 2667 } 2668 break; 2669 2670 default: 2671 Zotero.debug("Unknown predicate '" + row.predicate + "': " + concat, 2); 2672 report += concat + "\n"; 2673 continue; 2674 } 2675 } 2676 catch (e) { 2677 Zotero.logError(e); 2678 } 2679 } 2680 2681 if (collectionRels.length) { 2682 for (let i = 0; i < collectionRels.length; i++) { 2683 collectionRels[i][1] = yield resolvePredicate(collectionRels[i][1]); 2684 } 2685 yield Zotero.DB.queryAsync(collectionSQL + collectionRels.map(() => "(?, ?, ?)").join(", "), collectionRels.reduce((x, y) => x.concat(y))); 2686 } 2687 if (itemRels.length) { 2688 for (let i = 0; i < itemRels.length; i++) { 2689 itemRels[i][1] = yield resolvePredicate(itemRels[i][1]); 2690 } 2691 yield Zotero.DB.queryAsync(itemSQL + itemRels.map(() => "(?, ?, ?)").join(", "), itemRels.reduce((x, y) => x.concat(y))); 2692 } 2693 2694 start += limit; 2695 } 2696 if (report.length) { 2697 report = "Removed relations:\n\n" + report; 2698 Zotero.debug(report); 2699 } 2700 yield Zotero.DB.queryAsync("DROP TABLE relations"); 2701 2702 // 2703 // Migrate related items 2704 // 2705 // If no user id and no local key, create a local key 2706 if (!(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'")) 2707 && !(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='localUserKey'"))) { 2708 yield Zotero.DB.queryAsync("INSERT INTO settings (setting, key, value) VALUES ('account', 'localUserKey', ?)", Zotero.randomString(8)); 2709 } 2710 var predicateID = predicateMap["dc:relation"]; 2711 if (!predicateID) { 2712 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO relationPredicates VALUES (NULL, 'dc:relation')"); 2713 predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", 'dc:relation'); 2714 } 2715 yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemRelations SELECT ISA.itemID, " + predicateID + ", 'http://zotero.org/' || (CASE WHEN G.libraryID IS NULL THEN 'users/' || IFNULL((SELECT value FROM settings WHERE setting='account' AND key='userID'), 'local/' || (SELECT value FROM settings WHERE setting='account' AND key='localUserKey')) ELSE 'groups/' || G.groupID END) || '/items/' || I.key FROM itemSeeAlso ISA JOIN items I ON (ISA.linkedItemID=I.itemID) LEFT JOIN groups G USING (libraryID)"); 2716 yield Zotero.DB.queryAsync("DROP TABLE itemSeeAlso"); 2717 }); 2718 }