ChromeMigrationUtils.sys.mjs (17689B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 11 MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", 12 }); 13 14 const S100NS_FROM1601TO1970 = 0x19db1ded53e8000; 15 const S100NS_PER_MS = 10; 16 17 export var ChromeMigrationUtils = { 18 // Supported browsers with importable logins. 19 CONTEXTUAL_LOGIN_IMPORT_BROWSERS: ["chrome", "chromium-edge", "chromium"], 20 21 _extensionVersionDirectoryNames: {}, 22 23 // The cache for the locale strings. 24 // For example, the data could be: 25 // { 26 // "profile-id-1": { 27 // "extension-id-1": { 28 // "name": { 29 // "message": "Fake App 1" 30 // } 31 // }, 32 // } 33 _extensionLocaleStrings: {}, 34 35 get supportsLoginsForPlatform() { 36 return ["macosx", "win"].includes(AppConstants.platform); 37 }, 38 39 /** 40 * Get all extensions installed in a specific profile. 41 * 42 * @param {string} profileId - A Chrome user profile ID. For example, "Profile 1". 43 * @returns {Array} All installed Chrome extensions information. 44 */ 45 async getExtensionList(profileId) { 46 if (profileId === undefined) { 47 profileId = await this.getLastUsedProfileId(); 48 } 49 let path = await this.getExtensionPath(profileId); 50 let extensionList = []; 51 try { 52 for (const child of await IOUtils.getChildren(path)) { 53 const info = await IOUtils.stat(child); 54 if (info.type === "directory") { 55 const name = PathUtils.filename(child); 56 let extensionInformation = await this.getExtensionInformation( 57 name, 58 profileId 59 ); 60 if (extensionInformation) { 61 extensionList.push(extensionInformation); 62 } 63 } 64 } 65 } catch (ex) { 66 console.error(ex); 67 } 68 return extensionList; 69 }, 70 71 /** 72 * Get information of a specific Chrome extension. 73 * 74 * @param {string} extensionId - The extension ID. 75 * @param {string} profileId - The user profile's ID. 76 * @returns {object} The Chrome extension information. 77 */ 78 async getExtensionInformation(extensionId, profileId) { 79 if (profileId === undefined) { 80 profileId = await this.getLastUsedProfileId(); 81 } 82 let extensionInformation = null; 83 try { 84 let manifestPath = await this.getExtensionPath(profileId); 85 manifestPath = PathUtils.join(manifestPath, extensionId); 86 // If there are multiple sub-directories in the extension directory, 87 // read the files in the latest directory. 88 let directories = 89 await this._getSortedByVersionSubDirectoryNames(manifestPath); 90 if (!directories[0]) { 91 return null; 92 } 93 94 manifestPath = PathUtils.join( 95 manifestPath, 96 directories[0], 97 "manifest.json" 98 ); 99 let manifest = await IOUtils.readJSON(manifestPath); 100 // No app attribute means this is a Chrome extension not a Chrome app. 101 if (!manifest.app) { 102 const DEFAULT_LOCALE = manifest.default_locale; 103 let name = await this._getLocaleString( 104 manifest.name, 105 DEFAULT_LOCALE, 106 extensionId, 107 profileId 108 ); 109 let description = await this._getLocaleString( 110 manifest.description, 111 DEFAULT_LOCALE, 112 extensionId, 113 profileId 114 ); 115 if (name) { 116 extensionInformation = { 117 id: extensionId, 118 name, 119 description, 120 }; 121 } else { 122 throw new Error("Cannot read the Chrome extension's name property."); 123 } 124 } 125 } catch (ex) { 126 console.error(ex); 127 } 128 return extensionInformation; 129 }, 130 131 /** 132 * Get the manifest's locale string. 133 * 134 * @param {string} key - The key of a locale string, for example __MSG_name__. 135 * @param {string} locale - The specific language of locale string. 136 * @param {string} extensionId - The extension ID. 137 * @param {string} profileId - The user profile's ID. 138 * @returns {string|null} The locale string. 139 */ 140 async _getLocaleString(key, locale, extensionId, profileId) { 141 if (typeof key !== "string") { 142 console.debug("invalid manifest key"); 143 return null; 144 } 145 // Return the key string if it is not a locale key. 146 // The key string starts with "__MSG_" and ends with "__". 147 // For example, "__MSG_name__". 148 // https://developer.chrome.com/apps/i18n 149 if (!key.startsWith("__MSG_") || !key.endsWith("__")) { 150 return key; 151 } 152 153 let localeString = null; 154 try { 155 let localeFile; 156 if ( 157 this._extensionLocaleStrings[profileId] && 158 this._extensionLocaleStrings[profileId][extensionId] 159 ) { 160 localeFile = this._extensionLocaleStrings[profileId][extensionId]; 161 } else { 162 if (!this._extensionLocaleStrings[profileId]) { 163 this._extensionLocaleStrings[profileId] = {}; 164 } 165 let localeFilePath = await this.getExtensionPath(profileId); 166 localeFilePath = PathUtils.join(localeFilePath, extensionId); 167 let directories = 168 await this._getSortedByVersionSubDirectoryNames(localeFilePath); 169 // If there are multiple sub-directories in the extension directory, 170 // read the files in the latest directory. 171 localeFilePath = PathUtils.join( 172 localeFilePath, 173 directories[0], 174 "_locales", 175 locale, 176 "messages.json" 177 ); 178 localeFile = await IOUtils.readJSON(localeFilePath); 179 this._extensionLocaleStrings[profileId][extensionId] = localeFile; 180 } 181 const PREFIX_LENGTH = 6; 182 const SUFFIX_LENGTH = 2; 183 // Get the locale key from the string with locale prefix and suffix. 184 // For example, it will get the "name" sub-string from the "__MSG_name__" string. 185 key = key.substring(PREFIX_LENGTH, key.length - SUFFIX_LENGTH); 186 if (localeFile[key] && localeFile[key].message) { 187 localeString = localeFile[key].message; 188 } 189 } catch (ex) { 190 console.error(ex); 191 } 192 return localeString; 193 }, 194 195 /** 196 * Check that a specific extension is installed or not. 197 * 198 * @param {string} extensionId - The extension ID. 199 * @param {string} profileId - The user profile's ID. 200 * @returns {boolean} Return true if the extension is installed otherwise return false. 201 */ 202 async isExtensionInstalled(extensionId, profileId) { 203 if (profileId === undefined) { 204 profileId = await this.getLastUsedProfileId(); 205 } 206 let extensionPath = await this.getExtensionPath(profileId); 207 let isInstalled = await IOUtils.exists( 208 PathUtils.join(extensionPath, extensionId) 209 ); 210 return isInstalled; 211 }, 212 213 /** 214 * Get the last used user profile's ID. 215 * 216 * @returns {string} The last used user profile's ID. 217 */ 218 async getLastUsedProfileId() { 219 let localState = await this.getLocalState(); 220 return localState ? localState.profile.last_used : "Default"; 221 }, 222 223 /** 224 * Get the local state file content. 225 * 226 * @param {string} chromeProjectName 227 * The type of Chrome data we're looking for (Chromium, Canary, etc.) 228 * @param {string} [dataPath=undefined] 229 * The data path that should be used as the parent directory when getting 230 * the local state. If not supplied, the data path is calculated using 231 * getDataPath and the chromeProjectName. 232 * @returns {object} The JSON-based content. 233 */ 234 async getLocalState(chromeProjectName = "Chrome", dataPath) { 235 let localState = null; 236 try { 237 if (!dataPath) { 238 dataPath = await this.getDataPath(chromeProjectName); 239 } 240 let localStatePath = PathUtils.join(dataPath, "Local State"); 241 localState = JSON.parse(await IOUtils.readUTF8(localStatePath)); 242 } catch (ex) { 243 // Don't report the error if it's just a file not existing. 244 if (ex.name != "NotFoundError") { 245 console.error(ex); 246 } 247 throw ex; 248 } 249 return localState; 250 }, 251 252 /** 253 * Get the path of Chrome extension directory. 254 * 255 * @param {string} profileId - The user profile's ID. 256 * @returns {string} The path of Chrome extension directory. 257 */ 258 async getExtensionPath(profileId) { 259 return PathUtils.join(await this.getDataPath(), profileId, "Extensions"); 260 }, 261 262 /** 263 * Get the path of an application data directory. 264 * 265 * @param {string} chromeProjectName - The Chrome project name, e.g. "Chrome", "Canary", etc. 266 * Defaults to "Chrome". 267 * @returns {string} The path of application data directory. 268 */ 269 async getDataPath(chromeProjectName = "Chrome") { 270 const SNAP_REAL_HOME = "SNAP_REAL_HOME"; 271 272 const SUB_DIRECTORIES = { 273 win: { 274 Brave: [ 275 ["LocalAppData", "BraveSoftware", "Brave-Browser", "User Data"], 276 ], 277 Chrome: [["LocalAppData", "Google", "Chrome", "User Data"]], 278 "Chrome Beta": [["LocalAppData", "Google", "Chrome Beta", "User Data"]], 279 Chromium: [["LocalAppData", "Chromium", "User Data"]], 280 Canary: [["LocalAppData", "Google", "Chrome SxS", "User Data"]], 281 Edge: [["LocalAppData", "Microsoft", "Edge", "User Data"]], 282 "Edge Beta": [["LocalAppData", "Microsoft", "Edge Beta", "User Data"]], 283 "360 SE": [["AppData", "360se6", "User Data"]], 284 Opera: [["AppData", "Opera Software", "Opera Stable"]], 285 "Opera GX": [["AppData", "Opera Software", "Opera GX Stable"]], 286 Vivaldi: [["LocalAppData", "Vivaldi", "User Data"]], 287 }, 288 macosx: { 289 Brave: [ 290 ["ULibDir", "Application Support", "BraveSoftware", "Brave-Browser"], 291 ], 292 Chrome: [["ULibDir", "Application Support", "Google", "Chrome"]], 293 Chromium: [["ULibDir", "Application Support", "Chromium"]], 294 Canary: [["ULibDir", "Application Support", "Google", "Chrome Canary"]], 295 Edge: [["ULibDir", "Application Support", "Microsoft Edge"]], 296 "Edge Beta": [ 297 ["ULibDir", "Application Support", "Microsoft Edge Beta"], 298 ], 299 "Opera GX": [ 300 ["ULibDir", "Application Support", "com.operasoftware.OperaGX"], 301 ], 302 Opera: [["ULibDir", "Application Support", "com.operasoftware.Opera"]], 303 Vivaldi: [["ULibDir", "Application Support", "Vivaldi"]], 304 }, 305 linux: { 306 Brave: [["Home", ".config", "BraveSoftware", "Brave-Browser"]], 307 Chrome: [["Home", ".config", "google-chrome"]], 308 "Chrome Beta": [["Home", ".config", "google-chrome-beta"]], 309 "Chrome Dev": [["Home", ".config", "google-chrome-unstable"]], 310 Chromium: [ 311 ["Home", ".config", "chromium"], 312 313 // If we're installed normally, we can look for Chromium installed 314 // as a Snap on Ubuntu Linux by looking here. 315 ["Home", "snap", "chromium", "common", "chromium"], 316 317 // If we're installed as a Snap, "Home" is a special place that 318 // the Snap environment has given us, and the Chromium data is 319 // not within it. We want to, instead, start at the path set 320 // on the environment variable "SNAP_REAL_HOME". 321 // See: https://snapcraft.io/docs/environment-variables#heading--snap-real-home 322 [SNAP_REAL_HOME, "snap", "chromium", "common", "chromium"], 323 ], 324 // Opera GX is not available on Linux. 325 // Canary is not available on Linux. 326 // Edge is not available on Linux. 327 Opera: [["Home", ".config", "opera"]], 328 Vivaldi: [["Home", ".config", "vivaldi"]], 329 }, 330 }; 331 let options = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName]; 332 if (!options) { 333 return null; 334 } 335 336 for (let subfolders of options) { 337 let rootDir = subfolders[0]; 338 try { 339 let targetPath; 340 341 if (rootDir == SNAP_REAL_HOME) { 342 targetPath = Services.env.get("SNAP_REAL_HOME"); 343 } else if (rootDir === "Home" && Services.env.get("BB_ORIGINAL_HOME")) { 344 targetPath = Services.env.get("BB_ORIGINAL_HOME"); 345 } else { 346 targetPath = Services.dirsvc.get(rootDir, Ci.nsIFile).path; 347 } 348 349 targetPath = PathUtils.join(targetPath, ...subfolders.slice(1)); 350 if (await IOUtils.exists(targetPath)) { 351 return targetPath; 352 } 353 } catch (ex) { 354 // The path logic here shouldn't error, so log it: 355 console.error(ex); 356 } 357 } 358 return null; 359 }, 360 361 /** 362 * Get the directory objects sorted by version number. 363 * 364 * @param {string} path - The path to the extension directory. 365 * otherwise return all file/directory object. 366 * @returns {Array} The file/directory object array. 367 */ 368 async _getSortedByVersionSubDirectoryNames(path) { 369 if (this._extensionVersionDirectoryNames[path]) { 370 return this._extensionVersionDirectoryNames[path]; 371 } 372 373 let entries = []; 374 try { 375 for (const child of await IOUtils.getChildren(path)) { 376 const info = await IOUtils.stat(child); 377 if (info.type === "directory") { 378 const name = PathUtils.filename(child); 379 entries.push(name); 380 } 381 } 382 } catch (ex) { 383 console.error(ex); 384 entries = []; 385 } 386 387 // The directory name is the version number string of the extension. 388 // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2. 389 // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again. 390 // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc 391 entries.sort((a, b) => Services.vc.compare(b, a)); 392 393 this._extensionVersionDirectoryNames[path] = entries; 394 return entries; 395 }, 396 397 /** 398 * Convert Chrome time format to Date object. Google Chrome uses FILETIME / 10 as time. 399 * FILETIME is based on the same structure of Windows. 400 * 401 * @param {number} aTime Chrome time 402 * @param {string|number|Date} aFallbackValue a date or timestamp (valid argument 403 * for the Date constructor) that will be used if the chrometime value passed is 404 * invalid. 405 * @returns {Date} converted Date object 406 */ 407 chromeTimeToDate(aTime, aFallbackValue) { 408 // The date value may be 0 in some cases. Because of the subtraction below, 409 // that'd generate a date before the unix epoch, which can upset consumers 410 // due to the unix timestamp then being negative. Catch this case: 411 if (!aTime) { 412 return new Date(aFallbackValue); 413 } 414 return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000); 415 }, 416 417 /** 418 * Convert Date object to Chrome time format. For details on Chrome time, see 419 * chromeTimeToDate. 420 * 421 * @param {Date|number} aDate Date object or integer equivalent 422 * @returns {number} Chrome time 423 */ 424 dateToChromeTime(aDate) { 425 return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS; 426 }, 427 428 /** 429 * Returns an array of chromium browser ids that have importable logins. 430 */ 431 _importableLoginsCache: null, 432 async getImportableLogins(formOrigin) { 433 // Only provide importable if we actually support importing. 434 if (!this.supportsLoginsForPlatform) { 435 return undefined; 436 } 437 438 // Lazily fill the cache with all importable login browsers. 439 if (!this._importableLoginsCache) { 440 this._importableLoginsCache = new Map(); 441 442 // Just handle these chromium-based browsers for now. 443 for (const browserId of this.CONTEXTUAL_LOGIN_IMPORT_BROWSERS) { 444 // Skip if there's no profile data. 445 const migrator = await lazy.MigrationUtils.getMigrator(browserId); 446 if (!migrator) { 447 continue; 448 } 449 450 // Check each profile for logins. 451 const dataPath = await migrator._getChromeUserDataPathIfExists(); 452 for (const profile of await migrator.getSourceProfiles()) { 453 const path = PathUtils.join(dataPath, profile.id, "Login Data"); 454 // Skip if login data is missing. 455 if (!(await IOUtils.exists(path))) { 456 console.error(`Missing file at ${path}`); 457 continue; 458 } 459 460 try { 461 for (const row of await lazy.MigrationUtils.getRowsFromDBWithoutLocks( 462 path, 463 `Importable ${browserId} logins`, 464 `SELECT origin_url 465 FROM logins 466 WHERE blacklisted_by_user = 0` 467 )) { 468 const url = row.getString(0); 469 try { 470 // Initialize an array if it doesn't exist for the origin yet. 471 const origin = lazy.LoginHelper.getLoginOrigin(url); 472 const entries = this._importableLoginsCache.get(origin) || []; 473 if (!entries.length) { 474 this._importableLoginsCache.set(origin, entries); 475 } 476 477 // Add the browser if it doesn't exist yet. 478 if (!entries.includes(browserId)) { 479 entries.push(browserId); 480 } 481 } catch (ex) { 482 console.error( 483 `Failed to process importable url ${url} from ${browserId}`, 484 ex 485 ); 486 } 487 } 488 } catch (ex) { 489 console.error( 490 `Failed to get importable logins from ${browserId}`, 491 ex 492 ); 493 } 494 } 495 } 496 } 497 return this._importableLoginsCache.get(formOrigin); 498 }, 499 };