browser/components/migration/src/ChromeProfileMigrator.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/migration/src/ChromeProfileMigrator.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,343 @@
     1.4 +/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     1.5 + * vim: sw=2 ts=2 sts=2 et */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +const Cc = Components.classes;
    1.13 +const Ci = Components.interfaces;
    1.14 +const Cu = Components.utils;
    1.15 +const Cr = Components.results;
    1.16 +
    1.17 +const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
    1.18 +
    1.19 +const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
    1.20 +const S100NS_PER_MS = 10;
    1.21 +
    1.22 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.23 +Cu.import("resource://gre/modules/Services.jsm");
    1.24 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.25 +Cu.import("resource://gre/modules/FileUtils.jsm");
    1.26 +Cu.import("resource:///modules/MigrationUtils.jsm");
    1.27 +
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.29 +                                  "resource://gre/modules/PlacesUtils.jsm");
    1.30 +
    1.31 +/**
    1.32 + * Convert Chrome time format to Date object
    1.33 + *
    1.34 + * @param   aTime
    1.35 + *          Chrome time 
    1.36 + * @return  converted Date object
    1.37 + * @note    Google Chrome uses FILETIME / 10 as time.
    1.38 + *          FILETIME is based on same structure of Windows.
    1.39 + */
    1.40 +function chromeTimeToDate(aTime)
    1.41 +{
    1.42 +  return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970 ) / 10000);
    1.43 +}
    1.44 +
    1.45 +/**
    1.46 + * Insert bookmark items into specific folder.
    1.47 + *
    1.48 + * @param   aFolderId
    1.49 + *          id of folder where items will be inserted
    1.50 + * @param   aItems
    1.51 + *          bookmark items to be inserted
    1.52 + */
    1.53 +function insertBookmarkItems(aFolderId, aItems)
    1.54 +{
    1.55 +  for (let i = 0; i < aItems.length; i++) {
    1.56 +    let item = aItems[i];
    1.57 +
    1.58 +    try {
    1.59 +      if (item.type == "url") {
    1.60 +        PlacesUtils.bookmarks.insertBookmark(aFolderId,
    1.61 +                                             NetUtil.newURI(item.url),
    1.62 +                                             PlacesUtils.bookmarks.DEFAULT_INDEX,
    1.63 +                                             item.name);
    1.64 +      } else if (item.type == "folder") {
    1.65 +        let newFolderId =
    1.66 +          PlacesUtils.bookmarks.createFolder(aFolderId,
    1.67 +                                             item.name,
    1.68 +                                             PlacesUtils.bookmarks.DEFAULT_INDEX);
    1.69 +
    1.70 +        insertBookmarkItems(newFolderId, item.children);
    1.71 +      }
    1.72 +    } catch (e) {
    1.73 +      Cu.reportError(e);
    1.74 +    }
    1.75 +  }
    1.76 +}
    1.77 +
    1.78 +
    1.79 +function ChromeProfileMigrator() {
    1.80 +  let chromeUserDataFolder = FileUtils.getDir(
    1.81 +#ifdef XP_WIN
    1.82 +    "LocalAppData", ["Google", "Chrome", "User Data"]
    1.83 +#elifdef XP_MACOSX
    1.84 +    "ULibDir", ["Application Support", "Google", "Chrome"]
    1.85 +#else
    1.86 +    "Home", [".config", "google-chrome"]
    1.87 +#endif
    1.88 +    , false);
    1.89 +  this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
    1.90 +    chromeUserDataFolder : null;
    1.91 +}
    1.92 +
    1.93 +ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
    1.94 +
    1.95 +ChromeProfileMigrator.prototype.getResources =
    1.96 +  function Chrome_getResources(aProfile) {
    1.97 +    if (this._chromeUserDataFolder) {
    1.98 +      let profileFolder = this._chromeUserDataFolder.clone();
    1.99 +      profileFolder.append(aProfile);
   1.100 +      if (profileFolder.exists()) {
   1.101 +        let possibleResources = [GetBookmarksResource(profileFolder),
   1.102 +                                 GetHistoryResource(profileFolder),
   1.103 +                                 GetCookiesResource(profileFolder)];
   1.104 +        return [r for each (r in possibleResources) if (r != null)];
   1.105 +      }
   1.106 +    }
   1.107 +    return [];
   1.108 +  };
   1.109 +
   1.110 +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
   1.111 +  get: function Chrome_sourceProfiles() {
   1.112 +    if ("__sourceProfiles" in this)
   1.113 +      return this.__sourceProfiles;
   1.114 +
   1.115 +    if (!this._chromeUserDataFolder)
   1.116 +      return [];
   1.117 +
   1.118 +    let profiles;
   1.119 +    try {
   1.120 +      // Local State is a JSON file that contains profile info.
   1.121 +      let localState = this._chromeUserDataFolder.clone();
   1.122 +      localState.append("Local State");
   1.123 +      if (!localState.exists())
   1.124 +        throw new Error("Chrome's 'Local State' file does not exist.");
   1.125 +      if (!localState.isReadable())
   1.126 +        throw new Error("Chrome's 'Local State' file could not be read.");
   1.127 +
   1.128 +      let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
   1.129 +      fstream.init(localState, -1, 0, 0);
   1.130 +      let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
   1.131 +                                                        { charset: "UTF-8" });
   1.132 +      let info_cache = JSON.parse(inputStream).profile.info_cache;
   1.133 +      if (info_cache)
   1.134 +        profiles = Object.keys(info_cache);
   1.135 +    } catch (e) {
   1.136 +      Cu.reportError("Error detecting Chrome profiles: " + e);
   1.137 +      // If we weren't able to detect any profiles above, fallback to the Default profile.
   1.138 +      let defaultProfileFolder = this._chromeUserDataFolder.clone();
   1.139 +      defaultProfileFolder.append("Default");
   1.140 +      if (defaultProfileFolder.exists())
   1.141 +        profiles = ["Default"];
   1.142 +    }
   1.143 +
   1.144 +    // Only list profiles from which any data can be imported
   1.145 +    return this.__sourceProfiles = profiles.filter(function(profileName) {
   1.146 +      let resources = this.getResources(profileName);
   1.147 +      return resources && resources.length > 0;
   1.148 +    }, this);
   1.149 +  }
   1.150 +});
   1.151 +
   1.152 +Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
   1.153 +  get: function Chrome_sourceHomePageURL() {
   1.154 +    let prefsFile = this._chromeUserDataFolder.clone();
   1.155 +    prefsFile.append("Preferences");
   1.156 +    if (prefsFile.exists()) {
   1.157 +      // XXX reading and parsing JSON is synchronous.
   1.158 +      let fstream = Cc[FILE_INPUT_STREAM_CID].
   1.159 +                    createInstance(Ci.nsIFileInputStream);
   1.160 +      fstream.init(file, -1, 0, 0);
   1.161 +      try {
   1.162 +        return JSON.parse(
   1.163 +          NetUtil.readInputStreamToString(fstream, fstream.available(),
   1.164 +                                          { charset: "UTF-8" })
   1.165 +            ).homepage;
   1.166 +      }
   1.167 +      catch(e) {
   1.168 +        Cu.reportError("Error parsing Chrome's preferences file: " + e);
   1.169 +      }
   1.170 +    }
   1.171 +    return "";
   1.172 +  }
   1.173 +});
   1.174 +
   1.175 +function GetBookmarksResource(aProfileFolder) {
   1.176 +  let bookmarksFile = aProfileFolder.clone();
   1.177 +  bookmarksFile.append("Bookmarks");
   1.178 +  if (!bookmarksFile.exists())
   1.179 +    return null;
   1.180 +
   1.181 +  return {
   1.182 +    type: MigrationUtils.resourceTypes.BOOKMARKS,
   1.183 +
   1.184 +    migrate: function(aCallback) {
   1.185 +      NetUtil.asyncFetch(bookmarksFile, MigrationUtils.wrapMigrateFunction(
   1.186 +        function(aInputStream, aResultCode) {
   1.187 +          if (!Components.isSuccessCode(aResultCode))
   1.188 +            throw new Error("Could not read Bookmarks file");
   1.189 +            
   1.190 +          // Parse Chrome bookmark file that is JSON format
   1.191 +          let bookmarkJSON = NetUtil.readInputStreamToString(
   1.192 +            aInputStream, aInputStream.available(), { charset : "UTF-8" });
   1.193 +          let roots = JSON.parse(bookmarkJSON).roots;
   1.194 +          PlacesUtils.bookmarks.runInBatchMode({
   1.195 +            runBatched: function() {
   1.196 +              // Importing bookmark bar items
   1.197 +              if (roots.bookmark_bar.children &&
   1.198 +                  roots.bookmark_bar.children.length > 0) {
   1.199 +                // Toolbar
   1.200 +                let parentId = PlacesUtils.toolbarFolderId;
   1.201 +                if (!MigrationUtils.isStartupMigration) { 
   1.202 +                  parentId = MigrationUtils.createImportedBookmarksFolder(
   1.203 +                    "Chrome", parentId);
   1.204 +                }
   1.205 +                insertBookmarkItems(parentId, roots.bookmark_bar.children);
   1.206 +              }
   1.207 +
   1.208 +              // Importing bookmark menu items
   1.209 +              if (roots.other.children &&
   1.210 +                  roots.other.children.length > 0) {
   1.211 +                // Bookmark menu
   1.212 +                let parentId = PlacesUtils.bookmarksMenuFolderId;
   1.213 +                if (!MigrationUtils.isStartupMigration) { 
   1.214 +                  parentId = MigrationUtils.createImportedBookmarksFolder(
   1.215 +                    "Chrome", parentId);
   1.216 +                }
   1.217 +                insertBookmarkItems(parentId, roots.other.children);
   1.218 +              }
   1.219 +            }
   1.220 +          }, null);
   1.221 +        }, aCallback));
   1.222 +    }
   1.223 +  };
   1.224 +}
   1.225 +
   1.226 +function GetHistoryResource(aProfileFolder) {
   1.227 +  let historyFile = aProfileFolder.clone();
   1.228 +  historyFile.append("History");
   1.229 +  if (!historyFile.exists())
   1.230 +    return null;
   1.231 +
   1.232 +  return {
   1.233 +    type: MigrationUtils.resourceTypes.HISTORY,
   1.234 +
   1.235 +    migrate: function(aCallback) {
   1.236 +      let dbConn = Services.storage.openUnsharedDatabase(historyFile);
   1.237 +      let stmt = dbConn.createAsyncStatement(
   1.238 +        "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0");
   1.239 +
   1.240 +      stmt.executeAsync({
   1.241 +        handleResult : function(aResults) {
   1.242 +          let places = [];
   1.243 +          for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
   1.244 +            try {
   1.245 +              // if having typed_count, we changes transition type to typed.
   1.246 +              let transType = PlacesUtils.history.TRANSITION_LINK;
   1.247 +              if (row.getResultByName("typed_count") > 0)
   1.248 +                transType = PlacesUtils.history.TRANSITION_TYPED;
   1.249 +
   1.250 +              places.push({
   1.251 +                uri: NetUtil.newURI(row.getResultByName("url")),
   1.252 +                title: row.getResultByName("title"),
   1.253 +                visits: [{
   1.254 +                  transitionType: transType,
   1.255 +                  visitDate: chromeTimeToDate(
   1.256 +                               row.getResultByName(
   1.257 +                                 "last_visit_time")) * 1000,
   1.258 +                }],
   1.259 +              });
   1.260 +            } catch (e) {
   1.261 +              Cu.reportError(e);
   1.262 +            }
   1.263 +          }
   1.264 +
   1.265 +          try {
   1.266 +            PlacesUtils.asyncHistory.updatePlaces(places);
   1.267 +          } catch (e) {
   1.268 +            Cu.reportError(e);
   1.269 +          }
   1.270 +        },
   1.271 +
   1.272 +        handleError : function(aError) {
   1.273 +          Cu.reportError("Async statement execution returned with '" +
   1.274 +                         aError.result + "', '" + aError.message + "'");
   1.275 +        },
   1.276 +
   1.277 +        handleCompletion : function(aReason) {
   1.278 +          dbConn.asyncClose();
   1.279 +          aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
   1.280 +        }
   1.281 +      });
   1.282 +      stmt.finalize();
   1.283 +    }
   1.284 +  };
   1.285 +}
   1.286 +
   1.287 +function GetCookiesResource(aProfileFolder) {
   1.288 +  let cookiesFile = aProfileFolder.clone();
   1.289 +  cookiesFile.append("Cookies");
   1.290 +  if (!cookiesFile.exists())
   1.291 +    return null;
   1.292 +
   1.293 +  return {
   1.294 +    type: MigrationUtils.resourceTypes.COOKIES,
   1.295 +
   1.296 +    migrate: function(aCallback) {
   1.297 +      let dbConn = Services.storage.openUnsharedDatabase(cookiesFile);
   1.298 +      let stmt = dbConn.createAsyncStatement(
   1.299 +          "SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies");
   1.300 +
   1.301 +      stmt.executeAsync({
   1.302 +        handleResult : function(aResults) {
   1.303 +          for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
   1.304 +            let host_key = row.getResultByName("host_key");
   1.305 +            if (host_key.match(/^\./)) {
   1.306 +              // 1st character of host_key may be ".", so we have to remove it
   1.307 +              host_key = host_key.substr(1);
   1.308 +            }
   1.309 +
   1.310 +            try {
   1.311 +              let expiresUtc =
   1.312 +                chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
   1.313 +              Services.cookies.add(host_key,
   1.314 +                                   row.getResultByName("path"),
   1.315 +                                   row.getResultByName("name"),
   1.316 +                                   row.getResultByName("value"),
   1.317 +                                   row.getResultByName("secure"),
   1.318 +                                   row.getResultByName("httponly"),
   1.319 +                                   false,
   1.320 +                                   parseInt(expiresUtc));
   1.321 +            } catch (e) {
   1.322 +              Cu.reportError(e);
   1.323 +            }
   1.324 +          }
   1.325 +        },
   1.326 +
   1.327 +        handleError : function(aError) {
   1.328 +          Cu.reportError("Async statement execution returned with '" +
   1.329 +                         aError.result + "', '" + aError.message + "'");
   1.330 +        },
   1.331 +
   1.332 +        handleCompletion : function(aReason) {
   1.333 +          dbConn.asyncClose();
   1.334 +          aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
   1.335 +        },
   1.336 +      });
   1.337 +      stmt.finalize();
   1.338 +    }
   1.339 +  }
   1.340 +}
   1.341 +
   1.342 +ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
   1.343 +ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
   1.344 +ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
   1.345 +
   1.346 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);

mercurial