profile.js (9924B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2016 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 "use strict"; 27 28 Components.utils.import("resource://gre/modules/osfile.jsm"); 29 30 Zotero.Profile = { 31 dir: OS.Constants.Path.profileDir, 32 33 getDefaultInProfilesDir: Zotero.Promise.coroutine(function* (profilesDir) { 34 var profilesIni = OS.Path.join(profilesDir, "profiles.ini"); 35 36 try { 37 var iniContents = yield Zotero.File.getContentsAsync(profilesIni); 38 } 39 catch (e) { 40 if (e instanceof OS.File.Error && e.becauseNoSuchFile) { 41 return false; 42 } 43 throw e; 44 } 45 46 // cheap and dirty ini parser 47 var curSection = null; 48 var defaultSection = null; 49 var nSections = 0; 50 for (let line of iniContents.split(/(?:\r?\n|\r)/)) { 51 let tline = line.trim(); 52 if(tline[0] == "[" && tline[tline.length-1] == "]") { 53 curSection = {}; 54 if(tline != "[General]") nSections++; 55 } else if(curSection && tline != "") { 56 let equalsIndex = tline.indexOf("="); 57 let key = tline.substr(0, equalsIndex); 58 let val = tline.substr(equalsIndex+1); 59 curSection[key] = val; 60 if(key == "Default" && val == "1") { 61 defaultSection = curSection; 62 } 63 } 64 } 65 if (!defaultSection && curSection) defaultSection = curSection; 66 67 if (!defaultSection || !defaultSection.Path) return false; 68 69 var defaultProfile = defaultSection.IsRelative === "1" 70 ? OS.Path.join(profilesDir, ...defaultSection.Path.split("/")) 71 : defaultSection.Path; 72 73 try { 74 // Note: exists() returns false on no access, so use stat() instead 75 yield OS.File.stat(defaultProfile); 76 } 77 catch (e) { 78 if (e instanceof OS.File.Error) { 79 if (e.becauseNoSuchFile) { 80 return false; 81 } 82 throw e; 83 } 84 } 85 return [defaultProfile, nSections > 1]; 86 }), 87 88 89 getProfilesDir: function () { 90 return OS.Path.dirname(this.dir); 91 }, 92 93 94 /** 95 * Get the path to the Profiles directory of the other app from this one (Firefox or Zotero), 96 * which may or may not exist 97 * 98 * @return {String} - Path 99 */ 100 getOtherAppProfilesDir: function () { 101 var dir = OS.Path.dirname(OS.Path.dirname(OS.Path.dirname(this.dir))); 102 103 if (Zotero.isStandalone) { 104 if (Zotero.isWin) { 105 dir = OS.Path.join(OS.Path.dirname(dir), "Mozilla", "Firefox"); 106 } 107 else if (Zotero.isMac) { 108 dir = OS.Path.join(dir, "Firefox"); 109 } 110 else { 111 dir = OS.Path.join(dir, ".mozilla", "firefox"); 112 } 113 } 114 else { 115 if (Zotero.isWin) { 116 dir = OS.Path.join(OS.Path.dirname(dir), "Zotero", "Zotero"); 117 } 118 else if (Zotero.isMac) { 119 dir = OS.Path.join(dir, "Zotero"); 120 } else { 121 dir = OS.Path.join(dir, ".zotero", "zotero"); 122 } 123 } 124 125 return OS.Path.join(dir, "Profiles"); 126 }, 127 128 129 /** 130 * Find other profile directories (for this app or the other app) using the given data directory 131 * 132 * @param {String} dataDir 133 * @param {Boolean} [includeOtherApps=false] - Check Firefox profiles 134 * @return {String[]} 135 */ 136 findOtherProfilesUsingDataDirectory: Zotero.Promise.coroutine(function* (dataDir, includeOtherApps = true) { 137 let otherAppProfiles = includeOtherApps ? (yield this._findOtherAppProfiles()) : []; 138 let otherProfiles = (yield this._findOtherProfiles()).concat(otherAppProfiles); 139 140 // First get profiles pointing at this directory 141 otherProfiles = yield Zotero.Promise.filter(otherProfiles, Zotero.Promise.coroutine(function* (dir) { 142 let prefs = yield Zotero.File.getContentsAsync(OS.Path.join(dir, "prefs.js")); 143 prefs = prefs.trim().split(/(?:\r\n|\r|\n)/); 144 145 return prefs.some(line => { 146 return line.includes("extensions.zotero.useDataDir") && line.includes("true"); 147 }) && prefs.some(line => { 148 return line.match(/extensions\.zotero\.(lastD|d)ataDir/) && line.includes(dataDir) 149 }); 150 })); 151 152 // If the parent of the source directory is a profile directory from the other app, add that 153 // to the list, which addresses the situation where the source directory is a custom 154 // location for the current profile but is a default in the other app (meaning it wouldn't 155 // be added above). 156 let dataDirParent = OS.Path.dirname(dataDir); 157 if (otherAppProfiles.includes(dataDirParent) && !otherProfiles.includes(dataDirParent)) { 158 otherProfiles.push(dataDirParent); 159 } 160 161 if (otherProfiles.length) { 162 Zotero.debug("Found other profiles pointing to " + dataDir); 163 Zotero.debug(otherProfiles); 164 } 165 else { 166 Zotero.debug("No other profiles point to " + dataDir); 167 } 168 169 return otherProfiles; 170 }), 171 172 173 updateProfileDataDirectory: Zotero.Promise.coroutine(function* (profileDir, oldDir, newDir) { 174 let prefsFile = OS.Path.join(profileDir, "prefs.js"); 175 let prefsFileTmp = OS.Path.join(profileDir, "prefs.js.tmp"); 176 Zotero.debug("Updating " + prefsFile + " to point to new data directory"); 177 let contents = yield Zotero.File.getContentsAsync(prefsFile); 178 contents = contents 179 .trim() 180 .split(/(?:\r\n|\r|\n)/) 181 // Remove existing lines 182 .filter(line => !line.match(/extensions\.zotero\.(useD|lastD|d)ataDir/)); 183 // Shouldn't happen, but let's make sure we don't corrupt the prefs file 184 let safeVal = newDir.replace(/["]/g, ""); 185 contents.push( 186 `user_pref("extensions.zotero.dataDir", "${safeVal}");`, 187 `user_pref("extensions.zotero.lastDataDir", "${safeVal}");`, 188 'user_pref("extensions.zotero.useDataDir", true);' 189 ); 190 let lineSep = Zotero.isWin ? "\r\n" : "\n"; 191 contents = contents.join(lineSep) + lineSep; 192 yield OS.File.writeAtomic( 193 prefsFile, 194 contents, 195 { 196 tmpPath: prefsFileTmp, 197 encoding: 'utf-8' 198 } 199 ); 200 }), 201 202 203 /** 204 * @return {Boolean} - True if accessible or skipped, false if not 205 */ 206 checkFirefoxProfileAccess: async function () { 207 try { 208 let profilesParent = OS.Path.dirname(Zotero.Profile.getOtherAppProfilesDir()); 209 Zotero.debug("Looking for Firefox profile in " + profilesParent); 210 let defProfile = await this.getDefaultInProfilesDir(profilesParent); 211 if (defProfile) { 212 let profileDir = defProfile[0]; 213 Zotero.debug("Found default profile at " + profileDir); 214 let prefsFile = OS.Path.join(profileDir, "prefs.js"); 215 await Zotero.File.getContentsAsync(prefsFile); 216 let dir = OS.Path.join(profileDir, Zotero.DataDirectory.legacyDirName); 217 Zotero.debug("Checking for 'zotero' subdirectory"); 218 if ((await OS.File.stat(dir)).isDir) { 219 let dbFilename = Zotero.DataDirectory.getDatabaseFilename(); 220 let dbFile = OS.Path.join(dir, dbFilename); 221 Zotero.debug("Checking database access within 'zotero' subdirectory"); 222 (await OS.File.stat(dbFile)).lastModificationDate; 223 } 224 else { 225 Zotero.debug("'zotero' is not a directory!"); 226 } 227 } 228 else { 229 Zotero.debug("No default profile found"); 230 } 231 } 232 catch (e) { 233 if (e instanceof OS.File.Error && e.becauseNoSuchFile) { 234 return true; 235 } 236 Zotero.debug(e, 2) 237 return false 238 } 239 return true; 240 }, 241 242 243 readPrefsFromFile: async function (prefsFile) { 244 var sandbox = new Components.utils.Sandbox("http://www.example.com/"); 245 Components.utils.evalInSandbox( 246 "var prefs = {};"+ 247 "function user_pref(key, val) {"+ 248 "prefs[key] = val;"+ 249 "}" 250 , sandbox); 251 252 (await Zotero.File.getContentsAsync(prefsFile)) 253 .split(/\n/) 254 .filter((line) => { 255 // Strip comments 256 return !line.startsWith('#') 257 // Only process lines in our pref branch 258 && line.includes(ZOTERO_CONFIG.PREF_BRANCH); 259 }) 260 // Process each line individually 261 .forEach((line) => { 262 try { 263 Zotero.debug("Processing " + line); 264 Components.utils.evalInSandbox(line, sandbox); 265 } 266 catch (e) { 267 Zotero.logError("Error processing prefs line: " + line); 268 } 269 }); 270 271 return sandbox.prefs; 272 }, 273 274 275 // 276 // Private methods 277 // 278 279 /** 280 * Get all profile directories within the given directory 281 * 282 * @return {String[]} - Array of paths 283 */ 284 _getProfilesInDir: Zotero.Promise.coroutine(function* (profilesDir) { 285 var dirs = []; 286 yield Zotero.File.iterateDirectory(profilesDir, function* (iterator) { 287 while (true) { 288 let entry = yield iterator.next(); 289 // entry.isDir can be false for some reason on Travis, causing spurious test failures 290 if (Zotero.automatedTest && !entry.isDir && (yield OS.File.stat(entry.path)).isDir) { 291 Zotero.debug("Overriding isDir for " + entry.path); 292 entry.isDir = true; 293 } 294 if (entry.isDir && (yield OS.File.exists(OS.Path.join(entry.path, "prefs.js")))) { 295 dirs.push(entry.path); 296 } 297 } 298 }); 299 return dirs; 300 }), 301 302 303 /** 304 * Find other profile directories for this app (Firefox or Zotero) 305 * 306 * @return {String[]} - Array of paths 307 */ 308 _findOtherProfiles: Zotero.Promise.coroutine(function* () { 309 var profileDir = this.dir; 310 var profilesDir = this.getProfilesDir(); 311 return this._getProfilesInDir(profilesDir).filter(dir => dir != profileDir); 312 }), 313 314 315 /** 316 * Find profile directories for the other app (Firefox or Zotero) 317 * 318 * @return {String[]} - Array of paths 319 */ 320 _findOtherAppProfiles: Zotero.Promise.coroutine(function* () { 321 var dir = this.getOtherAppProfilesDir(); 322 return (yield OS.File.exists(dir)) ? this._getProfilesInDir(dir) : []; 323 }) 324 };