Remove Zotero for Firefox data directory migration code

The new-install branch in DataDirectory.init() read prefs.js from the
default Firefox profile to detect a pre-2017 dataDir setting, and the
read was unwrapped, making prefs.js access errors fatal at startup [1].
We could add a try/catch, but after nine years, it's probably safe to
just remove the migration. If anyone is returning from >9 years in the
wilderness and they still want old unsynced Zotero data, they can
manually move their data to the default location.

[1] https://forums.zotero.org/discussion/131176/installation-error-accessing-mozillas-pref-js-see-msg-pls
This commit is contained in:
Dan Stillman 2026-04-27 15:00:04 -04:00
parent 462357e49e
commit 1ff2f640eb
5 changed files with 15 additions and 453 deletions

View file

@ -177,26 +177,18 @@ Zotero.DataDirectory = {
// New installation of 5.0+ with no data directory specified, so check all the places the data
// could be
else {
let useProfile = false;
let useFirefoxProfile = false;
let useFirefoxProfileCustom = false;
let dataDirNamedAfterProfile = false;
dataDir = this.defaultDir;
Zotero.fxProfileAccessError = false;
// If there's already a profile pointing to the default location, use a different
// data directory named after the profile, as long as one either doesn't exist yet or
// one does and it contains a database
try {
if (((await Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir, false))).length) {
if (((await Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir))).length) {
let profileName = PathUtils.filename(Zotero.Profile.dir).match(/[^.]+\.(.+)/)[1];
let newDataDir = this.defaultDir + ' ' + profileName;
if (!((await OS.File.exists(newDataDir)))
|| ((await OS.File.exists(OS.Path.join(newDataDir, dbFilename))))) {
dataDir = newDataDir;
dataDirNamedAfterProfile = true;
}
}
}
@ -216,15 +208,13 @@ Zotero.DataDirectory = {
return dataDir;
}
// Check for <profile dir>/zotero/zotero.sqlite
let profileSubdirModTime;
// Check for <profile dir>/zotero/zotero.sqlite (Zotero Standalone <5.0)
try {
let dir = OS.Path.join(Zotero.Profile.dir, this.legacyDirName);
let dbFile = OS.Path.join(dir, dbFilename);
profileSubdirModTime = new Date(((await IOUtils.stat(dbFile))).lastModified);
Zotero.debug(`Database found at ${dbFile}, last modified ${profileSubdirModTime}`);
let mtime = new Date(((await IOUtils.stat(dbFile))).lastModified);
Zotero.debug(`Database found at ${dbFile}, last modified ${mtime}`);
dataDir = dir;
useProfile = true;
}
catch (e) {
if (e.name != 'NotFoundError') {
@ -232,122 +222,6 @@ Zotero.DataDirectory = {
}
}
//
// Check Firefox directory
//
if (!dataDirNamedAfterProfile) {
// Get default profile in Firefox dir
let defProfile;
let profilesDir = Zotero.Profile.getOtherAppProfilesDir();
let profilesParent = profilesDir ? PathUtils.parent(profilesDir) : null;
if (profilesParent) {
Zotero.debug("Looking for Firefox profile in " + profilesParent);
try {
defProfile = await Zotero.Profile.getDefaultInProfilesDir(profilesParent);
}
catch (e) {
Zotero.debug("An error occurred locating the Firefox profile; "
+ "not attempting to migrate from Zotero for Firefox");
Zotero.logError(e);
Zotero.fxProfileAccessError = true;
}
}
if (defProfile) {
let profileDir = defProfile[0];
Zotero.debug("Found default profile at " + profileDir);
// Read in prefs
let prefsFile = OS.Path.join(profileDir, "prefs.js");
if (await OS.File.exists(prefsFile)) {
let prefs = await Zotero.Profile.readPrefsFromFile(prefsFile);
// Check for data dir pref
if (prefs['extensions.zotero.dataDir'] && prefs['extensions.zotero.useDataDir']) {
Zotero.debug(`Found custom dataDir of ${prefs['extensions.zotero.dataDir']}`);
let nsIFile;
try {
nsIFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsIFile);
nsIFile.persistentDescriptor = prefs['extensions.zotero.dataDir'];
}
catch (e) {
Zotero.logError(e);
if (!useProfile) {
let msg = "Persistent descriptor in extensions.zotero.dataDir "
+ "did not resolve";
Zotero.debug(msg, 1);
throw new DOMException(msg, 'NotFoundError');
}
}
try {
let dbFile = OS.Path.join(nsIFile.path, dbFilename);
let mtime = new Date(((await IOUtils.stat(dbFile))).lastModified);
Zotero.debug(`Database found at ${dbFile}, last modified ${mtime}`);
// If custom location has a newer DB, use that
if (!useProfile || mtime > profileSubdirModTime) {
dataDir = nsIFile.path;
useFirefoxProfileCustom = true;
useProfile = false;
}
}
catch (e) {
Zotero.logError(e);
// If we have a DB in the Zotero profile and get an error trying to
// access the custom location in Firefox, use the Zotero profile, since
// there's at least some chance it's right. Otherwise, throw an error.
if (!useProfile) {
// The error message normally gets the path from the pref, but
// we got it from the prefs file, so include it here
e.dataDir = nsIFile.path;
throw e;
}
Zotero.fxProfileAccessError = true;
}
}
// If no custom dir specified, check for a subdirectory
else {
try {
let dir = OS.Path.join(profileDir, this.legacyDirName);
let dbFile = OS.Path.join(dir, dbFilename);
let mtime = new Date(((await IOUtils.stat(dbFile))).lastModified);
Zotero.debug(`Database found at ${dbFile}, last modified ${mtime}`);
// If newer than Zotero profile directory, use this one
if (!useProfile || mtime > profileSubdirModTime) {
dataDir = dir;
useFirefoxProfile = true;
useProfile = false;
}
}
// Legacy subdirectory doesn't exist or there was a problem accessing it, so
// just fall through to default location
catch (e) {
if (e.name != 'NotFoundError') {
Zotero.logError(e);
Zotero.fxProfileAccessError = true;
}
}
}
// If using data directory from Zotero for Firefox, transfer those prefs, because
// the fact that that DB was more recent and wasn't set in the Zotero profile prefs
// means that they were using Firefox.
if (useFirefoxProfile || useFirefoxProfileCustom) {
for (let key in prefs) {
if (key.substr(0, ZOTERO_CONFIG.PREF_BRANCH.length) === ZOTERO_CONFIG.PREF_BRANCH
&& key !== "extensions.zotero.firstRun2") {
Zotero.Prefs.set(key.substr(ZOTERO_CONFIG.PREF_BRANCH.length), prefs[key]);
}
}
// If data directory setting was transferred, use that
if (Zotero.Prefs.get('useDataDir')) {
return this.init();
}
}
}
}
}
this.set(dataDir);
}
@ -720,110 +594,6 @@ Zotero.DataDirectory = {
},
// TODO: Remove after 5.0 upgrades
checkForLostLegacy: async function () {
var currentDir = this.dir;
if (currentDir != this.defaultDir) return;
if (Zotero.Prefs.get('ignoreLegacyDataDir.auto') || Zotero.Prefs.get('ignoreLegacyDataDir.explicit')) return;
try {
let profilesDir = Zotero.Profile.getOtherAppProfilesDir();
let profilesParent = profilesDir ? PathUtils.parent(profilesDir) : null;
if (!profilesParent) {
return;
}
Zotero.debug("Looking for Firefox profile in " + profilesParent);
// get default profile
var defProfile;
try {
defProfile = await Zotero.Profile.getDefaultInProfilesDir(profilesParent);
}
catch (e) {
Zotero.logError(e);
return;
}
if (!defProfile) {
return;
}
let profileDir = defProfile[0];
Zotero.debug("Found default profile at " + profileDir);
let dir;
let mtime;
try {
dir = OS.Path.join(profileDir, this.legacyDirName);
let dbFile = OS.Path.join(dir, this.getDatabaseFilename());
let info = await IOUtils.stat(dbFile);
if (info.size < 1200000) {
Zotero.debug(`Legacy database is ${info.size} bytes -- ignoring`);
Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
return;
}
mtime = new Date(info.lastModified);
if (mtime < new Date(2017, 6, 1)) {
Zotero.debug(`Legacy database was last modified on ${mtime.toString()} -- ignoring`);
Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
return;
}
Zotero.debug(`Legacy database found at ${dbFile}, last modified ${mtime}`);
}
catch (e) {
Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
if (e.name == 'NotFoundError') {
return;
}
throw e;
}
let ps = Services.prompt;
let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
let dontAskAgain = {};
let index = ps.confirmEx(null,
"Other Data Directory Found",
"Zotero found a previous data directory within your Firefox profile, "
+ `last modified on ${mtime.toLocaleDateString()}. `
+ "If items or files are missing from Zotero that were present in Zotero for Firefox, "
+ "your previous data directory may not have been properly migrated to the new default location "
+ `in ${this.defaultDir}.\n\n`
+ `Do you wish to continue using the current data directory or switch to the previous one?\n\n`
+ `If you switch, your current data directory will be moved to ${this.defaultDir + '-Old'}, `
+ `and the previous directory will be migrated to ${this.defaultDir}.`,
buttonFlags,
"Use Current Directory",
null,
"Switch to Previous Directory",
"Don\u0027t ask me again",
dontAskAgain
);
if (index == 1) {
return;
}
if (dontAskAgain.value) {
Zotero.Prefs.set('ignoreLegacyDataDir.explicit', true);
}
if (index == 0) {
return;
}
// Switch to previous directory
this.set(dir);
// Set a marker to rename the current ~/Zotero directory
try {
await Zotero.File.putContentsAsync(OS.Path.join(this.defaultDir, 'move-to-old'), '');
}
catch (e) {
Zotero.logError(e);
}
Zotero.Utilities.Internal.quit(true);
}
catch (e) {
Zotero.logError(e);
}
},
/**
* Determine if current data directory is in a legacy location
*/

