services/sync/modules/engines/clients.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/engines/clients.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,468 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = [
     1.9 +  "ClientEngine",
    1.10 +  "ClientsRec"
    1.11 +];
    1.12 +
    1.13 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.14 +
    1.15 +Cu.import("resource://services-common/stringbundle.js");
    1.16 +Cu.import("resource://services-sync/constants.js");
    1.17 +Cu.import("resource://services-sync/engines.js");
    1.18 +Cu.import("resource://services-sync/record.js");
    1.19 +Cu.import("resource://services-sync/util.js");
    1.20 +
    1.21 +const CLIENTS_TTL = 1814400; // 21 days
    1.22 +const CLIENTS_TTL_REFRESH = 604800; // 7 days
    1.23 +
    1.24 +const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
    1.25 +
    1.26 +this.ClientsRec = function ClientsRec(collection, id) {
    1.27 +  CryptoWrapper.call(this, collection, id);
    1.28 +}
    1.29 +ClientsRec.prototype = {
    1.30 +  __proto__: CryptoWrapper.prototype,
    1.31 +  _logName: "Sync.Record.Clients",
    1.32 +  ttl: CLIENTS_TTL
    1.33 +};
    1.34 +
    1.35 +Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands", "version", "protocols"]);
    1.36 +
    1.37 +
    1.38 +this.ClientEngine = function ClientEngine(service) {
    1.39 +  SyncEngine.call(this, "Clients", service);
    1.40 +
    1.41 +  // Reset the client on every startup so that we fetch recent clients
    1.42 +  this._resetClient();
    1.43 +}
    1.44 +ClientEngine.prototype = {
    1.45 +  __proto__: SyncEngine.prototype,
    1.46 +  _storeObj: ClientStore,
    1.47 +  _recordObj: ClientsRec,
    1.48 +  _trackerObj: ClientsTracker,
    1.49 +
    1.50 +  // Always sync client data as it controls other sync behavior
    1.51 +  get enabled() true,
    1.52 +
    1.53 +  get lastRecordUpload() {
    1.54 +    return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
    1.55 +  },
    1.56 +  set lastRecordUpload(value) {
    1.57 +    Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
    1.58 +  },
    1.59 +
    1.60 +  // Aggregate some stats on the composition of clients on this account
    1.61 +  get stats() {
    1.62 +    let stats = {
    1.63 +      hasMobile: this.localType == "mobile",
    1.64 +      names: [this.localName],
    1.65 +      numClients: 1,
    1.66 +    };
    1.67 +
    1.68 +    for each (let {name, type} in this._store._remoteClients) {
    1.69 +      stats.hasMobile = stats.hasMobile || type == "mobile";
    1.70 +      stats.names.push(name);
    1.71 +      stats.numClients++;
    1.72 +    }
    1.73 +
    1.74 +    return stats;
    1.75 +  },
    1.76 +
    1.77 +  /**
    1.78 +   * Obtain information about device types.
    1.79 +   *
    1.80 +   * Returns a Map of device types to integer counts.
    1.81 +   */
    1.82 +  get deviceTypes() {
    1.83 +    let counts = new Map();
    1.84 +
    1.85 +    counts.set(this.localType, 1);
    1.86 +
    1.87 +    for each (let record in this._store._remoteClients) {
    1.88 +      let type = record.type;
    1.89 +      if (!counts.has(type)) {
    1.90 +        counts.set(type, 0);
    1.91 +      }
    1.92 +
    1.93 +      counts.set(type, counts.get(type) + 1);
    1.94 +    }
    1.95 +
    1.96 +    return counts;
    1.97 +  },
    1.98 +
    1.99 +  get localID() {
   1.100 +    // Generate a random GUID id we don't have one
   1.101 +    let localID = Svc.Prefs.get("client.GUID", "");
   1.102 +    return localID == "" ? this.localID = Utils.makeGUID() : localID;
   1.103 +  },
   1.104 +  set localID(value) Svc.Prefs.set("client.GUID", value),
   1.105 +
   1.106 +  get localName() {
   1.107 +    let localName = Svc.Prefs.get("client.name", "");
   1.108 +    if (localName != "")
   1.109 +      return localName;
   1.110 +
   1.111 +    // Generate a client name if we don't have a useful one yet
   1.112 +    let env = Cc["@mozilla.org/process/environment;1"]
   1.113 +                .getService(Ci.nsIEnvironment);
   1.114 +    let user = env.get("USER") || env.get("USERNAME") ||
   1.115 +               Svc.Prefs.get("account") || Svc.Prefs.get("username");
   1.116 +
   1.117 +    let appName;
   1.118 +    let brand = new StringBundle("chrome://branding/locale/brand.properties");
   1.119 +    let brandName = brand.get("brandShortName");
   1.120 +    try {
   1.121 +      let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
   1.122 +      appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
   1.123 +    } catch (ex) {}
   1.124 +    appName = appName || brandName;
   1.125 +
   1.126 +    let system =
   1.127 +      // 'device' is defined on unix systems
   1.128 +      Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
   1.129 +      // hostname of the system, usually assigned by the user or admin
   1.130 +      Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
   1.131 +      // fall back on ua info string
   1.132 +      Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
   1.133 +
   1.134 +    return this.localName = Str.sync.get("client.name2", [user, appName, system]);
   1.135 +  },
   1.136 +  set localName(value) Svc.Prefs.set("client.name", value),
   1.137 +
   1.138 +  get localType() Svc.Prefs.get("client.type", "desktop"),
   1.139 +  set localType(value) Svc.Prefs.set("client.type", value),
   1.140 +
   1.141 +  isMobile: function isMobile(id) {
   1.142 +    if (this._store._remoteClients[id])
   1.143 +      return this._store._remoteClients[id].type == "mobile";
   1.144 +    return false;
   1.145 +  },
   1.146 +
   1.147 +  _syncStartup: function _syncStartup() {
   1.148 +    // Reupload new client record periodically.
   1.149 +    if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
   1.150 +      this._tracker.addChangedID(this.localID);
   1.151 +      this.lastRecordUpload = Date.now() / 1000;
   1.152 +    }
   1.153 +    SyncEngine.prototype._syncStartup.call(this);
   1.154 +  },
   1.155 +
   1.156 +  // Always process incoming items because they might have commands
   1.157 +  _reconcile: function _reconcile() {
   1.158 +    return true;
   1.159 +  },
   1.160 +
   1.161 +  // Treat reset the same as wiping for locally cached clients
   1.162 +  _resetClient: function _resetClient() this._wipeClient(),
   1.163 +
   1.164 +  _wipeClient: function _wipeClient() {
   1.165 +    SyncEngine.prototype._resetClient.call(this);
   1.166 +    this._store.wipe();
   1.167 +  },
   1.168 +
   1.169 +  removeClientData: function removeClientData() {
   1.170 +    let res = this.service.resource(this.engineURL + "/" + this.localID);
   1.171 +    res.delete();
   1.172 +  },
   1.173 +
   1.174 +  // Override the default behavior to delete bad records from the server.
   1.175 +  handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
   1.176 +    this._log.debug("Handling HMAC mismatch for " + item.id);
   1.177 +
   1.178 +    let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
   1.179 +    if (base != SyncEngine.kRecoveryStrategy.error)
   1.180 +      return base;
   1.181 +
   1.182 +    // It's a bad client record. Save it to be deleted at the end of the sync.
   1.183 +    this._log.debug("Bad client record detected. Scheduling for deletion.");
   1.184 +    this._deleteId(item.id);
   1.185 +
   1.186 +    // Neither try again nor error; we're going to delete it.
   1.187 +    return SyncEngine.kRecoveryStrategy.ignore;
   1.188 +  },
   1.189 +
   1.190 +  /**
   1.191 +   * A hash of valid commands that the client knows about. The key is a command
   1.192 +   * and the value is a hash containing information about the command such as
   1.193 +   * number of arguments and description.
   1.194 +   */
   1.195 +  _commands: {
   1.196 +    resetAll:    { args: 0, desc: "Clear temporary local data for all engines" },
   1.197 +    resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
   1.198 +    wipeAll:     { args: 0, desc: "Delete all client data for all engines" },
   1.199 +    wipeEngine:  { args: 1, desc: "Delete all client data for engine" },
   1.200 +    logout:      { args: 0, desc: "Log out client" },
   1.201 +    displayURI:  { args: 3, desc: "Instruct a client to display a URI" },
   1.202 +  },
   1.203 +
   1.204 +  /**
   1.205 +   * Remove any commands for the local client and mark it for upload.
   1.206 +   */
   1.207 +  clearCommands: function clearCommands() {
   1.208 +    delete this.localCommands;
   1.209 +    this._tracker.addChangedID(this.localID);
   1.210 +  },
   1.211 +
   1.212 +  /**
   1.213 +   * Sends a command+args pair to a specific client.
   1.214 +   *
   1.215 +   * @param command Command string
   1.216 +   * @param args Array of arguments/data for command
   1.217 +   * @param clientId Client to send command to
   1.218 +   */
   1.219 +  _sendCommandToClient: function sendCommandToClient(command, args, clientId) {
   1.220 +    this._log.trace("Sending " + command + " to " + clientId);
   1.221 +
   1.222 +    let client = this._store._remoteClients[clientId];
   1.223 +    if (!client) {
   1.224 +      throw new Error("Unknown remote client ID: '" + clientId + "'.");
   1.225 +    }
   1.226 +
   1.227 +    // notDupe compares two commands and returns if they are not equal.
   1.228 +    let notDupe = function(other) {
   1.229 +      return other.command != command || !Utils.deepEquals(other.args, args);
   1.230 +    };
   1.231 +
   1.232 +    let action = {
   1.233 +      command: command,
   1.234 +      args: args,
   1.235 +    };
   1.236 +
   1.237 +    if (!client.commands) {
   1.238 +      client.commands = [action];
   1.239 +    }
   1.240 +    // Add the new action if there are no duplicates.
   1.241 +    else if (client.commands.every(notDupe)) {
   1.242 +      client.commands.push(action);
   1.243 +    }
   1.244 +    // It must be a dupe. Skip.
   1.245 +    else {
   1.246 +      return;
   1.247 +    }
   1.248 +
   1.249 +    this._log.trace("Client " + clientId + " got a new action: " + [command, args]);
   1.250 +    this._tracker.addChangedID(clientId);
   1.251 +  },
   1.252 +
   1.253 +  /**
   1.254 +   * Check if the local client has any remote commands and perform them.
   1.255 +   *
   1.256 +   * @return false to abort sync
   1.257 +   */
   1.258 +  processIncomingCommands: function processIncomingCommands() {
   1.259 +    return this._notify("clients:process-commands", "", function() {
   1.260 +      let commands = this.localCommands;
   1.261 +
   1.262 +      // Immediately clear out the commands as we've got them locally.
   1.263 +      this.clearCommands();
   1.264 +
   1.265 +      // Process each command in order.
   1.266 +      for each ({command: command, args: args} in commands) {
   1.267 +        this._log.debug("Processing command: " + command + "(" + args + ")");
   1.268 +
   1.269 +        let engines = [args[0]];
   1.270 +        switch (command) {
   1.271 +          case "resetAll":
   1.272 +            engines = null;
   1.273 +            // Fallthrough
   1.274 +          case "resetEngine":
   1.275 +            this.service.resetClient(engines);
   1.276 +            break;
   1.277 +          case "wipeAll":
   1.278 +            engines = null;
   1.279 +            // Fallthrough
   1.280 +          case "wipeEngine":
   1.281 +            this.service.wipeClient(engines);
   1.282 +            break;
   1.283 +          case "logout":
   1.284 +            this.service.logout();
   1.285 +            return false;
   1.286 +          case "displayURI":
   1.287 +            this._handleDisplayURI.apply(this, args);
   1.288 +            break;
   1.289 +          default:
   1.290 +            this._log.debug("Received an unknown command: " + command);
   1.291 +            break;
   1.292 +        }
   1.293 +      }
   1.294 +
   1.295 +      return true;
   1.296 +    })();
   1.297 +  },
   1.298 +
   1.299 +  /**
   1.300 +   * Validates and sends a command to a client or all clients.
   1.301 +   *
   1.302 +   * Calling this does not actually sync the command data to the server. If the
   1.303 +   * client already has the command/args pair, it won't receive a duplicate
   1.304 +   * command.
   1.305 +   *
   1.306 +   * @param command
   1.307 +   *        Command to invoke on remote clients
   1.308 +   * @param args
   1.309 +   *        Array of arguments to give to the command
   1.310 +   * @param clientId
   1.311 +   *        Client ID to send command to. If undefined, send to all remote
   1.312 +   *        clients.
   1.313 +   */
   1.314 +  sendCommand: function sendCommand(command, args, clientId) {
   1.315 +    let commandData = this._commands[command];
   1.316 +    // Don't send commands that we don't know about.
   1.317 +    if (!commandData) {
   1.318 +      this._log.error("Unknown command to send: " + command);
   1.319 +      return;
   1.320 +    }
   1.321 +    // Don't send a command with the wrong number of arguments.
   1.322 +    else if (!args || args.length != commandData.args) {
   1.323 +      this._log.error("Expected " + commandData.args + " args for '" +
   1.324 +                      command + "', but got " + args);
   1.325 +      return;
   1.326 +    }
   1.327 +
   1.328 +    if (clientId) {
   1.329 +      this._sendCommandToClient(command, args, clientId);
   1.330 +    } else {
   1.331 +      for (let id in this._store._remoteClients) {
   1.332 +        this._sendCommandToClient(command, args, id);
   1.333 +      }
   1.334 +    }
   1.335 +  },
   1.336 +
   1.337 +  /**
   1.338 +   * Send a URI to another client for display.
   1.339 +   *
   1.340 +   * A side effect is the score is increased dramatically to incur an
   1.341 +   * immediate sync.
   1.342 +   *
   1.343 +   * If an unknown client ID is specified, sendCommand() will throw an
   1.344 +   * Error object.
   1.345 +   *
   1.346 +   * @param uri
   1.347 +   *        URI (as a string) to send and display on the remote client
   1.348 +   * @param clientId
   1.349 +   *        ID of client to send the command to. If not defined, will be sent
   1.350 +   *        to all remote clients.
   1.351 +   * @param title
   1.352 +   *        Title of the page being sent.
   1.353 +   */
   1.354 +  sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
   1.355 +    this._log.info("Sending URI to client: " + uri + " -> " +
   1.356 +                   clientId + " (" + title + ")");
   1.357 +    this.sendCommand("displayURI", [uri, this.localID, title], clientId);
   1.358 +
   1.359 +    this._tracker.score += SCORE_INCREMENT_XLARGE;
   1.360 +  },
   1.361 +
   1.362 +  /**
   1.363 +   * Handle a single received 'displayURI' command.
   1.364 +   *
   1.365 +   * Interested parties should observe the "weave:engine:clients:display-uri"
   1.366 +   * topic. The callback will receive an object as the subject parameter with
   1.367 +   * the following keys:
   1.368 +   *
   1.369 +   *   uri       URI (string) that is requested for display.
   1.370 +   *   clientId  ID of client that sent the command.
   1.371 +   *   title     Title of page that loaded URI (likely) corresponds to.
   1.372 +   *
   1.373 +   * The 'data' parameter to the callback will not be defined.
   1.374 +   *
   1.375 +   * @param uri
   1.376 +   *        String URI that was received
   1.377 +   * @param clientId
   1.378 +   *        ID of client that sent URI
   1.379 +   * @param title
   1.380 +   *        String title of page that URI corresponds to. Older clients may not
   1.381 +   *        send this.
   1.382 +   */
   1.383 +  _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) {
   1.384 +    this._log.info("Received a URI for display: " + uri + " (" + title +
   1.385 +                   ") from " + clientId);
   1.386 +
   1.387 +    let subject = {uri: uri, client: clientId, title: title};
   1.388 +    Svc.Obs.notify("weave:engine:clients:display-uri", subject);
   1.389 +  }
   1.390 +};
   1.391 +
   1.392 +function ClientStore(name, engine) {
   1.393 +  Store.call(this, name, engine);
   1.394 +}
   1.395 +ClientStore.prototype = {
   1.396 +  __proto__: Store.prototype,
   1.397 +
   1.398 +  create: function create(record) this.update(record),
   1.399 +
   1.400 +  update: function update(record) {
   1.401 +    // Only grab commands from the server; local name/type always wins
   1.402 +    if (record.id == this.engine.localID)
   1.403 +      this.engine.localCommands = record.commands;
   1.404 +    else
   1.405 +      this._remoteClients[record.id] = record.cleartext;
   1.406 +  },
   1.407 +
   1.408 +  createRecord: function createRecord(id, collection) {
   1.409 +    let record = new ClientsRec(collection, id);
   1.410 +
   1.411 +    // Package the individual components into a record for the local client
   1.412 +    if (id == this.engine.localID) {
   1.413 +      record.name = this.engine.localName;
   1.414 +      record.type = this.engine.localType;
   1.415 +      record.commands = this.engine.localCommands;
   1.416 +      record.version = Services.appinfo.version;
   1.417 +      record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
   1.418 +    }
   1.419 +    else
   1.420 +      record.cleartext = this._remoteClients[id];
   1.421 +
   1.422 +    return record;
   1.423 +  },
   1.424 +
   1.425 +  itemExists: function itemExists(id) id in this.getAllIDs(),
   1.426 +
   1.427 +  getAllIDs: function getAllIDs() {
   1.428 +    let ids = {};
   1.429 +    ids[this.engine.localID] = true;
   1.430 +    for (let id in this._remoteClients)
   1.431 +      ids[id] = true;
   1.432 +    return ids;
   1.433 +  },
   1.434 +
   1.435 +  wipe: function wipe() {
   1.436 +    this._remoteClients = {};
   1.437 +  },
   1.438 +};
   1.439 +
   1.440 +function ClientsTracker(name, engine) {
   1.441 +  Tracker.call(this, name, engine);
   1.442 +  Svc.Obs.add("weave:engine:start-tracking", this);
   1.443 +  Svc.Obs.add("weave:engine:stop-tracking", this);
   1.444 +}
   1.445 +ClientsTracker.prototype = {
   1.446 +  __proto__: Tracker.prototype,
   1.447 +
   1.448 +  _enabled: false,
   1.449 +
   1.450 +  observe: function observe(subject, topic, data) {
   1.451 +    switch (topic) {
   1.452 +      case "weave:engine:start-tracking":
   1.453 +        if (!this._enabled) {
   1.454 +          Svc.Prefs.observe("client.name", this);
   1.455 +          this._enabled = true;
   1.456 +        }
   1.457 +        break;
   1.458 +      case "weave:engine:stop-tracking":
   1.459 +        if (this._enabled) {
   1.460 +          Svc.Prefs.ignore("clients.name", this);
   1.461 +          this._enabled = false;
   1.462 +        }
   1.463 +        break;
   1.464 +      case "nsPref:changed":
   1.465 +        this._log.debug("client.name preference changed");
   1.466 +        this.addChangedID(Svc.Prefs.get("client.GUID"));
   1.467 +        this.score += SCORE_INCREMENT_XLARGE;
   1.468 +        break;
   1.469 +    }
   1.470 +  }
   1.471 +};

mercurial