diff options
Diffstat (limited to 'browser/components/migration/360seProfileMigrator.jsm')
-rw-r--r-- | browser/components/migration/360seProfileMigrator.jsm | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/browser/components/migration/360seProfileMigrator.jsm b/browser/components/migration/360seProfileMigrator.jsm new file mode 100644 index 0000000000..940236eff9 --- /dev/null +++ b/browser/components/migration/360seProfileMigrator.jsm @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { FileUtils } = ChromeUtils.import( + "resource://gre/modules/FileUtils.jsm" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { MigrationUtils, MigratorPrototype } = ChromeUtils.import( + "resource:///modules/MigrationUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PlacesUIUtils", + "resource:///modules/PlacesUIUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Sqlite", + "resource://gre/modules/Sqlite.jsm" +); + +XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); + +const kBookmarksFileName = "360sefav.db"; + +function copyToTempUTF8File(file, charset) { + let inputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + inputStream.init(file, -1, -1, 0); + let inputStr = NetUtil.readInputStreamToString( + inputStream, + inputStream.available(), + { charset } + ); + + // Use random to reduce the likelihood of a name collision in createUnique. + let rand = Math.floor(Math.random() * Math.pow(2, 15)); + let leafName = "mozilla-temp-" + rand; + let tempUTF8File = FileUtils.getFile( + "TmpD", + ["mozilla-temp-files", leafName], + true + ); + tempUTF8File.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + let out = FileUtils.openAtomicFileOutputStream(tempUTF8File); + try { + let bufferedOut = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + bufferedOut.init(out, 4096); + try { + let converterOut = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + converterOut.init(bufferedOut, "utf-8"); + try { + converterOut.writeString(inputStr || ""); + bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish(); + } finally { + converterOut.close(); + } + } finally { + bufferedOut.close(); + } + } finally { + out.close(); + } + + return tempUTF8File; +} + +function parseINIStrings(file) { + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let parser = factory.createINIParser(file); + let obj = {}; + for (let section of parser.getSections()) { + obj[section] = {}; + + for (let key of parser.getKeys(section)) { + obj[section][key] = parser.getString(section, key); + } + } + return obj; +} + +function getHash(aStr) { + // return the two-digit hexadecimal code for a byte + let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); + + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(Ci.nsICryptoHash.MD5); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stringStream.data = aStr; + hasher.updateFromStream(stringStream, -1); + + // convert the binary hash data to a hex string. + let binary = hasher.finish(false); + return Array.from(binary, (c, i) => toHexString(binary.charCodeAt(i))) + .join("") + .toLowerCase(); +} + +function Bookmarks(aProfileFolder) { + let file = aProfileFolder.clone(); + file.append(kBookmarksFileName); + + this._file = file; +} +Bookmarks.prototype = { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + get exists() { + return this._file.exists() && this._file.isReadable(); + }, + + migrate(aCallback) { + return (async () => { + let folderMap = new Map(); + let toolbarBMs = []; + + let connection = await Sqlite.openConnection({ + path: this._file.path, + }); + + let histogramBookmarkRoots = 0; + try { + let rows = await connection.execute( + `WITH RECURSIVE + bookmark(id, parent_id, is_folder, title, url, pos) AS ( + VALUES(0, -1, 1, '', '', 0) + UNION + SELECT f.id, f.parent_id, f.is_folder, f.title, f.url, f.pos + FROM tb_fav AS f + JOIN bookmark AS b ON f.parent_id = b.id + ORDER BY f.pos ASC + ) + SELECT id, parent_id, is_folder, title, url FROM bookmark WHERE id` + ); + + for (let row of rows) { + let id = parseInt(row.getResultByName("id"), 10); + let parent_id = parseInt(row.getResultByName("parent_id"), 10); + let is_folder = parseInt(row.getResultByName("is_folder"), 10); + let title = row.getResultByName("title"); + let url = row.getResultByName("url"); + + let bmToInsert; + + if (is_folder) { + bmToInsert = { + children: [], + title, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }; + folderMap.set(id, bmToInsert); + } else { + try { + new URL(url); + } catch (ex) { + Cu.reportError( + `Ignoring ${url} when importing from 360se because of exception: ${ex}` + ); + continue; + } + + bmToInsert = { + title, + url, + }; + } + + if (folderMap.has(parent_id)) { + folderMap.get(parent_id).children.push(bmToInsert); + } else if (parent_id === 0) { + toolbarBMs.push(bmToInsert); + } + } + } finally { + await connection.close(); + } + + if (toolbarBMs.length) { + histogramBookmarkRoots |= + MigrationUtils.SOURCE_BOOKMARK_ROOTS_BOOKMARKS_TOOLBAR; + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + if ( + !Services.prefs.getBoolPref("browser.toolbars.bookmarks.2h2020") && + !MigrationUtils.isStartupMigration && + PlacesUtils.getChildCountForFolder( + PlacesUtils.bookmarks.toolbarGuid + ) > PlacesUIUtils.NUM_TOOLBAR_BOOKMARKS_TO_UNHIDE + ) { + parentGuid = await MigrationUtils.createImportedBookmarksFolder( + "360se", + parentGuid + ); + } + await MigrationUtils.insertManyBookmarksWrapper(toolbarBMs, parentGuid); + PlacesUIUtils.maybeToggleBookmarkToolbarVisibilityAfterMigration(); + } + Services.telemetry + .getKeyedHistogramById("FX_MIGRATION_BOOKMARKS_ROOTS") + .add("360se", histogramBookmarkRoots); + })().then( + () => aCallback(true), + e => { + Cu.reportError(e); + aCallback(false); + } + ); + }, +}; + +function Qihoo360seProfileMigrator() { + let paths = [ + // for v6 and above + { + users: ["360se6", "apps", "data", "users"], + defaultUser: "default", + }, + // for earlier versions + { + users: ["360se"], + defaultUser: "data", + }, + ]; + this._usersDir = null; + this._defaultUserPath = null; + for (let path of paths) { + let usersDir = FileUtils.getDir("AppData", path.users, false); + if (usersDir.exists()) { + this._usersDir = usersDir; + this._defaultUserPath = path.defaultUser; + break; + } + } +} + +Qihoo360seProfileMigrator.prototype = Object.create(MigratorPrototype); + +Qihoo360seProfileMigrator.prototype.getSourceProfiles = function() { + if ("__sourceProfiles" in this) { + return this.__sourceProfiles; + } + + if (!this._usersDir) { + this.__sourceProfiles = []; + return this.__sourceProfiles; + } + + let profiles = []; + let noLoggedInUser = true; + try { + let loginIni = this._usersDir.clone(); + loginIni.append("login.ini"); + if (!loginIni.exists()) { + throw new Error("360 Secure Browser's 'login.ini' does not exist."); + } + if (!loginIni.isReadable()) { + throw new Error( + "360 Secure Browser's 'login.ini' file could not be read." + ); + } + + let loginIniInUtf8 = copyToTempUTF8File(loginIni, "GBK"); + let loginIniObj = parseINIStrings(loginIniInUtf8); + try { + loginIniInUtf8.remove(false); + } catch (ex) {} + + let nowLoginEmail = loginIniObj.NowLogin && loginIniObj.NowLogin.email; + + /* + * NowLogin section may: + * 1. be missing or without email, before any user logs in. + * 2. represents the current logged in user + * 3. represents the most recent logged in user + * + * In the second case, user represented by NowLogin should be the first + * profile; otherwise the default user should be selected by default. + */ + if (nowLoginEmail) { + if (loginIniObj.NowLogin.IsLogined === "1") { + noLoggedInUser = false; + } + + profiles.push({ + id: this._getIdFromConfig(loginIniObj.NowLogin), + name: nowLoginEmail, + }); + } + + for (let section in loginIniObj) { + if ( + !loginIniObj[section].email || + (nowLoginEmail && loginIniObj[section].email == nowLoginEmail) + ) { + continue; + } + + profiles.push({ + id: this._getIdFromConfig(loginIniObj[section]), + name: loginIniObj[section].email, + }); + } + } catch (e) { + Cu.reportError("Error detecting 360 Secure Browser profiles: " + e); + } finally { + profiles[noLoggedInUser ? "unshift" : "push"]({ + id: this._defaultUserPath, + name: "Default", + }); + } + + this.__sourceProfiles = profiles.filter(profile => { + let resources = this.getResources(profile); + return resources && !!resources.length; + }); + return this.__sourceProfiles; +}; + +Qihoo360seProfileMigrator.prototype._getIdFromConfig = function(aConfig) { + return aConfig.UserMd5 || getHash(aConfig.email); +}; + +Qihoo360seProfileMigrator.prototype.getResources = function(aProfile) { + let profileFolder = this._usersDir.clone(); + profileFolder.append(aProfile.id); + + if (!profileFolder.exists()) { + return []; + } + + let resources = [new Bookmarks(profileFolder)]; + return resources.filter(r => r.exists); +}; + +Qihoo360seProfileMigrator.prototype.getLastUsedDate = async function() { + let sourceProfiles = await this.getSourceProfiles(); + let bookmarksPaths = sourceProfiles.map(({ id }) => { + return OS.Path.join(this._usersDir.path, id, kBookmarksFileName); + }); + if (!bookmarksPaths.length) { + return new Date(0); + } + let datePromises = bookmarksPaths.map(path => { + return OS.File.stat(path) + .catch(() => null) + .then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); +}; + +Qihoo360seProfileMigrator.prototype.classDescription = + "360 Secure Browser Profile Migrator"; +Qihoo360seProfileMigrator.prototype.contractID = + "@mozilla.org/profile/migrator;1?app=browser&type=360se"; +Qihoo360seProfileMigrator.prototype.classID = Components.ID( + "{d0037b95-296a-4a4e-94b2-c3d075d20ab1}" +); + +var EXPORTED_SYMBOLS = ["Qihoo360seProfileMigrator"]; |