forked from mirrors/gecko-dev
		
	 e44c888947
			
		
	
	
		e44c888947
		
	
	
	
	
		
			
			Patch originally by Ganesh, updated by Standard8 MozReview-Commit-ID: AihTLo5OyK1 --HG-- extra : rebase_source : f5a886326dd7f2fb2dba3309199ef277affe6b77
		
			
				
	
	
		
			500 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			500 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 | |
|  * vim: sw=2 ts=2 sts=2 et */
 | |
| /* 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 S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
 | |
| const S100NS_PER_MS = 10;
 | |
| 
 | |
| const AUTH_TYPE = {
 | |
|   SCHEME_HTML: 0,
 | |
|   SCHEME_BASIC: 1,
 | |
|   SCHEME_DIGEST: 2,
 | |
| };
 | |
| 
 | |
| ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 | |
| ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 | |
| ChromeUtils.import("resource://gre/modules/osfile.jsm");
 | |
| ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| ChromeUtils.import("resource:///modules/ChromeMigrationUtils.jsm");
 | |
| ChromeUtils.import("resource:///modules/MigrationUtils.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "PlacesUtils",
 | |
|                                "resource://gre/modules/PlacesUtils.jsm");
 | |
| ChromeUtils.defineModuleGetter(this, "OSCrypto",
 | |
|                                "resource://gre/modules/OSCrypto.jsm");
 | |
| 
 | |
| /**
 | |
|  * Convert Chrome time format to Date object
 | |
|  *
 | |
|  * @param   aTime
 | |
|  *          Chrome time
 | |
|  * @return  converted Date object
 | |
|  * @note    Google Chrome uses FILETIME / 10 as time.
 | |
|  *          FILETIME is based on same structure of Windows.
 | |
|  */
 | |
