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