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

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

mercurial