View file

@ -91,47 +91,19 @@ Zotero.Profile = {
/**
* Get the path to the Firefox Profiles directory, which may or may not exist
*
* @return {String|null} - Path, or null if none due to filesystem location
*/
getOtherAppProfilesDir: function () {
var dir = PathUtils.parent(PathUtils.parent(PathUtils.parent(this.dir)));
if (dir === '' || dir == '.') {
return null;
}
if (Zotero.isWin) {
dir = OS.Path.join(PathUtils.parent(dir), "Mozilla", "Firefox");
}
else if (Zotero.isMac) {
dir = OS.Path.join(dir, "Firefox");
}
else {
dir = OS.Path.join(dir, ".mozilla", "firefox");
}
return OS.Path.join(dir, "Profiles");
},
/**
* Find other profile directories (for this app or the other app) using the given data directory
* Find other Zotero profile directories using the given data directory
*
* @param {String} dataDir
* @param {Boolean} [includeOtherApps=false] - Check Firefox profiles
* @return {String[]}
*/
findOtherProfilesUsingDataDirectory: async function (dataDir, includeOtherApps = true) {
let otherAppProfiles = includeOtherApps ? await this._findOtherAppProfiles() : [];
let otherProfiles = (await this._findOtherProfiles()).concat(otherAppProfiles);
findOtherProfilesUsingDataDirectory: async function (dataDir) {
let otherProfiles = await this._findOtherProfiles();
// First get profiles pointing at this directory
for (let i = 0; i < otherProfiles.length; i++) {
let dir = otherProfiles[i];
let prefs = await Zotero.File.getContentsAsync(OS.Path.join(dir, "prefs.js"));
prefs = prefs.trim().split(/(?:\r\n|\r|\n)/);
let keep = prefs.some(line => line.includes("extensions.zotero.useDataDir") && line.includes("true"))
&& prefs.some(line => line.match(/extensions\.zotero\.(lastD|d)ataDir/) && line.includes(dataDir));
if (!keep) {
@ -140,15 +112,6 @@ Zotero.Profile = {
}
}
// If the parent of the source directory is a profile directory from the other app, add that
// to the list, which addresses the situation where the source directory is a custom
// location for the current profile but is a default in the other app (meaning it wouldn't
// be added above).
let dataDirParent = PathUtils.parent(dataDir);
if (otherAppProfiles.includes(dataDirParent) && !otherProfiles.includes(dataDirParent)) {
otherProfiles.push(dataDirParent);
}
if (otherProfiles.length) {
Zotero.debug("Found other profiles pointing to " + dataDir);
Zotero.debug(otherProfiles);
@ -191,82 +154,6 @@ Zotero.Profile = {
},
/**
* @return {Boolean} - True if accessible or skipped, false if not
*/
checkFirefoxProfileAccess: async function () {
try {
let profilesDir = Zotero.Profile.getOtherAppProfilesDir();
if (!profilesDir) {
return true;
}
let profilesParent = PathUtils.parent(profilesDir);
Zotero.debug("Looking for Firefox profile in " + profilesParent);
let defProfile = await this.getDefaultInProfilesDir(profilesParent);
if (defProfile) {
let profileDir = defProfile[0];
Zotero.debug("Found default profile at " + profileDir);
let prefsFile = OS.Path.join(profileDir, "prefs.js");
await Zotero.File.getContentsAsync(prefsFile);
let dir = OS.Path.join(profileDir, Zotero.DataDirectory.legacyDirName);
Zotero.debug("Checking for 'zotero' subdirectory");
if ((await OS.File.stat(dir)).isDir) {
let dbFilename = Zotero.DataDirectory.getDatabaseFilename();
let dbFile = OS.Path.join(dir, dbFilename);
Zotero.debug("Checking database access within 'zotero' subdirectory");
(await OS.File.stat(dbFile)).lastModificationDate;
}
else {
Zotero.debug("'zotero' is not a directory!");
}
}
else {
Zotero.debug("No default profile found");
}
}
catch (e) {
if (e.name == 'NotFoundError' || (e instanceof OS.File.Error && e.becauseNoSuchFile)) {
return true;
}
Zotero.debug(e, 2)
return false
}
return true;
},
readPrefsFromFile: async function (prefsFile) {
var sandbox = new Components.utils.Sandbox("http://www.example.com/");
Components.utils.evalInSandbox(
"var prefs = {};"+
"function user_pref(key, val) {"+
"prefs[key] = val;"+
"}"
, sandbox);
(await Zotero.File.getContentsAsync(prefsFile))
.split(/\n/)
.filter((line) => {
// Strip comments
return !line.startsWith('#')
// Only process lines in our pref branch
&& line.includes(ZOTERO_CONFIG.PREF_BRANCH);
})
// Process each line individually
.forEach((line) => {
try {
Zotero.debug("Processing " + line);
Components.utils.evalInSandbox(line, sandbox);
}
catch (e) {
Zotero.logError("Error processing prefs line: " + line);
}
});
return sandbox.prefs;
},
//
// Private methods
//
@ -293,7 +180,7 @@ Zotero.Profile = {
/**
* Find other profile directories for this app (Firefox or Zotero)
* Find other Zotero profile directories
*
* @return {Promise<String[]>} - Array of paths
*/
@ -301,16 +188,5 @@ Zotero.Profile = {
var profileDir = this.dir;
var profilesDir = this.getProfilesDir();
return (await this._getProfilesInDir(profilesDir)).filter(dir => dir != profileDir);
},
/**
* Find profile directories for the other app (Firefox or Zotero)
*
* @return {Promise<String[]>} - Array of paths
*/
_findOtherAppProfiles: async function () {
var dir = this.getOtherAppProfilesDir();
return dir && (await OS.File.exists(dir)) ? this._getProfilesInDir(dir) : [];
}
};

View file

@ -373,11 +373,6 @@ const { CommandLineOptions } = ChromeUtils.importESModule("chrome://zotero/conte
if (this.skipLoading) {
return;
}
await Zotero.DataDirectory.checkForLostLegacy();
if (this.restarting) {
return;
}
}
// Make sure data directory isn't in Dropbox, etc.

View file

@ -807,55 +807,6 @@ var ZoteroPane = new function () {
searchBar.searchTextbox.select();
}, 1);
//
// TEMP: Remove after people are no longer upgrading from Zotero for Firefox
//
var showFxProfileWarning = false;
var pref = 'firstRun.skipFirefoxProfileAccessCheck';
if (Zotero.fxProfileAccessError != undefined && Zotero.fxProfileAccessError) {
showFxProfileWarning = true;
}
else if (!Zotero.Prefs.get(pref)) {
showFxProfileWarning = !await Zotero.Profile.checkFirefoxProfileAccess();
}
if (showFxProfileWarning) {
Zotero.uiReadyPromise.then(async function () {
await Zotero.Promise.delay(2000);
var ps = Services.prompt;
var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
var text = "Zotero was unable to access your Firefox profile to check for "
+ "existing Zotero data.\n\n"
+ "If youve upgraded from Zotero 4.0 for Firefox and dont see the data "
+ "you expect, it may be located elsewhere on your computer. "
+ "Click “More Information” for help restoring your previous data.\n\n"
+ "If youre new to Zotero, you can ignore this message.";
var url = 'https://www.zotero.org/support/kb/data_missing_after_zotero_5_upgrade';
var dontShowAgain = {};
let index = ps.confirmEx(null,
Zotero.getString('general.warning'),
text,
buttonFlags,
Zotero.getString('general.moreInformation'),
"Ignore",
null,
Zotero.getString('general.dontShowAgain'),
dontShowAgain
);
if (dontShowAgain.value) {
Zotero.Prefs.set(pref, true)
}
if (index == 0) {
this.loadURI(url);
}
}.bind(this));
}
// Once we successfully find it once, don't bother checking again
else {
Zotero.Prefs.set(pref, true);
}
if (Zotero.proxyFailure) {
try {
Zotero.Sync.Runner.updateIcons(Zotero.proxyFailure);
@ -880,9 +831,6 @@ var ZoteroPane = new function () {
else if (Zotero.Sync.Server.manualSyncRequired) {
Zotero.debug('Manual sync required -- skipping auto-sync', 4);
}
else if (showFxProfileWarning) {
Zotero.debug('Firefox profile access error -- skipping initial auto-sync', 4);
}
else {
startupSync = true;
Zotero.Sync.Runner.sync({

View file

@ -79,8 +79,12 @@ user_pref("loop.copy.ticket", 196);
await Zotero.File.putContentsAsync(prefsFile2, contents2);
await Zotero.File.putContentsAsync(prefsFile3, contents3);
var stub = sinon.stub(Zotero.Profile, "getOtherAppProfilesDir")
.returns(OS.Path.join(tmpDir, "Profiles"));
var stub = sinon.stub(Zotero.Profile, "_findOtherProfiles")
.returns([
OS.Path.join(tmpDir, "Profiles", profile1),
OS.Path.join(tmpDir, "Profiles", profile2),
OS.Path.join(tmpDir, "Profiles", profile3)
]);
var dirs = await Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir);
@ -89,37 +93,6 @@ user_pref("loop.copy.ticket", 196);
assert.sameMembers(dirs, [OS.Path.join(tmpDir, "Profiles", profile2)]);
assert.lengthOf(dirs, 1);
});
it("should find other-app profile with directory as a legacy default location", async function () {
let contents1 = `user_pref("extensions.lastAppVersion", "49.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let contents2 = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let prefsFile1 = OS.Path.join(tmpDir, "Profiles", profile1, "prefs.js");
let prefsFile2 = OS.Path.join(tmpDir, "Profiles", profile2, "prefs.js");
await Zotero.File.putContentsAsync(prefsFile1, contents1);
await Zotero.File.putContentsAsync(prefsFile2, contents2);
var stub = sinon.stub(Zotero.Profile, "getOtherAppProfilesDir")
.returns(OS.Path.join(tmpDir, "Profiles"));
var dirs = await Zotero.Profile.findOtherProfilesUsingDataDirectory(
OS.Path.join(OS.Path.dirname(prefsFile1), Zotero.DataDirectory.legacyDirName)
);
stub.restore();
assert.sameMembers(dirs, [OS.Path.join(tmpDir, "Profiles", profile1)]);
assert.lengthOf(dirs, 1);
});
});