services/sync/modules/engines/clients.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     5 this.EXPORTED_SYMBOLS = [
     6   "ClientEngine",
     7   "ClientsRec"
     8 ];
    10 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    12 Cu.import("resource://services-common/stringbundle.js");
    13 Cu.import("resource://services-sync/constants.js");
    14 Cu.import("resource://services-sync/engines.js");
    15 Cu.import("resource://services-sync/record.js");
    16 Cu.import("resource://services-sync/util.js");
    18 const CLIENTS_TTL = 1814400; // 21 days
    19 const CLIENTS_TTL_REFRESH = 604800; // 7 days
    21 const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
    23 this.ClientsRec = function ClientsRec(collection, id) {
    24   CryptoWrapper.call(this, collection, id);
    25 }
    26 ClientsRec.prototype = {
    27   __proto__: CryptoWrapper.prototype,
    28   _logName: "Sync.Record.Clients",
    29   ttl: CLIENTS_TTL
    30 };
    32 Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands", "version", "protocols"]);
    35 this.ClientEngine = function ClientEngine(service) {
    36   SyncEngine.call(this, "Clients", service);
    38   // Reset the client on every startup so that we fetch recent clients
    39   this._resetClient();
    40 }
    41 ClientEngine.prototype = {
    42   __proto__: SyncEngine.prototype,
    43   _storeObj: ClientStore,
    44   _recordObj: ClientsRec,
    45   _trackerObj: ClientsTracker,
    47   // Always sync client data as it controls other sync behavior
    48   get enabled() true,
    50   get lastRecordUpload() {
    51     return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
    52   },
    53   set lastRecordUpload(value) {
    54     Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
    55   },
    57   // Aggregate some stats on the composition of clients on this account
    58   get stats() {
    59     let stats = {
    60       hasMobile: this.localType == "mobile",
    61       names: [this.localName],
    62       numClients: 1,
    63     };
    65     for each (let {name, type} in this._store._remoteClients) {
    66       stats.hasMobile = stats.hasMobile || type == "mobile";
    67       stats.names.push(name);
    68       stats.numClients++;
    69     }
    71     return stats;
    72   },
    74   /**
    75    * Obtain information about device types.
    76    *
    77    * Returns a Map of device types to integer counts.
    78    */
    79   get deviceTypes() {
    80     let counts = new Map();
    82     counts.set(this.localType, 1);
    84     for each (let record in this._store._remoteClients) {
    85       let type = record.type;
    86       if (!counts.has(type)) {
    87         counts.set(type, 0);
    88       }
    90       counts.set(type, counts.get(type) + 1);
    91     }
    93     return counts;
    94   },
    96   get localID() {
    97     // Generate a random GUID id we don't have one
    98     let localID = Svc.Prefs.get("client.GUID", "");
    99     return localID == "" ? this.localID = Utils.makeGUID() : localID;
   100   },
   101   set localID(value) Svc.Prefs.set("client.GUID", value),
   103   get localName() {
   104     let localName = Svc.Prefs.get("client.name", "");
   105     if (localName != "")
   106       return localName;
   108     // Generate a client name if we don't have a useful one yet
   109     let env = Cc["@mozilla.org/process/environment;1"]
   110                 .getService(Ci.nsIEnvironment);
   111     let user = env.get("USER") || env.get("USERNAME") ||
   112                Svc.Prefs.get("account") || Svc.Prefs.get("username");
   114     let appName;
   115     let brand = new StringBundle("chrome://branding/locale/brand.properties");
   116     let brandName = brand.get("brandShortName");
   117     try {
   118       let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
   119       appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
   120     } catch (ex) {}
   121     appName = appName || brandName;
   123     let system =
   124       // 'device' is defined on unix systems
   125       Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
   126       // hostname of the system, usually assigned by the user or admin
   127       Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
   128       // fall back on ua info string
   129       Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
   131     return this.localName = Str.sync.get("client.name2", [user, appName, system]);
   132   },
   133   set localName(value) Svc.Prefs.set("client.name", value),
   135   get localType() Svc.Prefs.get("client.type", "desktop"),
   136   set localType(value) Svc.Prefs.set("client.type", value),
   138   isMobile: function isMobile(id) {
   139     if (this._store._remoteClients[id])
   140       return this._store._remoteClients[id].type == "mobile";
   141     return false;
   142   },
   144   _syncStartup: function _syncStartup() {
   145     // Reupload new client record periodically.
   146     if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
   147       this._tracker.addChangedID(this.localID);
   148       this.lastRecordUpload = Date.now() / 1000;
   149     }
   150     SyncEngine.prototype._syncStartup.call(this);
   151   },
   153   // Always process incoming items because they might have commands
   154   _reconcile: function _reconcile() {
   155     return true;
   156   },
   158   // Treat reset the same as wiping for locally cached clients
   159   _resetClient: function _resetClient() this._wipeClient(),
   161   _wipeClient: function _wipeClient() {
   162     SyncEngine.prototype._resetClient.call(this);
   163     this._store.wipe();
   164   },
   166   removeClientData: function removeClientData() {
   167     let res = this.service.resource(this.engineURL + "/" + this.localID);
   168     res.delete();
   169   },
   171   // Override the default behavior to delete bad records from the server.
   172   handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
   173     this._log.debug("Handling HMAC mismatch for " + item.id);
   175     let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
   176     if (base != SyncEngine.kRecoveryStrategy.error)
   177       return base;
   179     // It's a bad client record. Save it to be deleted at the end of the sync.
   180     this._log.debug("Bad client record detected. Scheduling for deletion.");
   181     this._deleteId(item.id);
   183     // Neither try again nor error; we're going to delete it.
   184     return SyncEngine.kRecoveryStrategy.ignore;
   185   },
   187   /**
   188    * A hash of valid commands that the client knows about. The key is a command
   189    * and the value is a hash containing information about the command such as
   190    * number of arguments and description.
   191    */
   192   _commands: {
   193     resetAll:    { args: 0, desc: "Clear temporary local data for all engines" },
   194     resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
   195     wipeAll:     { args: 0, desc: "Delete all client data for all engines" },
   196     wipeEngine:  { args: 1, desc: "Delete all client data for engine" },
   197     logout:      { args: 0, desc: "Log out client" },
   198     displayURI:  { args: 3, desc: "Instruct a client to display a URI" },
   199   },
   201   /**
   202    * Remove any commands for the local client and mark it for upload.
   203    */
   204   clearCommands: function clearCommands() {
   205     delete this.localCommands;
   206     this._tracker.addChangedID(this.localID);
   207   },
   209   /**
   210    * Sends a command+args pair to a specific client.
   211    *
   212    * @param command Command string
   213    * @param args Array of arguments/data for command
   214    * @param clientId Client to send command to
   215    */
   216   _sendCommandToClient: function sendCommandToClient(command, args, clientId) {
   217     this._log.trace("Sending " + command + " to " + clientId);
   219     let client = this._store._remoteClients[clientId];
   220     if (!client) {
   221       throw new Error("Unknown remote client ID: '" + clientId + "'.");
   222     }
   224     // notDupe compares two commands and returns if they are not equal.
   225     let notDupe = function(other) {
   226       return other.command != command || !Utils.deepEquals(other.args, args);
   227     };
   229     let action = {
   230       command: command,
   231       args: args,
   232     };
   234     if (!client.commands) {
   235       client.commands = [action];
   236     }
   237     // Add the new action if there are no duplicates.
   238     else if (client.commands.every(notDupe)) {
   239       client.commands.push(action);
   240     }
   241     // It must be a dupe. Skip.
   242     else {
   243       return;
   244     }
   246     this._log.trace("Client " + clientId + " got a new action: " + [command, args]);
   247     this._tracker.addChangedID(clientId);
   248   },
   250   /**
   251    * Check if the local client has any remote commands and perform them.
   252    *
   253    * @return false to abort sync
   254    */
   255   processIncomingCommands: function processIncomingCommands() {
   256     return this._notify("clients:process-commands", "", function() {
   257       let commands = this.localCommands;
   259       // Immediately clear out the commands as we've got them locally.
   260       this.clearCommands();
   262       // Process each command in order.
   263       for each ({command: command, args: args} in commands) {
   264         this._log.debug("Processing command: " + command + "(" + args + ")");
   266         let engines = [args[0]];
   267         switch (command) {
   268           case "resetAll":
   269             engines = null;
   270             // Fallthrough
   271           case "resetEngine":
   272             this.service.resetClient(engines);
   273             break;
   274           case "wipeAll":
   275             engines = null;
   276             // Fallthrough
   277           case "wipeEngine":
   278             this.service.wipeClient(engines);
   279             break;
   280           case "logout":
   281             this.service.logout();
   282             return false;
   283           case "displayURI":
   284             this._handleDisplayURI.apply(this, args);
   285             break;
   286           default:
   287             this._log.debug("Received an unknown command: " + command);
   288             break;
   289         }
   290       }
   292       return true;
   293     })();
   294   },
   296   /**
   297    * Validates and sends a command to a client or all clients.
   298    *
   299    * Calling this does not actually sync the command data to the server. If the
   300    * client already has the command/args pair, it won't receive a duplicate
   301    * command.
   302    *
   303    * @param command
   304    *        Command to invoke on remote clients
   305    * @param args
   306    *        Array of arguments to give to the command
   307    * @param clientId
   308    *        Client ID to send command to. If undefined, send to all remote
   309    *        clients.
   310    */
   311   sendCommand: function sendCommand(command, args, clientId) {
   312     let commandData = this._commands[command];
   313     // Don't send commands that we don't know about.
   314     if (!commandData) {
   315       this._log.error("Unknown command to send: " + command);
   316       return;
   317     }
   318     // Don't send a command with the wrong number of arguments.
   319     else if (!args || args.length != commandData.args) {
   320       this._log.error("Expected " + commandData.args + " args for '" +
   321                       command + "', but got " + args);
   322       return;
   323     }
   325     if (clientId) {
   326       this._sendCommandToClient(command, args, clientId);
   327     } else {
   328       for (let id in this._store._remoteClients) {
   329         this._sendCommandToClient(command, args, id);
   330       }
   331     }
   332   },
   334   /**
   335    * Send a URI to another client for display.
   336    *
   337    * A side effect is the score is increased dramatically to incur an
   338    * immediate sync.
   339    *
   340    * If an unknown client ID is specified, sendCommand() will throw an
   341    * Error object.
   342    *
   343    * @param uri
   344    *        URI (as a string) to send and display on the remote client
   345    * @param clientId
   346    *        ID of client to send the command to. If not defined, will be sent
   347    *        to all remote clients.
   348    * @param title
   349    *        Title of the page being sent.
   350    */
   351   sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
   352     this._log.info("Sending URI to client: " + uri + " -> " +
   353                    clientId + " (" + title + ")");
   354     this.sendCommand("displayURI", [uri, this.localID, title], clientId);
   356     this._tracker.score += SCORE_INCREMENT_XLARGE;
   357   },
   359   /**
   360    * Handle a single received 'displayURI' command.
   361    *
   362    * Interested parties should observe the "weave:engine:clients:display-uri"
   363    * topic. The callback will receive an object as the subject parameter with
   364    * the following keys:
   365    *
   366    *   uri       URI (string) that is requested for display.
   367    *   clientId  ID of client that sent the command.
   368    *   title     Title of page that loaded URI (likely) corresponds to.
   369    *
   370    * The 'data' parameter to the callback will not be defined.
   371    *
   372    * @param uri
   373    *        String URI that was received
   374    * @param clientId
   375    *        ID of client that sent URI
   376    * @param title
   377    *        String title of page that URI corresponds to. Older clients may not
   378    *        send this.
   379    */
   380   _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) {
   381     this._log.info("Received a URI for display: " + uri + " (" + title +
   382                    ") from " + clientId);
   384     let subject = {uri: uri, client: clientId, title: title};
   385     Svc.Obs.notify("weave:engine:clients:display-uri", subject);
   386   }
   387 };
   389 function ClientStore(name, engine) {
   390   Store.call(this, name, engine);
   391 }
   392 ClientStore.prototype = {
   393   __proto__: Store.prototype,
   395   create: function create(record) this.update(record),
   397   update: function update(record) {
   398     // Only grab commands from the server; local name/type always wins
   399     if (record.id == this.engine.localID)
   400       this.engine.localCommands = record.commands;
   401     else
   402       this._remoteClients[record.id] = record.cleartext;
   403   },
   405   createRecord: function createRecord(id, collection) {
   406     let record = new ClientsRec(collection, id);
   408     // Package the individual components into a record for the local client
   409     if (id == this.engine.localID) {
   410       record.name = this.engine.localName;
   411       record.type = this.engine.localType;
   412       record.commands = this.engine.localCommands;
   413       record.version = Services.appinfo.version;
   414       record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
   415     }
   416     else
   417       record.cleartext = this._remoteClients[id];
   419     return record;
   420   },
   422   itemExists: function itemExists(id) id in this.getAllIDs(),
   424   getAllIDs: function getAllIDs() {
   425     let ids = {};
   426     ids[this.engine.localID] = true;
   427     for (let id in this._remoteClients)
   428       ids[id] = true;
   429     return ids;
   430   },
   432   wipe: function wipe() {
   433     this._remoteClients = {};
   434   },
   435 };
   437 function ClientsTracker(name, engine) {
   438   Tracker.call(this, name, engine);
   439   Svc.Obs.add("weave:engine:start-tracking", this);
   440   Svc.Obs.add("weave:engine:stop-tracking", this);
   441 }
   442 ClientsTracker.prototype = {
   443   __proto__: Tracker.prototype,
   445   _enabled: false,
   447   observe: function observe(subject, topic, data) {
   448     switch (topic) {
   449       case "weave:engine:start-tracking":
   450         if (!this._enabled) {
   451           Svc.Prefs.observe("client.name", this);
   452           this._enabled = true;
   453         }
   454         break;
   455       case "weave:engine:stop-tracking":
   456         if (this._enabled) {
   457           Svc.Prefs.ignore("clients.name", this);
   458           this._enabled = false;
   459         }
   460         break;
   461       case "nsPref:changed":
   462         this._log.debug("client.name preference changed");
   463         this.addChangedID(Svc.Prefs.get("client.GUID"));
   464         this.score += SCORE_INCREMENT_XLARGE;
   465         break;
   466     }
   467   }
   468 };

mercurial