zotero.js (82793B)
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 // Commonly used imports accessible anywhere 27 Components.utils.import("resource://zotero/config.js"); 28 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 29 Components.utils.import("resource://gre/modules/Services.jsm"); 30 Components.utils.import("resource://gre/modules/osfile.jsm"); 31 Components.utils.import("resource://gre/modules/PluralForm.jsm"); 32 Components.classes["@mozilla.org/net/osfileconstantsservice;1"] 33 .getService(Components.interfaces.nsIOSFileConstantsService) 34 .init(); 35 36 Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); 37 38 /* 39 * Core functions 40 */ 41 (function(){ 42 // Privileged (public) methods 43 this.getStorageDirectory = getStorageDirectory; 44 this.debug = debug; 45 this.log = log; 46 this.logError = logError; 47 this.localeJoin = localeJoin; 48 this.setFontSize = setFontSize; 49 this.flattenArguments = flattenArguments; 50 this.getAncestorByTagName = getAncestorByTagName; 51 this.randomString = randomString; 52 this.moveToUnique = moveToUnique; 53 this.reinit = reinit; // defined in zotero-service.js 54 55 // Public properties 56 this.initialized = false; 57 this.skipLoading = false; 58 this.startupError; 59 this.__defineGetter__("startupErrorHandler", function() { return _startupErrorHandler; }); 60 this.version; 61 this.platform; 62 this.locale; 63 this.dir; // locale direction: 'ltr' or 'rtl' 64 this.isMac; 65 this.isWin; 66 this.initialURL; // used by Schema to show the changelog on upgrades 67 this.Promise = require('resource://zotero/bluebird.js'); 68 69 this.getActiveZoteroPane = function() { 70 var win = Services.wm.getMostRecentWindow("navigator:browser"); 71 return win ? win.ZoteroPane : null; 72 }; 73 74 /** 75 * @property {Boolean} locked Whether all Zotero panes are locked 76 * with an overlay 77 */ 78 this.__defineGetter__('locked', function () { return _locked; }); 79 this.__defineSetter__('locked', function (lock) { 80 var wasLocked = _locked; 81 _locked = lock; 82 83 if (!wasLocked && lock) { 84 this.unlockDeferred = Zotero.Promise.defer(); 85 this.unlockPromise = this.unlockDeferred.promise; 86 } 87 else if (wasLocked && !lock) { 88 Zotero.debug("Running unlock callbacks"); 89 this.unlockDeferred.resolve(); 90 } 91 }); 92 93 /** 94 * @property {Boolean} closing True if the application is closing. 95 */ 96 this.closing = false; 97 98 99 this.unlockDeferred; 100 this.unlockPromise; 101 this.initializationDeferred; 102 this.initializationPromise; 103 104 this.hiDPISuffix = ""; 105 106 var _startupErrorHandler; 107 var _localizedStringBundle; 108 109 var _locked = false; 110 var _shutdownListeners = []; 111 var _progressMessage; 112 var _progressMeters; 113 var _progressPopup; 114 var _lastPercentage; 115 116 // whether we are waiting for another Zotero process to release its DB lock 117 var _waitingForDBLock = false; 118 119 /** 120 * Maintains nsITimers to be used when Zotero.wait() completes (to reduce performance penalty 121 * of initializing new objects) 122 */ 123 var _waitTimers = []; 124 125 /** 126 * Maintains nsITimerCallbacks to be used when Zotero.wait() completes 127 */ 128 var _waitTimerCallbacks = []; 129 130 /** 131 * Maintains running nsITimers in global scope, so that they don't disappear randomly 132 */ 133 var _runningTimers = new Map(); 134 135 var _startupTime = new Date(); 136 // Errors that were in the console at startup 137 var _startupErrors = []; 138 // Number of errors to maintain in the recent errors buffer 139 const ERROR_BUFFER_SIZE = 25; 140 // A rolling buffer of the last ERROR_BUFFER_SIZE errors 141 var _recentErrors = []; 142 143 /** 144 * Initialize the extension 145 * 146 * @return {Promise<Boolean>} 147 */ 148 this.init = Zotero.Promise.coroutine(function* (options) { 149 if (this.initialized || this.skipLoading) { 150 return false; 151 } 152 153 this.locked = true; 154 this.initializationDeferred = Zotero.Promise.defer(); 155 this.initializationPromise = this.initializationDeferred.promise; 156 this.uiReadyDeferred = Zotero.Promise.defer(); 157 this.uiReadyPromise = this.uiReadyDeferred.promise; 158 159 // Add a function to Zotero.Promise to check whether a value is still defined, and if not 160 // to throw a specific error that's ignored by the unhandled rejection handler in 161 // bluebird.js. This allows for easily cancelling promises when they're no longer 162 // needed, for example after a binding is destroyed. 163 // 164 // Example usage: 165 // 166 // getAsync.tap(() => Zotero.Promise.check(this.mode)) 167 // 168 // If the binding is destroyed while getAsync() is being resolved and this.mode no longer 169 // exists, subsequent lines won't be run, and nothing will be logged to the console. 170 this.Promise.check = function (val) { 171 if (!val && val !== 0) { 172 let e = new Error; 173 e.name = "ZoteroPromiseInterrupt"; 174 throw e; 175 } 176 }; 177 178 if (options) { 179 let opts = [ 180 'openPane', 181 'test', 182 'automatedTest', 183 'skipBundledFiles' 184 ]; 185 opts.filter(opt => options[opt]).forEach(opt => this[opt] = true); 186 187 this.forceDataDir = options.forceDataDir; 188 } 189 190 this.mainThread = Services.tm.mainThread; 191 192 this.clientName = ZOTERO_CONFIG.CLIENT_NAME; 193 194 var appInfo = Components.classes["@mozilla.org/xre/app-info;1"] 195 .getService(Components.interfaces.nsIXULAppInfo); 196 this.platformVersion = appInfo.platformVersion; 197 this.platformMajorVersion = parseInt(appInfo.platformVersion.match(/^[0-9]+/)[0]); 198 this.isFx = true; 199 this.isClient = true; 200 this.isStandalone = Services.appinfo.ID == ZOTERO_CONFIG['GUID']; 201 202 if (Zotero.isStandalone) { 203 var version = Services.appinfo.version; 204 } 205 else { 206 let deferred = Zotero.Promise.defer(); 207 Components.utils.import("resource://gre/modules/AddonManager.jsm"); 208 AddonManager.getAddonByID( 209 ZOTERO_CONFIG.GUID, 210 function (addon) { 211 deferred.resolve(addon.version); 212 } 213 ); 214 var version = yield deferred.promise; 215 } 216 Zotero.version = version; 217 218 // OS platform 219 var win = Components.classes["@mozilla.org/appshell/appShellService;1"] 220 .getService(Components.interfaces.nsIAppShellService) 221 .hiddenDOMWindow; 222 this.platform = win.navigator.platform; 223 this.isMac = (this.platform.substr(0, 3) == "Mac"); 224 this.isWin = (this.platform.substr(0, 3) == "Win"); 225 this.isLinux = (this.platform.substr(0, 5) == "Linux"); 226 this.oscpu = win.navigator.oscpu; 227 228 // Browser 229 Zotero.browser = "g"; 230 231 // 232 // Get settings from language pack (extracted by zotero-build/locale/merge_mozilla_files) 233 // 234 function getIntlProp(name, fallback = null) { 235 try { 236 return intlProps.GetStringFromName(name); 237 } 238 catch (e) { 239 Zotero.logError(`Couldn't load ${name} from intl.properties`); 240 return fallback; 241 } 242 } 243 function setOrClearIntlPref(name, type) { 244 var val = getIntlProp(name); 245 if (val !== null) { 246 if (type == 'boolean') { 247 val = val == 'true'; 248 } 249 Zotero.Prefs.set(name, val, 1); 250 } 251 else { 252 Zotero.Prefs.clear(name, 1); 253 } 254 } 255 var intlProps = Services.strings.createBundle("chrome://zotero/locale/mozilla/intl.properties"); 256 this.locale = getIntlProp('general.useragent.locale', 'en-US'); 257 let [get, numForms] = PluralForm.makeGetter(parseInt(getIntlProp('pluralRule', 1))); 258 this.pluralFormGet = get; 259 this.pluralFormNumForms = numForms; 260 setOrClearIntlPref('intl.accept_languages', 'string'); 261 262 // Also load the brand as appName 263 var brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); 264 this.appName = brandBundle.GetStringFromName("brandShortName"); 265 266 _localizedStringBundle = Services.strings.createBundle("chrome://zotero/locale/zotero.properties"); 267 268 // Set the locale direction to Zotero.dir 269 // DEBUG: is there a better way to get the entity from JS? 270 var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 271 .createInstance(); 272 xmlhttp.open('GET', 'chrome://global/locale/global.dtd', false); 273 xmlhttp.overrideMimeType('text/plain'); 274 xmlhttp.send(null); 275 var matches = xmlhttp.responseText.match(/(ltr|rtl)/); 276 if (matches && matches[0] == 'rtl') { 277 Zotero.dir = 'rtl'; 278 } 279 else { 280 Zotero.dir = 'ltr'; 281 } 282 Zotero.rtl = Zotero.dir == 'rtl'; 283 284 Zotero.Prefs.init(); 285 Zotero.Debug.init(options && options.forceDebugLog); 286 287 // Make sure that Zotero Standalone is not running as root 288 if(Zotero.isStandalone && !Zotero.isWin) _checkRoot(); 289 290 _addToolbarIcon(); 291 292 try { 293 yield Zotero.DataDirectory.init(); 294 if (this.restarting) { 295 return; 296 } 297 var dataDir = Zotero.DataDirectory.dir; 298 } 299 catch (e) { 300 // Zotero dir not found 301 if ((e instanceof OS.File.Error && e.becauseNoSuchFile) || e.name == 'NS_ERROR_FILE_NOT_FOUND') { 302 let foundInDefault = false; 303 try { 304 foundInDefault = (yield OS.File.exists(Zotero.DataDirectory.defaultDir)) 305 && (yield OS.File.exists( 306 OS.Path.join( 307 Zotero.DataDirectory.defaultDir, 308 Zotero.DataDirectory.getDatabaseFilename() 309 ) 310 )); 311 } 312 catch (e) { 313 Zotero.logError(e); 314 } 315 316 let previousDir = Zotero.Prefs.get('lastDataDir') 317 || Zotero.Prefs.get('dataDir') 318 || e.dataDir; 319 Zotero.startupError = foundInDefault 320 ? Zotero.getString( 321 'dataDir.notFound.defaultFound', 322 [ 323 Zotero.clientName, 324 previousDir, 325 Zotero.DataDirectory.defaultDir 326 ] 327 ) 328 : Zotero.getString('dataDir.notFound', Zotero.clientName); 329 _startupErrorHandler = function() { 330 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. 331 createInstance(Components.interfaces.nsIPromptService); 332 var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING 333 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING 334 + ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING; 335 // TEMP: lastDataDir can be removed once old persistent descriptors have been 336 // converted, which they are in getZoteroDirectory() in 5.0 337 if (foundInDefault) { 338 let index = ps.confirmEx(null, 339 Zotero.getString('general.error'), 340 Zotero.startupError, 341 buttonFlags, 342 Zotero.getString('dataDir.useNewLocation'), 343 Zotero.getString('general.quit'), 344 Zotero.getString('general.locate'), 345 null, {} 346 ); 347 // Revert to home directory 348 if (index == 0) { 349 Zotero.DataDirectory.set(Zotero.DataDirectory.defaultDir); 350 Zotero.Utilities.Internal.quit(true); 351 return; 352 } 353 // Locate data directory 354 else if (index == 2) { 355 Zotero.DataDirectory.choose(true); 356 } 357 358 } 359 else { 360 let index = ps.confirmEx(null, 361 Zotero.getString('general.error'), 362 Zotero.startupError 363 + (previousDir 364 ? '\n\n' + Zotero.getString('dataDir.previousDir') + ' ' + previousDir 365 : ''), 366 buttonFlags, 367 Zotero.getString('general.quit'), 368 Zotero.getString('dataDir.useDefaultLocation'), 369 Zotero.getString('general.locate'), 370 null, {} 371 ); 372 // Revert to home directory 373 if (index == 1) { 374 Zotero.DataDirectory.set(Zotero.DataDirectory.defaultDir); 375 Zotero.Utilities.Internal.quit(true); 376 return; 377 } 378 // Locate data directory 379 else if (index == 2) { 380 Zotero.DataDirectory.choose(true); 381 } 382 } 383 } 384 return; 385 } 386 // DEBUG: handle more startup errors 387 else { 388 throw e; 389 } 390 } 391 392 if (!Zotero.isConnector) { 393 if (!this.forceDataDir) { 394 yield Zotero.DataDirectory.checkForMigration( 395 dataDir, Zotero.DataDirectory.defaultDir 396 ); 397 if (this.skipLoading) { 398 return; 399 } 400 401 yield Zotero.DataDirectory.checkForLostLegacy(); 402 if (this.restarting) { 403 return; 404 } 405 } 406 407 // Make sure data directory isn't in Dropbox, etc. 408 if (Zotero.isStandalone) { 409 yield Zotero.DataDirectory.checkForUnsafeLocation(dataDir); 410 } 411 } 412 413 // Register shutdown handler to call Zotero.shutdown() 414 var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }}; 415 Services.obs.addObserver(_shutdownObserver, "quit-application", false); 416 417 try { 418 Zotero.IPC.init(); 419 } 420 catch (e) { 421 if (_checkDataDirAccessError(e)) { 422 return false; 423 } 424 throw (e); 425 } 426 427 // Get startup errors 428 try { 429 var messages = {}; 430 Services.console.getMessageArray(messages, {}); 431 _startupErrors = Object.keys(messages.value).map(i => messages[i]) 432 .filter(msg => _shouldKeepError(msg)); 433 } catch(e) { 434 Zotero.logError(e); 435 } 436 // Register error observer 437 Services.console.registerListener(ConsoleListener); 438 439 // Add shutdown listener to remove quit-application observer and console listener 440 this.addShutdownListener(function() { 441 Services.obs.removeObserver(_shutdownObserver, "quit-application", false); 442 Services.console.unregisterListener(ConsoleListener); 443 }); 444 445 // Load additional info for connector or not 446 if(Zotero.isConnector) { 447 Zotero.debug("Loading in connector mode"); 448 Zotero.Connector_Types.init(); 449 450 // Store a startupError until we get information from Zotero Standalone 451 Zotero.startupError = Zotero.getString("connector.loadInProgress") 452 453 if(!Zotero.isFirstLoadThisSession) { 454 // We want to get a checkInitComplete message before initializing if we switched to 455 // connector mode because Standalone was launched 456 Zotero.IPC.broadcast("checkInitComplete"); 457 } else { 458 Zotero.initComplete(); 459 } 460 } else { 461 Zotero.debug("Loading in full mode"); 462 return _initFull() 463 .then(function (success) { 464 if (!success) { 465 return false; 466 } 467 468 if(Zotero.isStandalone) Zotero.Standalone.init(); 469 Zotero.initComplete(); 470 }) 471 } 472 473 return true; 474 }); 475 476 /** 477 * Triggers events when initialization finishes 478 */ 479 this.initComplete = function() { 480 if(Zotero.initialized) return; 481 482 Zotero.debug("Running initialization callbacks"); 483 delete this.startupError; 484 this.initialized = true; 485 this.initializationDeferred.resolve(); 486 487 if(Zotero.isConnector) { 488 Zotero.Repo.init(); 489 Zotero.locked = false; 490 } 491 492 if(!Zotero.isFirstLoadThisSession) { 493 // trigger zotero-reloaded event 494 Zotero.debug('Triggering "zotero-reloaded" event'); 495 Services.obs.notifyObservers(Zotero, "zotero-reloaded", null); 496 } 497 498 Zotero.debug('Triggering "zotero-loaded" event'); 499 Services.obs.notifyObservers(Zotero, "zotero-loaded", null); 500 } 501 502 503 this.uiIsReady = function () { 504 if (this.uiReadyPromise.isPending()) { 505 Zotero.debug("User interface ready in " + (new Date() - _startupTime) + " ms"); 506 this.uiReadyDeferred.resolve(); 507 } 508 }; 509 510 511 var _addToolbarIcon = function () { 512 if (Zotero.isStandalone) return; 513 514 // Add toolbar icon 515 try { 516 Services.scriptloader.loadSubScript("chrome://zotero/content/icon.js", {}, "UTF-8"); 517 } 518 catch (e) { 519 if (Zotero) { 520 Zotero.debug(e, 1); 521 } 522 Components.utils.reportError(e); 523 } 524 }; 525 526 527 /** 528 * Initialization function to be called only if Zotero is in full mode 529 * 530 * @return {Promise:Boolean} 531 */ 532 var _initFull = Zotero.Promise.coroutine(function* () { 533 if (!(yield _initDB())) return false; 534 535 Zotero.VersionHeader.init(); 536 537 // Check for data reset/restore 538 var dataDir = Zotero.DataDirectory.dir; 539 var restoreFile = OS.Path.join(dataDir, 'restore-from-server'); 540 var resetDataDirFile = OS.Path.join(dataDir, 'reset-data-directory'); 541 542 var result = yield Zotero.Promise.all([OS.File.exists(restoreFile), OS.File.exists(resetDataDirFile)]); 543 if (result.some(r => r)) { 544 [Zotero.restoreFromServer, Zotero.resetDataDir] = result; 545 try { 546 yield Zotero.DB.closeDatabase(); 547 548 // TODO: better error handling 549 550 // TODO: prompt for location 551 // TODO: Back up database 552 // TODO: Reset translators and styles 553 554 555 556 if (Zotero.restoreFromServer) { 557 let dbfile = Zotero.DataDirectory.getDatabase(); 558 Zotero.debug("Deleting " + dbfile); 559 yield OS.File.remove(dbfile, { ignoreAbsent: true }); 560 let storageDir = OS.Path.join(dataDir, 'storage'); 561 Zotero.debug("Deleting " + storageDir.path); 562 OS.File.removeDir(storageDir, { ignoreAbsent: true }), 563 yield OS.File.remove(restoreFile); 564 Zotero.restoreFromServer = true; 565 } 566 else if (Zotero.resetDataDir) { 567 Zotero.initAutoSync = true; 568 569 // Clear some user prefs 570 [ 571 'sync.server.username', 572 'sync.storage.username' 573 ].forEach(p => Zotero.Prefs.clear(p)); 574 575 // Clear data directory 576 Zotero.debug("Deleting data directory files"); 577 let lastError; 578 // Delete all files in directory rather than removing directory, in case it's 579 // a symlink 580 yield Zotero.File.iterateDirectory(dataDir, function* (iterator) { 581 while (true) { 582 let entry = yield iterator.next(); 583 // Don't delete some files 584 if (entry.name == 'pipes') { 585 continue; 586 } 587 Zotero.debug("Deleting " + entry.path); 588 try { 589 if (entry.isDir) { 590 yield OS.File.removeDir(entry.path); 591 } 592 else { 593 yield OS.File.remove(entry.path); 594 } 595 } 596 // Keep trying to delete as much as we can 597 catch (e) { 598 lastError = e; 599 Zotero.logError(e); 600 } 601 } 602 }); 603 if (lastError) { 604 throw lastError; 605 } 606 } 607 Zotero.debug("Done with reset"); 608 609 if (!(yield _initDB())) return false; 610 } 611 catch (e) { 612 // Restore from backup? 613 alert(e); 614 return false; 615 } 616 } 617 618 Zotero.HTTP.triggerProxyAuth(); 619 620 // Add notifier queue callbacks to the DB layer 621 Zotero.DB.addCallback('begin', id => Zotero.Notifier.begin(id)); 622 Zotero.DB.addCallback('commit', id => Zotero.Notifier.commit(null, id)); 623 Zotero.DB.addCallback('rollback', id => Zotero.Notifier.reset(id)); 624 625 try { 626 // Require >=2.1b3 database to ensure proper locking 627 if (Zotero.isStandalone) { 628 let dbSystemVersion = yield Zotero.Schema.getDBVersion('system'); 629 if (dbSystemVersion > 0 && dbSystemVersion < 31) { 630 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 631 .createInstance(Components.interfaces.nsIPromptService); 632 var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) 633 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING) 634 + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING) 635 + ps.BUTTON_POS_2_DEFAULT; 636 var index = ps.confirmEx( 637 null, 638 Zotero.getString('dataDir.incompatibleDbVersion.title'), 639 Zotero.getString('dataDir.incompatibleDbVersion.text'), 640 buttonFlags, 641 Zotero.getString('general.useDefault'), 642 Zotero.getString('dataDir.chooseNewDataDirectory'), 643 Zotero.getString('general.quit'), 644 null, 645 {} 646 ); 647 648 var quit = false; 649 650 // Default location 651 if (index == 0) { 652 Zotero.Prefs.set("useDataDir", false) 653 654 Services.startup.quit( 655 Components.interfaces.nsIAppStartup.eAttemptQuit 656 | Components.interfaces.nsIAppStartup.eRestart 657 ); 658 } 659 // Select new data directory 660 else if (index == 1) { 661 let dir = yield Zotero.DataDirectory.choose(true); 662 if (!dir) { 663 quit = true; 664 } 665 } 666 else { 667 quit = true; 668 } 669 670 if (quit) { 671 Services.startup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit); 672 } 673 674 throw true; 675 } 676 } 677 678 try { 679 var updated = yield Zotero.Schema.updateSchema({ 680 onBeforeUpdate: (options = {}) => { 681 if (options.minor) return; 682 try { 683 Zotero.showZoteroPaneProgressMeter( 684 Zotero.getString('upgrade.status') 685 ) 686 } 687 catch (e) { 688 Zotero.logError(e); 689 } 690 } 691 }); 692 } 693 catch (e) { 694 Zotero.logError(e); 695 696 if (e instanceof Zotero.DB.IncompatibleVersionException) { 697 let kbURL = "https://www.zotero.org/support/kb/newer_db_version"; 698 let msg = (e.dbClientVersion 699 ? Zotero.getString('startupError.incompatibleDBVersion', 700 [Zotero.clientName, e.dbClientVersion]) 701 : Zotero.getString('startupError.zoteroVersionIsOlder')) + "\n\n" 702 + Zotero.getString('startupError.zoteroVersionIsOlder.current', Zotero.version) 703 + "\n\n" 704 + Zotero.getString('startupError.zoteroVersionIsOlder.upgrade', 705 ZOTERO_CONFIG.DOMAIN_NAME); 706 Zotero.startupError = msg; 707 _startupErrorHandler = function() { 708 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 709 .getService(Components.interfaces.nsIPromptService); 710 var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) 711 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) 712 + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING) 713 + ps.BUTTON_POS_0_DEFAULT; 714 715 var index = ps.confirmEx( 716 null, 717 Zotero.getString('general.error'), 718 Zotero.startupError, 719 buttonFlags, 720 Zotero.getString('general.checkForUpdate'), 721 null, 722 Zotero.getString('general.moreInformation'), 723 null, 724 {} 725 ); 726 727 // "Check for Update" button 728 if(index === 0) { 729 if(Zotero.isStandalone) { 730 Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 731 .getService(Components.interfaces.nsIWindowWatcher) 732 .openWindow(null, 'chrome://mozapps/content/update/updates.xul', 733 'updateChecker', 'chrome,centerscreen,modal', null); 734 } else { 735 // In Firefox, show the add-on manager 736 Components.utils.import("resource://gre/modules/AddonManager.jsm"); 737 AddonManager.getAddonByID(ZOTERO_CONFIG['GUID'], 738 function (addon) { 739 // Disable auto-update so that the user is presented with the option 740 var initUpdateState = addon.applyBackgroundUpdates; 741 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; 742 addon.findUpdates({ 743 onNoUpdateAvailable: function() { 744 ps.alert( 745 null, 746 Zotero.getString('general.noUpdatesFound'), 747 Zotero.getString('general.isUpToDate', 'Zotero') 748 ); 749 }, 750 onUpdateAvailable: function() { 751 // Show available update 752 Components.classes["@mozilla.org/appshell/window-mediator;1"] 753 .getService(Components.interfaces.nsIWindowMediator) 754 .getMostRecentWindow('navigator:browser') 755 .BrowserOpenAddonsMgr('addons://updates/available'); 756 }, 757 onUpdateFinished: function() { 758 // Restore add-on auto-update state, but don't fire 759 // too quickly or the update will not show in the 760 // add-on manager 761 setTimeout(function() { 762 addon.applyBackgroundUpdates = initUpdateState; 763 }, 1000); 764 } 765 }, 766 AddonManager.UPDATE_WHEN_USER_REQUESTED 767 ); 768 } 769 ); 770 } 771 } 772 // Load More Info page 773 else if (index == 2) { 774 let io = Components.classes['@mozilla.org/network/io-service;1'] 775 .getService(Components.interfaces.nsIIOService); 776 let uri = io.newURI(kbURL, null, null); 777 let handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1'] 778 .getService(Components.interfaces.nsIExternalProtocolService) 779 .getProtocolHandlerInfo('http'); 780 handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault; 781 handler.launchWithURI(uri, null); 782 } 783 }; 784 throw e; 785 } 786 787 let stack = e.stack ? Zotero.Utilities.Internal.filterStack(e.stack) : null; 788 Zotero.startupError = Zotero.getString('startupError.databaseUpgradeError') 789 + "\n\n" 790 + (stack || e); 791 throw e; 792 } 793 794 yield Zotero.Users.init(); 795 yield Zotero.Libraries.init(); 796 797 yield Zotero.ItemTypes.init(); 798 yield Zotero.ItemFields.init(); 799 yield Zotero.CreatorTypes.init(); 800 yield Zotero.FileTypes.init(); 801 yield Zotero.CharacterSets.init(); 802 yield Zotero.RelationPredicates.init(); 803 804 Zotero.locked = false; 805 806 // Initialize various services 807 Zotero.Integration.init(); 808 809 if(Zotero.Prefs.get("httpServer.enabled")) { 810 Zotero.Server.init(); 811 } 812 813 yield Zotero.Fulltext.init(); 814 815 Zotero.Notifier.registerObserver(Zotero.Tags, 'setting', 'tags'); 816 817 yield Zotero.Sync.Data.Local.init(); 818 yield Zotero.Sync.Data.Utilities.init(); 819 Zotero.Sync.Runner = new Zotero.Sync.Runner_Module; 820 Zotero.Sync.EventListeners.init(); 821 Zotero.Streamer = new Zotero.Streamer_Module; 822 Zotero.Streamer.init(); 823 824 Zotero.MIMETypeHandler.init(); 825 yield Zotero.Proxies.init(); 826 827 // Initialize keyboard shortcuts 828 Zotero.Keys.init(); 829 830 yield Zotero.Date.init(); 831 Zotero.LocateManager.init(); 832 yield Zotero.ID.init(); 833 yield Zotero.Collections.init(); 834 yield Zotero.Items.init(); 835 yield Zotero.Searches.init(); 836 yield Zotero.Tags.init(); 837 yield Zotero.Creators.init(); 838 yield Zotero.Groups.init(); 839 yield Zotero.Relations.init(); 840 841 // Load all library data except for items, which are loaded when libraries are first 842 // clicked on or if otherwise necessary 843 yield Zotero.Promise.each( 844 Zotero.Libraries.getAll(), 845 library => Zotero.Promise.coroutine(function* () { 846 yield Zotero.SyncedSettings.loadAll(library.libraryID); 847 if (library.libraryType != 'feed') { 848 yield Zotero.Collections.loadAll(library.libraryID); 849 yield Zotero.Searches.loadAll(library.libraryID); 850 } 851 })() 852 ); 853 854 855 Zotero.Items.startEmptyTrashTimer(); 856 857 yield Zotero.QuickCopy.init(); 858 Zotero.addShutdownListener(() => Zotero.QuickCopy.uninit()); 859 860 Zotero.Feeds.init(); 861 Zotero.addShutdownListener(() => Zotero.Feeds.uninit()); 862 863 Zotero.Schema.schemaUpdatePromise.then(Zotero.purgeDataObjects.bind(Zotero)); 864 865 return true; 866 } 867 catch (e) { 868 Zotero.logError(e); 869 if (!Zotero.startupError) { 870 Zotero.startupError = Zotero.getString('startupError') + "\n\n" + (e.stack || e); 871 } 872 return false; 873 } 874 }); 875 876 /** 877 * Initializes the DB connection 878 */ 879 var _initDB = Zotero.Promise.coroutine(function* (haveReleasedLock) { 880 // Initialize main database connection 881 Zotero.DB = new Zotero.DBConnection('zotero'); 882 883 try { 884 // Test read access 885 yield Zotero.DB.test(); 886 887 let dbfile = Zotero.DataDirectory.getDatabase(); 888 889 // Tell any other Zotero instances to release their lock, 890 // in case we lost the lock on the database (how?) and it's 891 // now open in two places at once 892 Zotero.IPC.broadcast("releaseLock " + dbfile); 893 894 // Test write access on Zotero data directory 895 if (!Zotero.File.pathToFile(OS.Path.dirname(dbfile)).isWritable()) { 896 var msg = 'Cannot write to ' + OS.Path.dirname(dbfile) + '/'; 897 } 898 // Test write access on Zotero database 899 else if (!Zotero.File.pathToFile(dbfile).isWritable()) { 900 var msg = 'Cannot write to ' + dbfile; 901 } 902 else { 903 var msg = false; 904 } 905 906 if (msg) { 907 var e = { 908 name: 'NS_ERROR_FILE_ACCESS_DENIED', 909 message: msg, 910 toString: function () { return this.message; } 911 }; 912 throw (e); 913 } 914 } 915 catch (e) { 916 if (_checkDataDirAccessError(e)) {} 917 // Storage busy 918 else if (e.message.includes('2153971713')) { 919 Zotero.startupError = Zotero.getString('startupError.databaseInUse') + "\n\n" 920 + Zotero.getString( 921 "startupError.close" + (Zotero.isStandalone ? 'Firefox' : 'Standalone') 922 ); 923 } 924 else { 925 let stack = e.stack ? Zotero.Utilities.Internal.filterStack(e.stack) : null; 926 Zotero.startupError = Zotero.getString('startupError') + "\n\n" + (stack || e); 927 } 928 929 Zotero.debug(e.toString(), 1); 930 Components.utils.reportError(e); // DEBUG: doesn't always work 931 Zotero.skipLoading = true; 932 return false; 933 } 934 935 return true; 936 }); 937 938 939 function _checkDataDirAccessError(e) { 940 if (e.name != 'NS_ERROR_FILE_ACCESS_DENIED' && !e.message.includes('2152857621')) { 941 return false; 942 } 943 944 var msg = Zotero.getString('dataDir.databaseCannotBeOpened', Zotero.clientName) 945 + "\n\n" 946 + Zotero.getString('dataDir.checkPermissions', Zotero.clientName); 947 // If already using default directory, just show it 948 if (Zotero.DataDirectory.dir == Zotero.DataDirectory.defaultDir) { 949 msg += "\n\n" + Zotero.getString('dataDir.location', Zotero.DataDirectory.dir); 950 } 951 // Otherwise suggest moving to default, since there's a good chance this is due to security 952 // software preventing Zotero from accessing the selected directory (particularly if it's 953 // a Firefox profile) 954 else { 955 msg += "\n\n" 956 + Zotero.getString('dataDir.moveToDefaultLocation', Zotero.clientName) 957 + "\n\n" 958 + Zotero.getString( 959 'dataDir.migration.failure.full.current', Zotero.DataDirectory.dir 960 ) 961 + "\n" 962 + Zotero.getString( 963 'dataDir.migration.failure.full.recommended', Zotero.DataDirectory.defaultDir 964 ); 965 } 966 Zotero.startupError = msg; 967 return true; 968 } 969 970 971 /** 972 * Called when the DB has been released by another Zotero process to perform necessary 973 * initialization steps 974 */ 975 this.onDBLockReleased = function() { 976 if(Zotero.isConnector) { 977 // if DB lock is released, switch out of connector mode 978 switchConnectorMode(false); 979 } else if(_waitingForDBLock) { 980 // if waiting for DB lock and we get it, continue init 981 _waitingForDBLock = false; 982 } 983 } 984 985 this.shutdown = Zotero.Promise.coroutine(function* () { 986 Zotero.debug("Shutting down Zotero"); 987 988 try { 989 // set closing to true 990 Zotero.closing = true; 991 992 // run shutdown listener 993 for (let listener of _shutdownListeners) { 994 try { 995 listener(); 996 } catch(e) { 997 Zotero.logError(e); 998 } 999 } 1000 1001 // remove temp directory 1002 Zotero.removeTempDirectory(); 1003 1004 if (Zotero.DB) { 1005 // close DB 1006 yield Zotero.DB.closeDatabase(true) 1007 1008 if (!Zotero.restarting) { 1009 // broadcast that DB lock has been released 1010 Zotero.IPC.broadcast("lockReleased"); 1011 } 1012 } 1013 } catch(e) { 1014 Zotero.logError(e); 1015 throw e; 1016 } 1017 }); 1018 1019 1020 this.getProfileDirectory = function () { 1021 Zotero.warn("Zotero.getProfileDirectory() is deprecated -- use Zotero.Profile.dir"); 1022 return Zotero.File.pathToFile(Zotero.Profile.dir); 1023 } 1024 1025 1026 this.getZoteroDirectory = function () { 1027 Zotero.warn("Zotero.getZoteroDirectory() is deprecated -- use Zotero.DataDirectory.dir"); 1028 return Zotero.File.pathToFile(Zotero.DataDirectory.dir); 1029 } 1030 1031 1032 function getStorageDirectory(){ 1033 var file = OS.Path.join(Zotero.DataDirectory.dir, 'storage'); 1034 file = Zotero.File.pathToFile(file); 1035 Zotero.File.createDirectoryIfMissing(file); 1036 return file; 1037 } 1038 1039 1040 this.getZoteroDatabase = function (name, ext) { 1041 Zotero.warn("Zotero.getZoteroDatabase() is deprecated -- use Zotero.DataDirectory.getDatabase()"); 1042 return Zotero.File.pathToFile(Zotero.DataDirectory.getDatabase(name, ext)); 1043 } 1044 1045 1046 /** 1047 * @return {nsIFile} 1048 */ 1049 this.getTempDirectory = function () { 1050 var tmp = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 1051 tmp.append('tmp'); 1052 Zotero.File.createDirectoryIfMissing(tmp); 1053 return tmp; 1054 } 1055 1056 1057 this.removeTempDirectory = function () { 1058 var tmp = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 1059 tmp.append('tmp'); 1060 if (tmp.exists()) { 1061 try { 1062 tmp.remove(true); 1063 } 1064 catch (e) {} 1065 } 1066 } 1067 1068 1069 this.getStylesDirectory = function () { 1070 var dir = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 1071 dir.append('styles'); 1072 Zotero.File.createDirectoryIfMissing(dir); 1073 return dir; 1074 } 1075 1076 1077 this.getTranslatorsDirectory = function () { 1078 var dir = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 1079 dir.append('translators'); 1080 Zotero.File.createDirectoryIfMissing(dir); 1081 return dir; 1082 } 1083 1084 1085 this.openMainWindow = function () { 1086 var prefService = Components.classes["@mozilla.org/preferences-service;1"] 1087 .getService(Components.interfaces.nsIPrefBranch); 1088 var chromeURI = prefService.getCharPref('toolkit.defaultChromeURI'); 1089 var flags = prefService.getCharPref("toolkit.defaultChromeFeatures", "chrome,dialog=no,all"); 1090 var ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] 1091 .getService(Components.interfaces.nsIWindowWatcher); 1092 return ww.openWindow(null, chromeURI, '_blank', flags, null); 1093 } 1094 1095 1096 /** 1097 * Launch a file, the best way we can 1098 */ 1099 this.launchFile = function (file) { 1100 file = Zotero.File.pathToFile(file); 1101 try { 1102 Zotero.debug("Launching " + file.path); 1103 file.launch(); 1104 } 1105 catch (e) { 1106 Zotero.debug(e, 2); 1107 Zotero.debug("launch() not supported -- trying fallback executable", 2); 1108 1109 try { 1110 if (Zotero.isWin) { 1111 var pref = "fallbackLauncher.windows"; 1112 } 1113 else { 1114 var pref = "fallbackLauncher.unix"; 1115 } 1116 let launcher = Zotero.Prefs.get(pref); 1117 this.launchFileWithApplication(file.path, launcher); 1118 } 1119 catch (e) { 1120 Zotero.debug(e); 1121 Zotero.debug("Launching via executable failed -- passing to loadUrl()"); 1122 1123 // If nsILocalFile.launch() isn't available and the fallback 1124 // executable doesn't exist, we just let the Firefox external 1125 // helper app window handle it 1126 var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"] 1127 .getService(Components.interfaces.nsIFileProtocolHandler); 1128 var uri = nsIFPH.newFileURI(file); 1129 1130 var nsIEPS = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]. 1131 getService(Components.interfaces.nsIExternalProtocolService); 1132 nsIEPS.loadUrl(uri); 1133 } 1134 } 1135 }; 1136 1137 1138 /** 1139 * Launch a file with the given application 1140 */ 1141 this.launchFileWithApplication = function (filePath, applicationPath) { 1142 var exec = Zotero.File.pathToFile(applicationPath); 1143 if (!exec.exists()) { 1144 throw new Error("'" + applicationPath + "' does not exist"); 1145 } 1146 1147 var args; 1148 // On macOS, if we only have an .app, launch it using 'open' 1149 if (Zotero.isMac && applicationPath.endsWith('.app')) { 1150 args = [filePath, '-a', applicationPath]; 1151 applicationPath = '/usr/bin/open'; 1152 } 1153 else { 1154 args = [filePath]; 1155 } 1156 1157 // Async, but we don't want to block 1158 Zotero.Utilities.Internal.exec(applicationPath, args); 1159 }; 1160 1161 1162 /** 1163 * Launch an HTTP URL externally, the best way we can 1164 * 1165 * Used only by Standalone 1166 */ 1167 this.launchURL = function (url) { 1168 if (!url.match(/^https?/)) { 1169 throw new Error("launchURL() requires an HTTP(S) URL"); 1170 } 1171 1172 try { 1173 var io = Components.classes['@mozilla.org/network/io-service;1'] 1174 .getService(Components.interfaces.nsIIOService); 1175 var uri = io.newURI(url, null, null); 1176 var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1'] 1177 .getService(Components.interfaces.nsIExternalProtocolService) 1178 .getProtocolHandlerInfo('http'); 1179 handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault; 1180 handler.launchWithURI(uri, null); 1181 } 1182 catch (e) { 1183 Zotero.debug("launchWithURI() not supported -- trying fallback executable"); 1184 1185 if (Zotero.isWin) { 1186 var pref = "fallbackLauncher.windows"; 1187 } 1188 else { 1189 var pref = "fallbackLauncher.unix"; 1190 } 1191 var path = Zotero.Prefs.get(pref); 1192 1193 var exec = Components.classes["@mozilla.org/file/local;1"] 1194 .createInstance(Components.interfaces.nsILocalFile); 1195 exec.initWithPath(path); 1196 if (!exec.exists()) { 1197 throw ("Fallback executable not found -- check extensions.zotero." + pref + " in about:config"); 1198 } 1199 1200 var proc = Components.classes["@mozilla.org/process/util;1"] 1201 .createInstance(Components.interfaces.nsIProcess); 1202 proc.init(exec); 1203 1204 var args = [url]; 1205 proc.runw(false, args, args.length); 1206 } 1207 } 1208 1209 1210 /** 1211 * Opens a URL in the basic viewer, and optionally run a callback on load 1212 * 1213 * @param {String} uri 1214 * @param {Function} [onLoad] - Function to run once URI is loaded; passed the loaded document 1215 */ 1216 this.openInViewer = function (uri, onLoad) { 1217 var wm = Services.wm; 1218 var win = wm.getMostRecentWindow("zotero:basicViewer"); 1219 if (win) { 1220 win.loadURI(uri); 1221 } else { 1222 let ww = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] 1223 .getService(Components.interfaces.nsIWindowWatcher); 1224 let arg = Components.classes["@mozilla.org/supports-string;1"] 1225 .createInstance(Components.interfaces.nsISupportsString); 1226 arg.data = uri; 1227 win = ww.openWindow(null, "chrome://zotero/content/standalone/basicViewer.xul", 1228 "basicViewer", "chrome,dialog=yes,resizable,centerscreen,menubar,scrollbars", arg); 1229 } 1230 if (onLoad) { 1231 let browser 1232 let func = function () { 1233 win.removeEventListener("load", func); 1234 browser = win.document.documentElement.getElementsByTagName('browser')[0]; 1235 browser.addEventListener("pageshow", innerFunc); 1236 }; 1237 let innerFunc = function () { 1238 browser.removeEventListener("pageshow", innerFunc); 1239 onLoad(browser.contentDocument); 1240 }; 1241 win.addEventListener("load", func); 1242 } 1243 }; 1244 1245 1246 /* 1247 * Debug logging function 1248 * 1249 * Uses prefs e.z.debug.log and e.z.debug.level (restart required) 1250 * 1251 * @param {} message 1252 * @param {Integer} [level=3] 1253 * @param {Integer} [maxDepth] 1254 * @param {Boolean|Integer} [stack] Whether to display the calling stack. 1255 * If true, stack is displayed starting from the caller. If an integer, 1256 * that many stack levels will be omitted starting from the caller. 1257 */ 1258 function debug(message, level, maxDepth, stack) { 1259 // Account for this alias 1260 if (stack === true) { 1261 stack = 1; 1262 } else if (stack >= 0) { 1263 stack++; 1264 } 1265 1266 Zotero.Debug.log(message, level, maxDepth, stack); 1267 } 1268 1269 1270 /* 1271 * Log a message to the Mozilla JS error console 1272 * 1273 * |type| is a string with one of the flag types in nsIScriptError: 1274 * 'error', 'warning', 'exception', 'strict' 1275 */ 1276 function log(message, type, sourceName, sourceLine, lineNumber, columnNumber) { 1277 var scriptError = Components.classes["@mozilla.org/scripterror;1"] 1278 .createInstance(Components.interfaces.nsIScriptError); 1279 1280 if (!type) { 1281 type = 'warning'; 1282 } 1283 var flags = scriptError[type + 'Flag']; 1284 1285 scriptError.init( 1286 message, 1287 sourceName ? sourceName : null, 1288 sourceLine != undefined ? sourceLine : null, 1289 lineNumber != undefined ? lineNumber : null, 1290 columnNumber != undefined ? columnNumber : null, 1291 flags, 1292 'component javascript' 1293 ); 1294 Services.console.logMessage(scriptError); 1295 } 1296 1297 /** 1298 * Log a JS error to the Mozilla error console and debug output 1299 * @param {Exception} err 1300 */ 1301 function logError(err) { 1302 Zotero.debug(err, 1); 1303 log(err.message ? err.message : err.toString(), "error", 1304 err.fileName ? err.fileName : (err.filename ? err.filename : null), null, 1305 err.lineNumber ? err.lineNumber : null, null); 1306 } 1307 1308 1309 this.warn = function (err) { 1310 Zotero.debug(err, 2); 1311 log(err.message ? err.message : err.toString(), "warning", 1312 err.fileName ? err.fileName : (err.filename ? err.filename : null), null, 1313 err.lineNumber ? err.lineNumber : null, null); 1314 } 1315 1316 1317 /** 1318 * Display an alert in a given window 1319 * 1320 * @param {Window} 1321 * @param {String} title 1322 * @param {String} msg 1323 */ 1324 this.alert = function (window, title, msg) { 1325 this.debug(`Alert:\n\n${msg}`); 1326 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 1327 .getService(Components.interfaces.nsIPromptService); 1328 ps.alert(window, title, msg); 1329 } 1330 1331 1332 this.getErrors = function (asStrings) { 1333 var errors = []; 1334 1335 for (let msg of _startupErrors.concat(_recentErrors)) { 1336 let altMessage; 1337 // Remove password in malformed XML errors 1338 if (msg.category == 'malformed-xml') { 1339 try { 1340 // msg.message is read-only, so store separately 1341 altMessage = msg.message.replace(/(https?:\/\/[^:]+:)([^@]+)(@[^"]+)/, "$1****$3"); 1342 } 1343 catch (e) {} 1344 } 1345 1346 if (asStrings) { 1347 errors.push(altMessage || msg.message) 1348 } 1349 else { 1350 errors.push(msg); 1351 } 1352 } 1353 return errors; 1354 } 1355 1356 1357 /** 1358 * Get versions, platform, etc. 1359 */ 1360 this.getSystemInfo = Zotero.Promise.coroutine(function* () { 1361 var info = { 1362 version: Zotero.version, 1363 platform: Zotero.platform, 1364 oscpu: Zotero.oscpu, 1365 locale: Zotero.locale, 1366 appName: Services.appinfo.name, 1367 appVersion: Services.appinfo.version 1368 }; 1369 1370 var extensions = yield Zotero.getInstalledExtensions(); 1371 info.extensions = extensions.join(', '); 1372 1373 var str = ''; 1374 for (var key in info) { 1375 str += key + ' => ' + info[key] + ', '; 1376 } 1377 str = str.substr(0, str.length - 2); 1378 return str; 1379 }); 1380 1381 1382 /** 1383 * @return {Promise<String[]>} - Promise for an array of extension names and versions 1384 */ 1385 this.getInstalledExtensions = Zotero.Promise.method(function () { 1386 var deferred = Zotero.Promise.defer(); 1387 function onHaveInstalledAddons(installed) { 1388 installed.sort(function(a, b) { 1389 return ((a.appDisabled || a.userDisabled) ? 1 : 0) - 1390 ((b.appDisabled || b.userDisabled) ? 1 : 0); 1391 }); 1392 var addons = []; 1393 for (let addon of installed) { 1394 switch (addon.id) { 1395 case "zotero@chnm.gmu.edu": 1396 case "{972ce4c6-7e08-4474-a285-3208198ce6fd}": // Default theme 1397 continue; 1398 } 1399 1400 addons.push(addon.name + " (" + addon.version 1401 + (addon.type != 2 ? ", " + addon.type : "") 1402 + ((addon.appDisabled || addon.userDisabled) ? ", disabled" : "") 1403 + ")"); 1404 } 1405 deferred.resolve(addons); 1406 } 1407 1408 Components.utils.import("resource://gre/modules/AddonManager.jsm"); 1409 AddonManager.getAllAddons(onHaveInstalledAddons); 1410 return deferred.promise; 1411 }); 1412 1413 /** 1414 * @param {String} name 1415 * @param {String[]} [params=[]] - Strings to substitute for placeholders 1416 * @param {Number} [num] - Number (also appearing in `params`) to use when determining which plural 1417 * form of the string to use; localized strings should include all forms in the order specified 1418 * in https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals, 1419 * separated by semicolons 1420 */ 1421 this.getString = function (name, params, num) { 1422 return this.getStringFromBundle(_localizedStringBundle, ...arguments); 1423 } 1424 1425 1426 this.getStringFromBundle = function (bundle, name, params, num) { 1427 try { 1428 if (params != undefined) { 1429 if (typeof params != 'object'){ 1430 params = [params]; 1431 } 1432 var l10n = bundle.formatStringFromName(name, params, params.length); 1433 } 1434 else { 1435 var l10n = bundle.GetStringFromName(name); 1436 } 1437 if (num !== undefined) { 1438 let availableForms = l10n.split(/;/); 1439 // If not enough available forms, use last one -- PluralForm.get() uses first by 1440 // default, but it's more likely that a localizer will translate the two English 1441 // strings with some plural form as the second one, so we might as well use that 1442 if (availableForms.length < this.pluralFormNumForms()) { 1443 l10n = availableForms[availableForms.length - 1]; 1444 } 1445 else { 1446 l10n = this.pluralFormGet(num, l10n); 1447 } 1448 } 1449 } 1450 catch (e){ 1451 if (e.name == 'NS_ERROR_ILLEGAL_VALUE') { 1452 Zotero.debug(params, 1); 1453 } 1454 else if (e.name != 'NS_ERROR_FAILURE') { 1455 Zotero.logError(e); 1456 } 1457 throw new Error('Localized string not available for ' + name); 1458 } 1459 return l10n; 1460 } 1461 1462 1463 /** 1464 * Defines property on the object 1465 * More compact way to do Object.defineProperty 1466 * 1467 * @param {Object} obj Target object 1468 * @param {String} prop Property to be defined 1469 * @param {Object} desc Propery descriptor. If not overriden, "enumerable" is true 1470 * @param {Object} opts Options: 1471 * lazy {Boolean} If true, the _getter_ is intended for late 1472 * initialization of the property. The getter is replaced with a simple 1473 * property once initialized. 1474 */ 1475 this.defineProperty = function(obj, prop, desc, opts) { 1476 if (typeof prop != 'string') throw new Error("Property must be a string"); 1477 var d = { __proto__: null, enumerable: true, configurable: true }; // Enumerable by default 1478 for (let p in desc) { 1479 if (!desc.hasOwnProperty(p)) continue; 1480 d[p] = desc[p]; 1481 } 1482 1483 if (opts) { 1484 if (opts.lazy && d.get) { 1485 let getter = d.get; 1486 d.configurable = true; // Make sure we can change the property later 1487 d.get = function() { 1488 let val = getter.call(this); 1489 1490 // Redefine getter on this object as non-writable value 1491 delete d.set; 1492 delete d.get; 1493 d.writable = false; 1494 d.value = val; 1495 Object.defineProperty(this, prop, d); 1496 1497 return val; 1498 } 1499 } 1500 } 1501 1502 Object.defineProperty(obj, prop, d); 1503 } 1504 1505 this.extendClass = function(superClass, newClass) { 1506 newClass._super = superClass; 1507 newClass.prototype = Object.create(superClass.prototype); 1508 newClass.prototype.constructor = newClass; 1509 } 1510 1511 1512 /* 1513 * This function should be removed 1514 * 1515 * |separator| defaults to a space (not a comma like Array.join()) if 1516 * not specified 1517 * 1518 * TODO: Substitute localized characters (e.g. Arabic comma and semicolon) 1519 */ 1520 function localeJoin(arr, separator) { 1521 if (typeof separator == 'undefined') { 1522 separator = ' '; 1523 } 1524 return arr.join(separator); 1525 } 1526 1527 1528 this.getLocaleCollation = function () { 1529 if (this.collation) { 1530 return this.collation; 1531 } 1532 1533 try { 1534 // DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're 1535 // passing manually? 1536 1537 let locales; 1538 // Fx55+ 1539 if (Services.locale.getAppLocalesAsBCP47) { 1540 locales = Services.locale.getAppLocalesAsBCP47(); 1541 } 1542 else { 1543 let locale; 1544 // Fx54 1545 if (Services.locale.getAppLocale) { 1546 locale = Services.locale.getAppLocale(); 1547 } 1548 // Fx <=53 1549 else { 1550 locale = Services.locale.getApplicationLocale(); 1551 locale = locale.getCategory('NSILOCALE_COLLATE'); 1552 } 1553 1554 // Extract a valid language tag 1555 try { 1556 locale = locale.match(/^[a-z]{2}(\-[A-Z]{2})?/)[0]; 1557 } 1558 catch (e) { 1559 throw new Error(`Error parsing locale ${locale}`); 1560 } 1561 locales = [locale]; 1562 } 1563 1564 var collator = new Intl.Collator(locales, { 1565 numeric: true, 1566 sensitivity: 'base' 1567 }); 1568 } 1569 catch (e) { 1570 Zotero.logError(e); 1571 1572 // Fall back to en-US sorting 1573 try { 1574 Zotero.logError("Falling back to en-US sorting"); 1575 collator = new Intl.Collator(['en-US'], { 1576 numeric: true, 1577 sensitivity: 'base' 1578 }); 1579 } 1580 catch (e) { 1581 Zotero.logError(e); 1582 1583 // If there's still an error, just skip sorting 1584 collator = { 1585 compare: function (a, b) { 1586 return 0; 1587 } 1588 }; 1589 } 1590 } 1591 1592 // Grab all ASCII punctuation and space at the begining of string 1593 var initPunctuationRE = /^[\x20-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/; 1594 // Punctuation that should be ignored when sorting 1595 var ignoreInitRE = /["'[{(]+$/; 1596 1597 // Until old code is updated, pretend we're returning an nsICollation 1598 return this.collation = { 1599 compareString: function (_, a, b) { 1600 if (!a && !b) return 0; 1601 if (!a || !b) return b ? -1 : 1; 1602 1603 // Compare initial punctuation 1604 var aInitP = initPunctuationRE.exec(a) || ''; 1605 var bInitP = initPunctuationRE.exec(b) || ''; 1606 1607 var aWordStart = 0, bWordStart = 0; 1608 if (aInitP) { 1609 aWordStart = aInitP[0].length; 1610 aInitP = aInitP[0].replace(ignoreInitRE, ''); 1611 } 1612 if (bInitP) { 1613 bWordStart = bInitP.length; 1614 bInitP = bInitP[0].replace(ignoreInitRE, ''); 1615 } 1616 1617 // If initial punctuation is equivalent, use collator comparison 1618 // that ignores all punctuation 1619 // 1620 // Update: Intl.Collator's ignorePunctuation also ignores whitespace, so we're 1621 // no longer using it, meaning we could take out most of the code to handle 1622 // initial punctuation separately, unless we think we'll at some point switch to 1623 // a collation function that ignores punctuation but not whitespace. 1624 if (aInitP == bInitP || !aInitP && !bInitP) return collator.compare(a, b); 1625 1626 // Otherwise consider "attached" words as well, e.g. the order should be 1627 // "__ n", "__z", "_a" 1628 // We don't actually care what the attached word is, just whether it's 1629 // there, since at this point we're guaranteed to have non-equivalent 1630 // initial punctuation 1631 if (aWordStart < a.length) aInitP += 'a'; 1632 if (bWordStart < b.length) bInitP += 'a'; 1633 1634 return aInitP.localeCompare(bInitP); 1635 } 1636 }; 1637 } 1638 1639 this.defineProperty(this, "localeCompare", { 1640 get: function() { 1641 var collation = this.getLocaleCollation(); 1642 return collation.compareString.bind(collation, 1); 1643 } 1644 }, {lazy: true}); 1645 1646 /* 1647 * Sets font size based on prefs -- intended for use on root element 1648 * (zotero-pane, note window, etc.) 1649 */ 1650 function setFontSize(rootElement) { 1651 var size = Zotero.Prefs.get('fontSize'); 1652 rootElement.style.fontSize = size + 'em'; 1653 if (size <= 1) { 1654 size = 'small'; 1655 } 1656 else if (size <= 1.25) { 1657 size = 'medium'; 1658 } 1659 else { 1660 size = 'large'; 1661 } 1662 // Custom attribute -- allows for additional customizations in zotero.css 1663 rootElement.setAttribute('zoteroFontSize', size); 1664 } 1665 1666 1667 /* 1668 * Flattens mixed arrays/values in a passed _arguments_ object and returns 1669 * an array of values -- allows for functions to accept both arrays of 1670 * values and/or an arbitrary number of individual values 1671 */ 1672 function flattenArguments(args){ 1673 // Put passed scalar values into an array 1674 if (args === null || typeof args == 'string' || typeof args.length == 'undefined') { 1675 args = [args]; 1676 } 1677 1678 var returns = []; 1679 for (var i=0; i<args.length; i++){ 1680 var arg = args[i]; 1681 if (!arg && arg !== 0) { 1682 continue; 1683 } 1684 if (Array.isArray(arg)) { 1685 returns.push(...arg); 1686 } 1687 else { 1688 returns.push(arg); 1689 } 1690 } 1691 return returns; 1692 } 1693 1694 1695 function getAncestorByTagName(elem, tagName){ 1696 while (elem.parentNode){ 1697 elem = elem.parentNode; 1698 if (elem.localName == tagName) { 1699 return elem; 1700 } 1701 } 1702 return false; 1703 } 1704 1705 1706 /** 1707 * Generate a random string of length 'len' (defaults to 8) 1708 **/ 1709 function randomString(len, chars) { 1710 return Zotero.Utilities.randomString(len, chars); 1711 } 1712 1713 1714 function moveToUnique(file, newFile){ 1715 newFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o644); 1716 var newName = newFile.leafName; 1717 newFile.remove(null); 1718 1719 // Move file to unique name 1720 file.moveTo(newFile.parent, newName); 1721 return file; 1722 } 1723 1724 1725 /** 1726 * Generate a function that produces a static output 1727 * 1728 * Zotero.lazy(fn) returns a function. The first time this function 1729 * is called, it calls fn() and returns its output. Subsequent 1730 * calls return the same output as the first without calling fn() 1731 * again. 1732 */ 1733 this.lazy = function(fn) { 1734 var x, called = false; 1735 return function() { 1736 if(!called) { 1737 x = fn.apply(this); 1738 called = true; 1739 } 1740 return x; 1741 }; 1742 }; 1743 1744 1745 this.serial = function (fn) { 1746 Components.utils.import("resource://zotero/concurrentCaller.js"); 1747 var caller = new ConcurrentCaller({ 1748 numConcurrent: 1, 1749 onError: e => Zotero.logError(e) 1750 }); 1751 return function () { 1752 var args = arguments; 1753 return caller.start(function () { 1754 return fn.apply(this, args); 1755 }.bind(this)); 1756 }; 1757 } 1758 1759 1760 this.spawn = function (generator, thisObject) { 1761 if (thisObject) { 1762 return Zotero.Promise.coroutine(generator.bind(thisObject))(); 1763 } 1764 return Zotero.Promise.coroutine(generator)(); 1765 } 1766 1767 1768 /** 1769 * Emulates the behavior of window.setTimeout 1770 * 1771 * @param {Function} func The function to be called 1772 * @param {Integer} ms The number of milliseconds to wait before calling func 1773 * @return {Integer} - ID of timer to be passed to clearTimeout() 1774 */ 1775 var _lastTimeoutID = 0; 1776 this.setTimeout = function (func, ms) { 1777 var id = ++_lastTimeoutID; 1778 1779 var timer = Components.classes["@mozilla.org/timer;1"] 1780 .createInstance(Components.interfaces.nsITimer); 1781 var timerCallback = { 1782 "notify": function () { 1783 func(); 1784 _runningTimers.delete(id); 1785 } 1786 }; 1787 timer.initWithCallback(timerCallback, ms, Components.interfaces.nsITimer.TYPE_ONE_SHOT); 1788 _runningTimers.set(id, timer); 1789 return id; 1790 }; 1791 1792 1793 this.clearTimeout = function (id) { 1794 var timer = _runningTimers.get(id); 1795 if (timer) { 1796 timer.cancel(); 1797 _runningTimers.delete(id); 1798 } 1799 }; 1800 1801 1802 /** 1803 * Show Zotero pane overlay and progress bar in all windows 1804 * 1805 * @param {String} msg 1806 * @param {Boolean} [determinate=false] 1807 * @param {Boolean} [modalOnly=false] - Don't use popup if Zotero pane isn't showing 1808 * @return void 1809 */ 1810 this.showZoteroPaneProgressMeter = function (msg, determinate, icon, modalOnly) { 1811 // If msg is undefined, keep any existing message. If false/null/"", clear. 1812 // The message is also cleared when the meters are hidden. 1813 _progressMessage = msg = (msg === undefined ? _progressMessage : msg) || ""; 1814 var currentWindow = Services.wm.getMostRecentWindow("navigator:browser"); 1815 var enumerator = Services.wm.getEnumerator("navigator:browser"); 1816 var progressMeters = []; 1817 while (enumerator.hasMoreElements()) { 1818 var win = enumerator.getNext(); 1819 if(!win.ZoteroPane) continue; 1820 if (!win.ZoteroPane.isShowing() && !modalOnly) { 1821 if (win != currentWindow) { 1822 continue; 1823 } 1824 1825 // If Zotero is closed in the top-most window, show a popup instead 1826 _progressPopup = new Zotero.ProgressWindow(); 1827 _progressPopup.changeHeadline("Zotero"); 1828 if (icon) { 1829 _progressPopup.addLines([msg], [icon]); 1830 } 1831 else { 1832 _progressPopup.addDescription(msg); 1833 } 1834 _progressPopup.show(); 1835 continue; 1836 } 1837 1838 var label = win.ZoteroPane.document.getElementById('zotero-pane-progress-label'); 1839 if (!label) { 1840 Components.utils.reportError("label not found in " + win.document.location.href); 1841 } 1842 if (msg) { 1843 label.hidden = false; 1844 label.value = msg; 1845 } 1846 else { 1847 label.hidden = true; 1848 } 1849 // This is the craziest thing. In Firefox 52.6.0, the very presence of this line 1850 // causes Zotero on Linux to burn 5% CPU at idle, even if everything below it in 1851 // the block is commented out. Same if the progressmeter itself is hidden="true". 1852 // For some reason it also doesn't seem to work to set the progressmeter to 1853 // 'determined' when hiding, which we're doing in lookup.js. So instead, create a new 1854 // progressmeter each time and delete it in _hideWindowZoteroPaneOverlay(). 1855 // 1856 //let progressMeter = win.ZoteroPane.document.getElementById('zotero-pane-progressmeter'); 1857 let doc = win.ZoteroPane.document; 1858 let container = doc.getElementById('zotero-pane-progressmeter-container'); 1859 let progressMeter = doc.createElement('progressmeter'); 1860 progressMeter.id = 'zotero-pane-progressmeter'; 1861 progressMeter.setAttribute('mode', 'undetermined'); 1862 if (determinate) { 1863 progressMeter.mode = 'determined'; 1864 progressMeter.value = 0; 1865 progressMeter.max = 1000; 1866 } 1867 else { 1868 progressMeter.mode = 'undetermined'; 1869 } 1870 container.appendChild(progressMeter); 1871 1872 _showWindowZoteroPaneOverlay(win.ZoteroPane.document); 1873 win.ZoteroPane.document.getElementById('zotero-pane-overlay-deck').selectedIndex = 0; 1874 1875 progressMeters.push(progressMeter); 1876 } 1877 this.locked = true; 1878 _progressMeters = progressMeters; 1879 } 1880 1881 1882 /** 1883 * @param {Number} percentage Percentage complete as integer or float 1884 */ 1885 this.updateZoteroPaneProgressMeter = function (percentage) { 1886 if(percentage !== null) { 1887 if (percentage < 0 || percentage > 100) { 1888 Zotero.debug("Invalid percentage value '" + percentage + "' in Zotero.updateZoteroPaneProgressMeter()"); 1889 return; 1890 } 1891 percentage = Math.round(percentage * 10); 1892 } 1893 if (percentage === _lastPercentage) { 1894 return; 1895 } 1896 for (let pm of _progressMeters) { 1897 if (percentage !== null) { 1898 if (pm.mode == 'undetermined') { 1899 pm.max = 1000; 1900 pm.mode = 'determined'; 1901 } 1902 pm.value = percentage; 1903 } else if(pm.mode === 'determined') { 1904 pm.mode = 'undetermined'; 1905 } 1906 } 1907 _lastPercentage = percentage; 1908 } 1909 1910 1911 /** 1912 * Hide Zotero pane overlay in all windows 1913 */ 1914 this.hideZoteroPaneOverlays = function () { 1915 this.locked = false; 1916 1917 var enumerator = Services.wm.getEnumerator("navigator:browser"); 1918 while (enumerator.hasMoreElements()) { 1919 var win = enumerator.getNext(); 1920 if(win.ZoteroPane && win.ZoteroPane.document) { 1921 _hideWindowZoteroPaneOverlay(win.ZoteroPane.document); 1922 } 1923 } 1924 1925 if (_progressPopup) { 1926 _progressPopup.close(); 1927 } 1928 1929 _progressMessage = null; 1930 _progressMeters = []; 1931 _progressPopup = null; 1932 _lastPercentage = null; 1933 } 1934 1935 1936 /** 1937 * Adds a listener to be called when Zotero shuts down (even if Firefox is not shut down) 1938 */ 1939 this.addShutdownListener = function(listener) { 1940 _shutdownListeners.push(listener); 1941 } 1942 1943 function _showWindowZoteroPaneOverlay(doc) { 1944 doc.getElementById('zotero-collections-tree').disabled = true; 1945 doc.getElementById('zotero-items-tree').disabled = true; 1946 doc.getElementById('zotero-pane-tab-catcher-top').hidden = false; 1947 doc.getElementById('zotero-pane-tab-catcher-bottom').hidden = false; 1948 doc.getElementById('zotero-pane-overlay').hidden = false; 1949 } 1950 1951 1952 function _hideWindowZoteroPaneOverlay(doc) { 1953 doc.getElementById('zotero-collections-tree').disabled = false; 1954 doc.getElementById('zotero-items-tree').disabled = false; 1955 doc.getElementById('zotero-pane-tab-catcher-top').hidden = true; 1956 doc.getElementById('zotero-pane-tab-catcher-bottom').hidden = true; 1957 doc.getElementById('zotero-pane-overlay').hidden = true; 1958 1959 // See note in showZoteroPaneProgressMeter() 1960 let pm = doc.getElementById('zotero-pane-progressmeter'); 1961 if (pm) { 1962 pm.parentNode.removeChild(pm); 1963 } 1964 } 1965 1966 1967 this.updateQuickSearchBox = function (document) { 1968 var searchBox = document.getElementById('zotero-tb-search'); 1969 if(!searchBox) return; 1970 1971 var mode = Zotero.Prefs.get("search.quicksearch-mode"); 1972 var prefix = 'zotero-tb-search-mode-'; 1973 var prefixLen = prefix.length; 1974 1975 var modes = { 1976 titleCreatorYear: { 1977 label: Zotero.getString('quickSearch.mode.titleCreatorYear') 1978 }, 1979 1980 fields: { 1981 label: Zotero.getString('quickSearch.mode.fieldsAndTags') 1982 }, 1983 1984 everything: { 1985 label: Zotero.getString('quickSearch.mode.everything') 1986 } 1987 }; 1988 1989 if (!modes[mode]) { 1990 Zotero.Prefs.set("search.quicksearch-mode", "fields"); 1991 mode = 'fields'; 1992 } 1993 1994 var hbox = document.getAnonymousNodes(searchBox)[0]; 1995 var input = hbox.getElementsByAttribute('class', 'textbox-input')[0]; 1996 1997 // Already initialized, so just update selection 1998 var button = hbox.getElementsByAttribute('id', 'zotero-tb-search-menu-button'); 1999 if (button.length) { 2000 button = button[0]; 2001 var menupopup = button.firstChild; 2002 for (let menuitem of menupopup.childNodes) { 2003 if (menuitem.id.substr(prefixLen) == mode) { 2004 menuitem.setAttribute('checked', true); 2005 searchBox.placeholder = modes[mode].label; 2006 return; 2007 } 2008 } 2009 return; 2010 } 2011 2012 // Otherwise, build menu 2013 button = document.createElement('button'); 2014 button.id = 'zotero-tb-search-menu-button'; 2015 button.setAttribute('type', 'menu'); 2016 2017 var menupopup = document.createElement('menupopup'); 2018 2019 for (var i in modes) { 2020 var menuitem = document.createElement('menuitem'); 2021 menuitem.setAttribute('id', prefix + i); 2022 menuitem.setAttribute('label', modes[i].label); 2023 menuitem.setAttribute('name', 'searchMode'); 2024 menuitem.setAttribute('type', 'radio'); 2025 //menuitem.setAttribute("tooltiptext", ""); 2026 2027 menupopup.appendChild(menuitem); 2028 2029 if (mode == i) { 2030 menuitem.setAttribute('checked', true); 2031 menupopup.selectedItem = menuitem; 2032 } 2033 } 2034 2035 menupopup.addEventListener("command", function(event) { 2036 var mode = event.target.id.substr(22); 2037 Zotero.Prefs.set("search.quicksearch-mode", mode); 2038 if (document.getElementById("zotero-tb-search").value == "") { 2039 event.stopPropagation(); 2040 } 2041 }, false); 2042 2043 button.appendChild(menupopup); 2044 hbox.insertBefore(button, input); 2045 2046 searchBox.placeholder = modes[mode].label; 2047 2048 // If Alt-Up/Down, show popup 2049 searchBox.addEventListener("keypress", function(event) { 2050 if (event.altKey && (event.keyCode == event.DOM_VK_UP || event.keyCode == event.DOM_VK_DOWN)) { 2051 document.getElementById('zotero-tb-search-menu-button').open = true; 2052 event.stopPropagation(); 2053 } 2054 }, false); 2055 } 2056 2057 2058 /* 2059 * Clear entries that no longer exist from various tables 2060 */ 2061 this.purgeDataObjects = Zotero.Promise.coroutine(function* () { 2062 var d = new Date(); 2063 2064 yield Zotero.DB.executeTransaction(function* () { 2065 return Zotero.Creators.purge(); 2066 }); 2067 yield Zotero.DB.executeTransaction(function* () { 2068 return Zotero.Tags.purge(); 2069 }); 2070 yield Zotero.Fulltext.purgeUnusedWords(); 2071 yield Zotero.DB.executeTransaction(function* () { 2072 return Zotero.Items.purge(); 2073 }); 2074 // DEBUG: this might not need to be permanent 2075 //yield Zotero.DB.executeTransaction(function* () { 2076 // return Zotero.Relations.purge(); 2077 //}); 2078 2079 Zotero.debug("Purged data tables in " + (new Date() - d) + " ms"); 2080 }); 2081 2082 2083 this.reloadDataObjects = function () { 2084 return Zotero.Promise.all([ 2085 Zotero.Collections.reloadAll(), 2086 Zotero.Creators.reloadAll(), 2087 Zotero.Items.reloadAll() 2088 ]); 2089 } 2090 2091 2092 /** 2093 * Brings Zotero Standalone to the foreground 2094 */ 2095 this.activateStandalone = function() { 2096 var uri = Services.io.newURI('zotero://select', null, null); 2097 var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1'] 2098 .getService(Components.interfaces.nsIExternalProtocolService) 2099 .getProtocolHandlerInfo('zotero'); 2100 handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault; 2101 handler.launchWithURI(uri, null); 2102 } 2103 2104 /** 2105 * Determines whether to keep an error message so that it can (potentially) be reported later 2106 */ 2107 function _shouldKeepError(msg) { 2108 const skip = ['CSS Parser', 'content javascript']; 2109 2110 //Zotero.debug(msg); 2111 try { 2112 msg.QueryInterface(Components.interfaces.nsIScriptError); 2113 //Zotero.debug(msg); 2114 if (skip.indexOf(msg.category) != -1 || msg.flags & msg.warningFlag) { 2115 return false; 2116 } 2117 } 2118 catch (e) { } 2119 2120 const blacklist = [ 2121 "No chrome package registered for chrome://communicator", 2122 '[JavaScript Error: "Components is not defined" {file: "chrome://nightly/content/talkback/talkback.js', 2123 '[JavaScript Error: "document.getElementById("sanitizeItem")', 2124 'No chrome package registered for chrome://piggy-bank', 2125 '[JavaScript Error: "[Exception... "\'Component is not available\' when calling method: [nsIHandlerService::getTypeFromExtension', 2126 '[JavaScript Error: "this._uiElement is null', 2127 'Error: a._updateVisibleText is not a function', 2128 '[JavaScript Error: "Warning: unrecognized command line flag ', 2129 'LibX:', 2130 'function skype_', 2131 '[JavaScript Error: "uncaught exception: Permission denied to call method Location.toString"]', 2132 'CVE-2009-3555', 2133 'OpenGL', 2134 'trying to re-register CID', 2135 'Services.HealthReport', 2136 '[JavaScript Error: "this.docShell is null"', 2137 '[JavaScript Error: "downloadable font:', 2138 '[JavaScript Error: "Image corrupt or truncated:', 2139 '[JavaScript Error: "The character encoding of the', 2140 'nsLivemarkService.js', 2141 'Sync.Engine.Tabs', 2142 'content-sessionStore.js', 2143 'org.mozilla.appSessions', 2144 'bad script XDR magic number', 2145 'did not contain an updates property', 2146 ]; 2147 2148 for (var i=0; i<blacklist.length; i++) { 2149 if (msg.message.indexOf(blacklist[i]) != -1) { 2150 //Zotero.debug("Skipping blacklisted error: " + msg.message); 2151 return false; 2152 } 2153 } 2154 2155 return true; 2156 } 2157 2158 /** 2159 * Warn if Zotero Standalone is running as root and clobber the cache directory if it is 2160 */ 2161 function _checkRoot() { 2162 var env = Components.classes["@mozilla.org/process/environment;1"]. 2163 getService(Components.interfaces.nsIEnvironment); 2164 var user = env.get("USER") || env.get("USERNAME"); 2165 if(user === "root") { 2166 // Show warning 2167 if(Services.prompt.confirmEx(null, "", Zotero.getString("standalone.rootWarning"), 2168 Services.prompt.BUTTON_POS_0*Services.prompt.BUTTON_TITLE_IS_STRING | 2169 Services.prompt.BUTTON_POS_1*Services.prompt.BUTTON_TITLE_IS_STRING, 2170 Zotero.getString("standalone.rootWarning.exit"), 2171 Zotero.getString("standalone.rootWarning.continue"), 2172 null, null, {}) == 0) { 2173 Components.utils.import("resource://gre/modules/ctypes.jsm"); 2174 var exit = Zotero.IPC.getLibc().declare("exit", ctypes.default_abi, 2175 ctypes.void_t, ctypes.int); 2176 // Zap cache files 2177 try { 2178 Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).remove(true); 2179 } catch(e) {} 2180 // Exit Zotero without giving XULRunner the opportunity to figure out the 2181 // cache is missing. Otherwise XULRunner will zap the prefs 2182 exit(0); 2183 } 2184 } 2185 } 2186 2187 /** 2188 * Observer for console messages 2189 * @namespace 2190 */ 2191 var ConsoleListener = { 2192 "QueryInterface":XPCOMUtils.generateQI([Components.interfaces.nsIConsoleMessage, 2193 Components.interfaces.nsISupports]), 2194 "observe":function(msg) { 2195 if(!_shouldKeepError(msg)) return; 2196 if(_recentErrors.length === ERROR_BUFFER_SIZE) _recentErrors.shift(); 2197 _recentErrors.push(msg); 2198 } 2199 }; 2200 }).call(Zotero); 2201 2202 Zotero.Prefs = new function(){ 2203 // Privileged methods 2204 this.init = init; 2205 this.get = get; 2206 this.set = set; 2207 2208 this.register = register; 2209 this.unregister = unregister; 2210 this.observe = observe; 2211 2212 // Public properties 2213 this.prefBranch; 2214 2215 function init(){ 2216 this.prefBranch = Services.prefs.getBranch(ZOTERO_CONFIG.PREF_BRANCH); 2217 2218 // Register observer to handle pref changes 2219 this.register(); 2220 2221 // Unregister observer handling pref changes 2222 if (Zotero.addShutdownListener) { 2223 Zotero.addShutdownListener(this.unregister.bind(this)); 2224 } 2225 2226 // Process pref version updates 2227 var fromVersion = this.get('prefVersion'); 2228 if (!fromVersion) { 2229 fromVersion = 0; 2230 } 2231 var toVersion = 2; 2232 if (fromVersion < toVersion) { 2233 for (var i = fromVersion + 1; i <= toVersion; i++) { 2234 switch (i) { 2235 case 1: 2236 // If a sync username is entered and ZFS is enabled, turn 2237 // on-demand downloading off to maintain current behavior 2238 if (this.get('sync.server.username')) { 2239 if (this.get('sync.storage.enabled') 2240 && this.get('sync.storage.protocol') == 'zotero') { 2241 this.set('sync.storage.downloadMode.personal', 'on-sync'); 2242 } 2243 if (this.get('sync.storage.groups.enabled')) { 2244 this.set('sync.storage.downloadMode.groups', 'on-sync'); 2245 } 2246 } 2247 break; 2248 2249 case 2: 2250 // Re-show saveButton guidance panel (and clear old saveIcon pref). 2251 // The saveButton guidance panel initially could auto-hide too easily. 2252 this.clear('firstRunGuidanceShown.saveIcon'); 2253 this.clear('firstRunGuidanceShown.saveButton'); 2254 break; 2255 } 2256 } 2257 this.set('prefVersion', toVersion); 2258 } 2259 } 2260 2261 2262 /** 2263 * Retrieve a preference 2264 **/ 2265 function get(pref, global){ 2266 try { 2267 if (global) { 2268 var branch = Services.prefs.getBranch(""); 2269 } 2270 else { 2271 var branch = this.prefBranch; 2272 } 2273 2274 switch (branch.getPrefType(pref)){ 2275 case branch.PREF_BOOL: 2276 return branch.getBoolPref(pref); 2277 case branch.PREF_STRING: 2278 return '' + branch.getComplexValue(pref, Components.interfaces.nsISupportsString); 2279 case branch.PREF_INT: 2280 return branch.getIntPref(pref); 2281 } 2282 } 2283 catch (e){ 2284 throw ("Invalid preference '" + pref + "'"); 2285 } 2286 } 2287 2288 2289 /** 2290 * Set a preference 2291 **/ 2292 function set(pref, value, global) { 2293 try { 2294 if (global) { 2295 var branch = Services.prefs.getBranch(""); 2296 } 2297 else { 2298 var branch = this.prefBranch; 2299 } 2300 2301 switch (branch.getPrefType(pref)) { 2302 case branch.PREF_BOOL: 2303 return branch.setBoolPref(pref, value); 2304 case branch.PREF_STRING: 2305 let str = Cc["@mozilla.org/supports-string;1"] 2306 .createInstance(Ci.nsISupportsString); 2307 str.data = value; 2308 return branch.setComplexValue(pref, Ci.nsISupportsString, str); 2309 case branch.PREF_INT: 2310 return branch.setIntPref(pref, value); 2311 2312 // If not an existing pref, create appropriate type automatically 2313 case 0: 2314 if (typeof value == 'boolean') { 2315 Zotero.debug("Creating boolean pref '" + pref + "'"); 2316 return branch.setBoolPref(pref, value); 2317 } 2318 if (typeof value == 'string') { 2319 Zotero.debug("Creating string pref '" + pref + "'"); 2320 return branch.setCharPref(pref, value); 2321 } 2322 if (parseInt(value) == value) { 2323 Zotero.debug("Creating integer pref '" + pref + "'"); 2324 return branch.setIntPref(pref, value); 2325 } 2326 throw new Error("Invalid preference value '" + value + "' for pref '" + pref + "'"); 2327 } 2328 } 2329 catch (e) { 2330 Zotero.logError(e); 2331 throw new Error("Invalid preference '" + pref + "'"); 2332 } 2333 } 2334 2335 2336 this.clear = function (pref, global) { 2337 if (global) { 2338 var branch = Services.prefs.getBranch(""); 2339 } 2340 else { 2341 var branch = this.prefBranch; 2342 } 2343 branch.clearUserPref(pref); 2344 } 2345 2346 2347 this.resetBranch = function (exclude = []) { 2348 var keys = this.prefBranch.getChildList("", {}); 2349 for (let key of keys) { 2350 if (this.prefBranch.prefHasUserValue(key)) { 2351 if (exclude.includes(key)) { 2352 continue; 2353 } 2354 Zotero.debug("Clearing " + key); 2355 this.prefBranch.clearUserPref(key); 2356 } 2357 } 2358 }; 2359 2360 2361 // Import settings bundles 2362 this.importSettings = function (str, uri) { 2363 var ps = Services.prompt; 2364 2365 if (!uri.match(/https:\/\/([^\.]+\.)?zotero.org\//)) { 2366 Zotero.debug("Ignoring settings file not from https://zotero.org"); 2367 return; 2368 } 2369 2370 str = Zotero.Utilities.trim(str.replace(/<\?xml.*\?>\s*/, '')); 2371 Zotero.debug(str); 2372 2373 var confirm = ps.confirm( 2374 null, 2375 "", 2376 "Apply settings from zotero.org?" 2377 ); 2378 2379 if (!confirm) { 2380 return; 2381 } 2382 2383 // TODO: parse settings XML 2384 } 2385 2386 // Handlers for some Zotero preferences 2387 var _handlers = [ 2388 [ "automaticScraperUpdates", function(val) { 2389 if (val){ 2390 Zotero.Schema.updateFromRepository(1); 2391 } 2392 else { 2393 Zotero.Schema.stopRepositoryTimer(); 2394 } 2395 }], 2396 ["fontSize", function (val) { 2397 Zotero.setFontSize( 2398 Zotero.getActiveZoteroPane().document.getElementById('zotero-pane') 2399 ); 2400 }], 2401 [ "layout", function(val) { 2402 Zotero.getActiveZoteroPane().updateLayout(); 2403 }], 2404 [ "note.fontSize", function(val) { 2405 if (val < 6) { 2406 Zotero.Prefs.set('note.fontSize', 11); 2407 } 2408 }], 2409 [ "zoteroDotOrgVersionHeader", function(val) { 2410 if (val) { 2411 Zotero.VersionHeader.register(); 2412 } 2413 else { 2414 Zotero.VersionHeader.unregister(); 2415 } 2416 }], 2417 [ "sync.autoSync", function(val) { 2418 if (val) { 2419 Zotero.Sync.EventListeners.AutoSyncListener.register(); 2420 Zotero.Sync.EventListeners.IdleListener.register(); 2421 } 2422 else { 2423 Zotero.Sync.EventListeners.AutoSyncListener.unregister(); 2424 Zotero.Sync.EventListeners.IdleListener.unregister(); 2425 } 2426 }], 2427 [ "search.quicksearch-mode", function(val) { 2428 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 2429 .getService(Components.interfaces.nsIWindowMediator); 2430 var enumerator = wm.getEnumerator("navigator:browser"); 2431 while (enumerator.hasMoreElements()) { 2432 var win = enumerator.getNext(); 2433 if (!win.ZoteroPane) continue; 2434 Zotero.updateQuickSearchBox(win.ZoteroPane.document); 2435 } 2436 2437 var enumerator = wm.getEnumerator("zotero:item-selector"); 2438 while (enumerator.hasMoreElements()) { 2439 var win = enumerator.getNext(); 2440 if (!win.Zotero) continue; 2441 Zotero.updateQuickSearchBox(win.document); 2442 } 2443 }] 2444 ]; 2445 2446 // 2447 // Methods to register a preferences observer 2448 // 2449 function register(){ 2450 this.prefBranch.QueryInterface(Components.interfaces.nsIPrefBranch2); 2451 this.prefBranch.addObserver("", this, false); 2452 2453 // Register pre-set handlers 2454 for (var i=0; i<_handlers.length; i++) { 2455 this.registerObserver(_handlers[i][0], _handlers[i][1]); 2456 } 2457 } 2458 2459 function unregister(){ 2460 if (!this.prefBranch){ 2461 return; 2462 } 2463 this.prefBranch.removeObserver("", this); 2464 } 2465 2466 /** 2467 * @param {nsIPrefBranch} subject The nsIPrefBranch we're observing (after appropriate QI) 2468 * @param {String} topic The string defined by NS_PREFBRANCH_PREFCHANGE_TOPIC_ID 2469 * @param {String} data The name of the pref that's been changed (relative to subject) 2470 */ 2471 function observe(subject, topic, data){ 2472 if (topic != "nsPref:changed" || !_observers[data] || !_observers[data].length) { 2473 return; 2474 } 2475 2476 var obs = _observers[data]; 2477 for (var i=0; i<obs.length; i++) { 2478 try { 2479 obs[i](this.get(data)); 2480 } 2481 catch (e) { 2482 Zotero.debug("Error while executing preference observer handler for " + data); 2483 Zotero.debug(e); 2484 } 2485 } 2486 } 2487 2488 var _observers = {}; 2489 this.registerObserver = function(name, handler) { 2490 _observers[name] = _observers[name] || []; 2491 _observers[name].push(handler); 2492 } 2493 2494 this.unregisterObserver = function(name, handler) { 2495 var obs = _observers[name]; 2496 if (!obs) { 2497 Zotero.debug("No preferences observer registered for " + name); 2498 return; 2499 } 2500 2501 var i = obs.indexOf(handler); 2502 if (i == -1) { 2503 Zotero.debug("Handler was not registered for preference " + name); 2504 return; 2505 } 2506 2507 obs.splice(i, 1); 2508 } 2509 } 2510 2511 2512 /* 2513 * Handles keyboard shortcut initialization from preferences, optionally 2514 * overriding existing global shortcuts 2515 * 2516 * Actions are configured in ZoteroPane.handleKeyPress() 2517 */ 2518 Zotero.Keys = new function() { 2519 this.init = init; 2520 this.windowInit = windowInit; 2521 this.getCommand = getCommand; 2522 2523 var _keys = {}; 2524 2525 2526 /* 2527 * Called by Zotero.init() 2528 */ 2529 function init() { 2530 var cmds = Zotero.Prefs.prefBranch.getChildList('keys', {}, {}); 2531 2532 // Get the key=>command mappings from the prefs 2533 for (let cmd of cmds) { 2534 cmd = cmd.substr(5); // strips 'keys.' 2535 // Remove old pref 2536 if (cmd == 'overrideGlobal') { 2537 Zotero.Prefs.clear('keys.overrideGlobal'); 2538 continue; 2539 } 2540 _keys[this.getKeyForCommand(cmd)] = cmd; 2541 } 2542 } 2543 2544 2545 /* 2546 * Called by ZoteroPane.onLoad() 2547 */ 2548 function windowInit(document) { 2549 var globalKeys = [ 2550 { 2551 name: 'openZotero', 2552 defaultKey: 'Z' 2553 }, 2554 { 2555 name: 'saveToZotero', 2556 defaultKey: 'S' 2557 } 2558 ]; 2559 2560 globalKeys.forEach(function (x) { 2561 let keyElem = document.getElementById('key_' + x.name); 2562 if (keyElem) { 2563 let prefKey = this.getKeyForCommand(x.name); 2564 // Only override the default with the pref if the <key> hasn't 2565 // been manually changed and the pref has been 2566 if (keyElem.getAttribute('key') == x.defaultKey 2567 && keyElem.getAttribute('modifiers') == 'accel shift' 2568 && prefKey != x.defaultKey) { 2569 keyElem.setAttribute('key', prefKey); 2570 } 2571 } 2572 }.bind(this)); 2573 } 2574 2575 2576 function getCommand(key) { 2577 key = key.toUpperCase(); 2578 return _keys[key] ? _keys[key] : false; 2579 } 2580 2581 2582 this.getKeyForCommand = function (cmd) { 2583 try { 2584 var key = Zotero.Prefs.get('keys.' + cmd); 2585 } 2586 catch (e) {} 2587 return key !== undefined ? key.toUpperCase() : false; 2588 } 2589 } 2590 2591 2592 /** 2593 * Add X-Zotero-Version header to HTTP requests to zotero.org 2594 * 2595 * @namespace 2596 */ 2597 Zotero.VersionHeader = { 2598 init: function () { 2599 if (Zotero.Prefs.get("zoteroDotOrgVersionHeader")) { 2600 this.register(); 2601 } 2602 Zotero.addShutdownListener(this.unregister); 2603 }, 2604 2605 // Called from this.init() and Zotero.Prefs.observe() 2606 register: function () { 2607 Services.obs.addObserver(this, "http-on-modify-request", false); 2608 }, 2609 2610 observe: function (subject, topic, data) { 2611 try { 2612 let channel = subject.QueryInterface(Components.interfaces.nsIHttpChannel); 2613 let domain = channel.URI.host; 2614 if (domain.endsWith(ZOTERO_CONFIG.DOMAIN_NAME)) { 2615 channel.setRequestHeader("X-Zotero-Version", Zotero.version, false); 2616 } 2617 else { 2618 let ua = channel.getRequestHeader('User-Agent'); 2619 ua = this.update(domain, ua); 2620 channel.setRequestHeader('User-Agent', ua, false); 2621 } 2622 } 2623 catch (e) { 2624 Zotero.debug(e, 1); 2625 } 2626 }, 2627 2628 /** 2629 * Add Firefox/[version] to the default user agent and replace Zotero/[version] with 2630 * Zotero/[major.minor] (except for requests to zotero.org, where we include the full version) 2631 * 2632 * @param {String} domain 2633 * @param {String} ua - User Agent 2634 * @param {String} [testAppName] - App name to look for (necessary in tests, which are 2635 * currently run in Firefox) 2636 */ 2637 update: function (domain, ua, testAppName) { 2638 var info = Services.appinfo; 2639 var appName = testAppName || info.name; 2640 2641 var pos = ua.indexOf(appName + '/'); 2642 2643 // Default UA 2644 if (pos != -1) { 2645 ua = ua.slice(0, pos) + `Firefox/${info.platformVersion.match(/^\d+/)[0]}.0 ` 2646 + appName + '/'; 2647 } 2648 // Fake UA from connector 2649 else { 2650 ua += ' ' + appName + '/'; 2651 } 2652 ua += Zotero.version.replace(/(\d+\.\d+).*/, '$1'); 2653 2654 return ua; 2655 }, 2656 2657 unregister: function () { 2658 Services.obs.removeObserver(Zotero.VersionHeader, "http-on-modify-request"); 2659 } 2660 } 2661 2662 Zotero.DragDrop = { 2663 currentEvent: null, 2664 currentOrientation: 0, 2665 currentSourceNode: null, 2666 2667 getDataFromDataTransfer: function (dataTransfer, firstOnly) { 2668 var dt = dataTransfer; 2669 2670 var dragData = { 2671 dataType: '', 2672 data: [], 2673 dropEffect: dt.dropEffect 2674 }; 2675 2676 var len = firstOnly ? 1 : dt.mozItemCount; 2677 2678 if (dt.types.contains('zotero/collection')) { 2679 dragData.dataType = 'zotero/collection'; 2680 let ids = dt.getData('zotero/collection').split(",").map(id => parseInt(id)); 2681 dragData.data = ids; 2682 } 2683 else if (dt.types.contains('zotero/item')) { 2684 dragData.dataType = 'zotero/item'; 2685 let ids = dt.getData('zotero/item').split(",").map(id => parseInt(id)); 2686 dragData.data = ids; 2687 } 2688 else { 2689 if (dt.types.contains('application/x-moz-file')) { 2690 dragData.dataType = 'application/x-moz-file'; 2691 var files = []; 2692 for (var i=0; i<len; i++) { 2693 var file = dt.mozGetDataAt("application/x-moz-file", i); 2694 if (!file) { 2695 continue; 2696 } 2697 file.QueryInterface(Components.interfaces.nsIFile); 2698 // Don't allow folder drag 2699 if (file.isDirectory()) { 2700 continue; 2701 } 2702 files.push(file); 2703 } 2704 dragData.data = files; 2705 } 2706 // This isn't an else because on Linux a link drag contains an empty application/x-moz-file too 2707 if (!dragData.data || !dragData.data.length) { 2708 if (dt.types.contains('text/x-moz-url')) { 2709 dragData.dataType = 'text/x-moz-url'; 2710 var urls = []; 2711 for (var i=0; i<len; i++) { 2712 var url = dt.getData("text/x-moz-url").split("\n")[0]; 2713 urls.push(url); 2714 } 2715 dragData.data = urls; 2716 } 2717 } 2718 } 2719 2720 return dragData; 2721 }, 2722 2723 2724 getDragSource: function (dataTransfer) { 2725 if (!dataTransfer) { 2726 //Zotero.debug("Drag data not available", 2); 2727 return false; 2728 } 2729 2730 // For items, the drag source is the CollectionTreeRow of the parent window 2731 // of the source tree 2732 if (dataTransfer.types.contains("zotero/item")) { 2733 let sourceNode = dataTransfer.mozSourceNode || this.currentSourceNode; 2734 if (!sourceNode || sourceNode.tagName != 'treechildren' 2735 || sourceNode.parentElement.id != 'zotero-items-tree') { 2736 return false; 2737 } 2738 var win = sourceNode.ownerDocument.defaultView; 2739 if (win.document.documentElement.getAttribute('windowtype') == 'zotero:search') { 2740 return win.ZoteroAdvancedSearch.itemsView.collectionTreeRow; 2741 } 2742 return win.ZoteroPane.collectionsView.selectedTreeRow; 2743 } 2744 2745 return false; 2746 }, 2747 2748 2749 getDragTarget: function (event) { 2750 var target = event.target; 2751 if (target.tagName == 'treechildren') { 2752 var tree = target.parentNode; 2753 if (tree.id == 'zotero-collections-tree') { 2754 let row = {}, col = {}, obj = {}; 2755 tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); 2756 let win = tree.ownerDocument.defaultView; 2757 return win.ZoteroPane.collectionsView.getRow(row.value); 2758 } 2759 } 2760 return false; 2761 } 2762 } 2763 2764 2765 /** 2766 * Functions for creating and destroying hidden browser objects 2767 **/ 2768 Zotero.Browser = new function() { 2769 var nBrowsers = 0; 2770 2771 this.createHiddenBrowser = function (win) { 2772 if (!win) { 2773 win = Services.wm.getMostRecentWindow("navigator:browser"); 2774 if (!win) { 2775 win = Services.ww.activeWindow; 2776 } 2777 // Use the hidden DOM window on macOS with the main window closed 2778 if (!win) { 2779 let appShellService = Components.classes["@mozilla.org/appshell/appShellService;1"] 2780 .getService(Components.interfaces.nsIAppShellService); 2781 win = appShellService.hiddenDOMWindow; 2782 } 2783 if (!win) { 2784 throw new Error("Parent window not available for hidden browser"); 2785 } 2786 } 2787 2788 // Create a hidden browser 2789 var hiddenBrowser = win.document.createElement("browser"); 2790 hiddenBrowser.setAttribute('type', 'content'); 2791 hiddenBrowser.setAttribute('disablehistory', 'true'); 2792 win.document.documentElement.appendChild(hiddenBrowser); 2793 // Disable some features 2794 hiddenBrowser.docShell.allowAuth = false; 2795 hiddenBrowser.docShell.allowDNSPrefetch = false; 2796 hiddenBrowser.docShell.allowImages = false; 2797 hiddenBrowser.docShell.allowJavascript = true; 2798 hiddenBrowser.docShell.allowMetaRedirects = false; 2799 hiddenBrowser.docShell.allowPlugins = false; 2800 Zotero.debug("Created hidden browser (" + (nBrowsers++) + ")"); 2801 return hiddenBrowser; 2802 } 2803 2804 this.deleteHiddenBrowser = function (myBrowsers) { 2805 if(!(myBrowsers instanceof Array)) myBrowsers = [myBrowsers]; 2806 for(var i=0; i<myBrowsers.length; i++) { 2807 var myBrowser = myBrowsers[i]; 2808 myBrowser.stop(); 2809 myBrowser.destroy(); 2810 myBrowser.parentNode.removeChild(myBrowser); 2811 myBrowser = null; 2812 Zotero.debug("Deleted hidden browser (" + (--nBrowsers) + ")"); 2813 } 2814 } 2815 } 2816 2817 2818 /* 2819 * Implements nsIWebProgressListener 2820 */ 2821 Zotero.WebProgressFinishListener = function(onFinish) { 2822 var _request; 2823 2824 this.getRequest = function () { 2825 return _request; 2826 }; 2827 2828 this.onStateChange = function(wp, req, stateFlags, status) { 2829 //Zotero.debug('onStageChange: ' + stateFlags); 2830 if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP 2831 && stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK) { 2832 _request = null; 2833 onFinish(); 2834 } 2835 else { 2836 _request = req; 2837 } 2838 } 2839 2840 this.onProgressChange = function(wp, req, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { 2841 //Zotero.debug('onProgressChange'); 2842 //Zotero.debug('Current: ' + curTotalProgress); 2843 //Zotero.debug('Max: ' + maxTotalProgress); 2844 } 2845 2846 this.onLocationChange = function(wp, req, location) {} 2847 this.onSecurityChange = function(wp, req, stateFlags, status) {} 2848 this.onStatusChange = function(wp, req, status, msg) {} 2849 } 2850 2851 /* 2852 * Saves or loads JSON objects. 2853 */ 2854 Zotero.JSON = new function() { 2855 this.serialize = function(arg) { 2856 Zotero.debug("WARNING: Zotero.JSON.serialize() is deprecated; use JSON.stringify()"); 2857 return JSON.stringify(arg); 2858 } 2859 2860 this.unserialize = function(arg) { 2861 Zotero.debug("WARNING: Zotero.JSON.unserialize() is deprecated; use JSON.parse()"); 2862 return JSON.parse(arg); 2863 } 2864 }