| function chromeTimeToDate(aTime) {
 | |
|   return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Convert Date object to Chrome time format
 | |
|  *
 | |
|  * @param   aDate
 | |
|  *          Date object or integer equivalent
 | |
|  * @return  Chrome time
 | |
|  * @note    For details on Chrome time, see chromeTimeToDate.
 | |
|  */
 | |
| function dateToChromeTime(aDate) {
 | |
|   return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Converts an array of chrome bookmark objects into one our own places code
 | |
|  * understands.
 | |
|  *
 | |
|  * @param   items
 | |
|  *          bookmark items to be inserted on this parent
 | |
|  * @param   errorAccumulator
 | |
|  *          function that gets called with any errors thrown so we don't drop them on the floor.
 | |
|  */
 | |
| function convertBookmarks(items, errorAccumulator) {
 | |
|   let itemsToInsert = [];
 | |
|   for (let item of items) {
 | |
|     try {
 | |
|       if (item.type == "url") {
 | |
|         if (item.url.trim().startsWith("chrome:")) {
 | |
|           // Skip invalid chrome URIs. Creating an actual URI always reports
 | |
|           // messages to the console, so we avoid doing that.
 | |
|           continue;
 | |
|         }
 | |
|         itemsToInsert.push({url: item.url, title: item.name});
 | |
|       } else if (item.type == "folder") {
 | |
|         let folderItem = {type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name};
 | |
|         folderItem.children = convertBookmarks(item.children, errorAccumulator);
 | |
|         itemsToInsert.push(folderItem);
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       Cu.reportError(ex);
 | |
|       errorAccumulator(ex);
 | |
|     }
 | |
|   }
 | |
|   return itemsToInsert;
 | |
| }
 | |
| 
 | |
| function ChromeProfileMigrator() {
 | |
|   this._chromeUserDataPathSuffix = "Chrome";
 | |
| }
 | |
| 
 | |
| ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
 | |
| 
 | |
| ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() {
 | |
|   if (this._chromeUserDataPath) {
 | |
|     return this._chromeUserDataPath;
 | |
|   }
 | |
|   let path = ChromeMigrationUtils.getDataPath(this._chromeUserDataPathSuffix);
 | |
|   let exists = await OS.File.exists(path);
 | |
|   if (exists) {
 | |
|     this._chromeUserDataPath = path;
 | |
|   } else {
 | |
|     this._chromeUserDataPath = null;
 | |
|   }
 | |
|   return this._chromeUserDataPath;
 | |
| };
 | |
| 
 | |
| ChromeProfileMigrator.prototype.getResources =
 | |
|   async function Chrome_getResources(aProfile) {
 | |
|     let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
 | |
|     if (chromeUserDataPath) {
 | |
|       let profileFolder = OS.Path.join(chromeUserDataPath, aProfile.id);
 | |
|       if (await OS.File.exists(profileFolder)) {
 | |
|         let possibleResourcePromises = [
 | |
|           GetBookmarksResource(profileFolder),
 | |
|           GetHistoryResource(profileFolder),
 | |
|           GetCookiesResource(profileFolder),
 | |
|         ];
 | |
|         if (AppConstants.platform == "win") {
 | |
|           possibleResourcePromises.push(GetWindowsPasswordsResource(profileFolder));
 | |
|         }
 | |
|         let possibleResources = await Promise.all(possibleResourcePromises);
 | |
|         return possibleResources.filter(r => r != null);
 | |
|       }
 | |
|     }
 | |
|     return [];
 | |
|   };
 | |
| 
 | |
| ChromeProfileMigrator.prototype.getLastUsedDate =
 | |
|   async function Chrome_getLastUsedDate() {
 | |
|     let sourceProfiles = await this.getSourceProfiles();
 | |
|     let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
 | |
|     if (!chromeUserDataPath) {
 | |
|       return new Date(0);
 | |
|     }
 | |
|     let datePromises = sourceProfiles.map(async profile => {
 | |
|       let basePath = OS.Path.join(chromeUserDataPath, profile.id);
 | |
|       let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(async leafName => {
 | |
|         let path = OS.Path.join(basePath, leafName);
 | |
|         let info = await OS.File.stat(path).catch(() => null);
 | |
|         return info ? info.lastModificationDate : 0;
 | |
|       });
 | |
|       let dates = await Promise.all(fileDatePromises);
 | |
|       return Math.max(...dates);
 | |
|     });
 | |
|     let datesOuter = await Promise.all(datePromises);
 | |
|     datesOuter.push(0);
 | |
|     return new Date(Math.max(...datesOuter));
 | |
|   };
 | |
| 
 | |
| ChromeProfileMigrator.prototype.getSourceProfiles =
 | |
|   async function Chrome_getSourceProfiles() {
 | |
|     if ("__sourceProfiles" in this)
 | |
|       return this.__sourceProfiles;
 | |
| 
 | |
|     let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
 | |
|     if (!chromeUserDataPath)
 | |
|       return [];
 | |
| 
 | |
|     let profiles = [];
 | |
|     try {
 | |
|       let localState = await ChromeMigrationUtils.getLocalState(this._chromeUserDataPathSuffix);
 | |
|       let info_cache = localState.profile.info_cache;
 | |
|       for (let profileFolderName in info_cache) {
 | |
|         profiles.push({
 | |
|           id: profileFolderName,
 | |
|           name: info_cache[profileFolderName].name || profileFolderName,
 | |
|         });
 | |
|       }
 | |
|     } catch (e) {
 | |
|       Cu.reportError("Error detecting Chrome profiles: " + e);
 | |
|       // If we weren't able to detect any profiles above, fallback to the Default profile.
 | |
|       let defaultProfilePath = OS.Path.join(chromeUserDataPath, "Default");
 | |
|       if (await OS.File.exists(defaultProfilePath)) {
 | |
|         profiles = [{
 | |
|           id: "Default",
 | |
|           name: "Default",
 | |
|         }];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let profileResources = await Promise.all(profiles.map(async profile => ({
 | |
|       profile,
 | |
|       resources: await this.getResources(profile),
 | |
|     })));
 | |
| 
 | |
|     // Only list profiles from which any data can be imported
 | |
|     this.__sourceProfiles = profileResources.filter(({resources}) => {
 | |
|       return resources && resources.length > 0;
 | |
|     }, this).map(({profile}) => profile);
 | |
|     return this.__sourceProfiles;
 | |
|   };
 | |
| 
 | |
| Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", {
 | |
|   get: function Chrome_sourceLocked() {
 | |
|     // There is an exclusive lock on some SQLite databases. Assume they are locked for now.
 | |
|     return true;
 | |
|   },
 | |
| });
 | |
| 
 | |
| async function GetBookmarksResource(aProfileFolder) {
 | |
|   let bookmarksPath = OS.Path.join(aProfileFolder, "Bookmarks");
 | |
|   if (!(await OS.File.exists(bookmarksPath)))
 | |
|     return null;
 | |
| 
 | |
|   return {
 | |
|     type: MigrationUtils.resourceTypes.BOOKMARKS,
 | |
| 
 | |
|     migrate(aCallback) {
 | |
|       return (async function() {
 | |
|         let gotErrors = false;
 | |
|         let errorGatherer = function() { gotErrors = true; };
 | |
|         // Parse Chrome bookmark file that is JSON format
 | |
|         let bookmarkJSON = await OS.File.read(bookmarksPath, {encoding: "UTF-8"});
 | |
|         let roots = JSON.parse(bookmarkJSON).roots;
 | |
| 
 | |
|         // Importing bookmark bar items
 | |
|         if (roots.bookmark_bar.children &&
 | |
|             roots.bookmark_bar.children.length > 0) {
 | |
|           // Toolbar
 | |
|           let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
 | |
|           let bookmarks = convertBookmarks(roots.bookmark_bar.children, errorGatherer);
 | |
|           if (!MigrationUtils.isStartupMigration) {
 | |
|             parentGuid =
 | |
|               await MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
 | |
|           }
 | |
|           await MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid);
 | |
|         }
 | |
| 
 | |
|         // Importing bookmark menu items
 | |
|         if (roots.other.children &&
 | |
|             roots.other.children.length > 0) {
 | |
|           // Bookmark menu
 | |
|           let parentGuid = PlacesUtils.bookmarks.menuGuid;
 | |
|           let bookmarks = convertBookmarks(roots.other.children, errorGatherer);
 | |
|           if (!MigrationUtils.isStartupMigration) {
 | |
|             parentGuid
 | |
|               = await MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
 | |
|           }
 | |
|           await MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid);
 | |
|         }
 | |
|         if (gotErrors) {
 | |
|           throw new Error("The migration included errors.");
 | |
|         }
 | |
|       })().then(() => aCallback(true),
 | |
|               () => aCallback(false));
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| async function GetHistoryResource(aProfileFolder) {
 | |
|   let historyPath = OS.Path.join(aProfileFolder, "History");
 | |
|   if (!(await OS.File.exists(historyPath)))
 | |
|     return null;
 | |
| 
 | |
|   return {
 | |
|     type: MigrationUtils.resourceTypes.HISTORY,
 | |
| 
 | |
|     migrate(aCallback) {
 | |
|       (async function() {
 | |
|         const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
 | |
|         const LIMIT = Services.prefs.getIntPref("browser.migrate.chrome.history.limit");
 | |
| 
 | |
|         let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
 | |
|         if (MAX_AGE_IN_DAYS) {
 | |
|           let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
 | |
|           query += " AND last_visit_time > " + maxAge;
 | |
|         }
 | |
|         if (LIMIT) {
 | |
|           query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
 | |
|         }
 | |
| 
 | |
|         let rows =
 | |
|           await MigrationUtils.getRowsFromDBWithoutLocks(historyPath, "Chrome history", query);
 | |
|         let pageInfos = [];
 | |
|         for (let row of rows) {
 | |
|           try {
 | |
|             // if having typed_count, we changes transition type to typed.
 | |
|             let transition = PlacesUtils.history.TRANSITIONS.LINK;
 | |
|             if (row.getResultByName("typed_count") > 0)
 | |
|               transition = PlacesUtils.history.TRANSITIONS.TYPED;
 | |
| 
 | |
|             pageInfos.push({
 | |
|               title: row.getResultByName("title"),
 | |
|               url: new URL(row.getResultByName("url")),
 | |
|               visits: [{
 | |
|                 transition,
 | |
|                 date: chromeTimeToDate(row.getResultByName("last_visit_time")),
 | |
|               }],
 | |
|             });
 | |
|           } catch (e) {
 | |
|             Cu.reportError(e);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (pageInfos.length > 0) {
 | |
|           await MigrationUtils.insertVisitsWrapper(pageInfos);
 | |
|         }
 | |
|       })().then(() => { aCallback(true); },
 | |
|               ex => {
 | |
|                 Cu.reportError(ex);
 | |
|                 aCallback(false);
 | |
|               });
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| async function GetCookiesResource(aProfileFolder) {
 | |
|   let cookiesPath = OS.Path.join(aProfileFolder, "Cookies");
 | |
|   if (!(await OS.File.exists(cookiesPath)))
 | |
|     return null;
 | |
| 
 | |
|   return {
 | |
|     type: MigrationUtils.resourceTypes.COOKIES,
 | |
| 
 | |
|     async migrate(aCallback) {
 | |
|       // We don't support decrypting cookies yet so only import plaintext ones.
 | |
|       let rows = await MigrationUtils.getRowsFromDBWithoutLocks(cookiesPath, "Chrome cookies",
 | |
|        `SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
 | |
|         FROM cookies
 | |
|         WHERE length(encrypted_value) = 0`).catch(ex => {
 | |
|           Cu.reportError(ex);
 | |
|           aCallback(false);
 | |
|         });
 | |
|       // If the promise was rejected we will have already called aCallback,
 | |
|       // so we can just return here.
 | |
|       if (!rows) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       for (let row of rows) {
 | |
|         let host_key = row.getResultByName("host_key");
 | |
|         if (host_key.match(/^\./)) {
 | |
|           // 1st character of host_key may be ".", so we have to remove it
 | |
|           host_key = host_key.substr(1);
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           let expiresUtc =
 | |
|             chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
 | |
|           Services.cookies.add(host_key,
 | |
|                                row.getResultByName("path"),
 | |
|                                row.getResultByName("name"),
 | |
|                                row.getResultByName("value"),
 | |
|                                row.getResultByName("secure"),
 | |
|                                row.getResultByName("httponly"),
 | |
|                                false,
 | |
|                                parseInt(expiresUtc),
 | |
|                                {});
 | |
|         } catch (e) {
 | |
|           Cu.reportError(e);
 | |
|         }
 | |
|       }
 | |
|       aCallback(true);
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| async function GetWindowsPasswordsResource(aProfileFolder) {
 | |
|   let loginPath = OS.Path.join(aProfileFolder, "Login Data");
 | |
|   if (!(await OS.File.exists(loginPath)))
 | |
|     return null;
 | |
| 
 | |
|   return {
 | |
|     type: MigrationUtils.resourceTypes.PASSWORDS,
 | |
| 
 | |
|     async migrate(aCallback) {
 | |
|       let rows = await MigrationUtils.getRowsFromDBWithoutLocks(loginPath, "Chrome passwords",
 | |
|        `SELECT origin_url, action_url, username_element, username_value,
 | |
|         password_element, password_value, signon_realm, scheme, date_created,
 | |
|         times_used FROM logins WHERE blacklisted_by_user = 0`).catch(ex => {
 | |
|           Cu.reportError(ex);
 | |
|           aCallback(false);
 | |
|         });
 | |
|       // If the promise was rejected we will have already called aCallback,
 | |
|       // so we can just return here.
 | |
|       if (!rows) {
 | |
|         return;
 | |
|       }
 | |
|       let crypto = new OSCrypto();
 | |
|       let logins = [];
 | |
|       for (let row of rows) {
 | |
|         try {
 | |
|           let origin_url = NetUtil.newURI(row.getResultByName("origin_url"));
 | |
|           // Ignore entries for non-http(s)/ftp URLs because we likely can't
 | |
|           // use them anyway.
 | |
|           const kValidSchemes = new Set(["https", "http", "ftp"]);
 | |
|           if (!kValidSchemes.has(origin_url.scheme)) {
 | |
|             continue;
 | |
|           }
 | |
|           let loginInfo = {
 | |
|             username: row.getResultByName("username_value"),
 | |
|             password: crypto.
 | |
|                       decryptData(crypto.arrayToString(row.getResultByName("password_value")),
 | |
|                                                        null),
 | |
|             hostname: origin_url.prePath,
 | |
|             formSubmitURL: null,
 | |
|             httpRealm: null,
 | |
|             usernameElement: row.getResultByName("username_element"),
 | |
|             passwordElement: row.getResultByName("password_element"),
 | |
|             timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
 | |
|             timesUsed: row.getResultByName("times_used") + 0,
 | |
|           };
 | |
| 
 | |
|           switch (row.getResultByName("scheme")) {
 | |
|             case AUTH_TYPE.SCHEME_HTML:
 | |
|               let action_url = NetUtil.newURI(row.getResultByName("action_url"));
 | |
|               if (!kValidSchemes.has(action_url.scheme)) {
 | |
|                 continue; // This continues the outer for loop.
 | |
|               }
 | |
|               loginInfo.formSubmitURL = action_url.prePath;
 | |
|               break;
 | |
|             case AUTH_TYPE.SCHEME_BASIC:
 | |
|             case AUTH_TYPE.SCHEME_DIGEST:
 | |
|               // signon_realm format is URIrealm, so we need remove URI
 | |
|               loginInfo.httpRealm = row.getResultByName("signon_realm")
 | |
|                                        .substring(loginInfo.hostname.length + 1);
 | |
|               break;
 | |
|             default:
 | |
|               throw new Error("Login data scheme type not supported: " +
 | |
|                               row.getResultByName("scheme"));
 | |
|           }
 | |
|           logins.push(loginInfo);
 | |
|         } catch (e) {
 | |
|           Cu.reportError(e);
 | |
|         }
 | |
|       }
 | |
|       try {
 | |
|         if (logins.length > 0) {
 | |
|           await MigrationUtils.insertLoginsWrapper(logins);
 | |
|         }
 | |
|       } catch (e) {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|       crypto.finalize();
 | |
|       aCallback(true);
 | |
|     },
 | |
|   };
 | |
| }
 | |
| 
 | |
| ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
 | |
| ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
 | |
| ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
 | |
| 
 | |
| 
 | |
| /**
 | |
|  *  Chromium migration
 | |
|  **/
 | |
| function ChromiumProfileMigrator() {
 | |
|   this._chromeUserDataPathSuffix = "Chromium";
 | |
| }
 | |
| 
 | |
| ChromiumProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 | |
| ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator";
 | |
| ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
 | |
| ChromiumProfileMigrator.prototype.classID = Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
 | |
| 
 | |
| var componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
 | |
| 
 | |
| /**
 | |
|  * Chrome Canary
 | |
|  * Not available on Linux
 | |
|  **/
 | |
| function CanaryProfileMigrator() {
 | |
|   this._chromeUserDataPathSuffix = "Canary";
 | |
| }
 | |
| CanaryProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 | |
| CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
 | |
| CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
 | |
| CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
 | |
| 
 | |
| if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
 | |
|   componentsArray.push(CanaryProfileMigrator);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Chrome Dev / Unstable and Beta. Only separate from `regular` chrome on Linux
 | |
|  */
 | |
| if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
 | |
|   function ChromeDevMigrator() {
 | |
|     this._chromeUserDataPathSuffix = "Chrome Dev";
 | |
|   }
 | |
|   ChromeDevMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 | |
|   ChromeDevMigrator.prototype.classDescription = "Chrome Dev Profile Migrator";
 | |
|   ChromeDevMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev";
 | |
|   ChromeDevMigrator.prototype.classID = Components.ID("{7370a02a-4886-42c3-a4ec-d48c726ec30a}");
 | |
| 
 | |
|   function ChromeBetaMigrator() {
 | |
|     this._chromeUserDataPathSuffix = "Chrome Beta";
 | |
|   }
 | |
|   ChromeBetaMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 | |
|   ChromeBetaMigrator.prototype.classDescription = "Chrome Beta Profile Migrator";
 | |
|   ChromeBetaMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta";
 | |
|   ChromeBetaMigrator.prototype.classID = Components.ID("{47f75963-840b-4950-a1f0-d9c1864f8b8e}");
 | |
| 
 | |
|   componentsArray.push(ChromeDevMigrator, ChromeBetaMigrator);
 | |
| }
 | |
| 
 | |
| this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);
 |