|
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/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = [ |
|
6 "ClientEngine", |
|
7 "ClientsRec" |
|
8 ]; |
|
9 |
|
10 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
11 |
|
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"); |
|
17 |
|
18 const CLIENTS_TTL = 1814400; // 21 days |
|
19 const CLIENTS_TTL_REFRESH = 604800; // 7 days |
|
20 |
|
21 const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"]; |
|
22 |
|
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 }; |
|
31 |
|
32 Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands", "version", "protocols"]); |
|
33 |
|
34 |
|
35 this.ClientEngine = function ClientEngine(service) { |
|
36 SyncEngine.call(this, "Clients", service); |
|
37 |
|
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, |
|
46 |
|
47 // Always sync client data as it controls other sync behavior |
|
48 get enabled() true, |
|
49 |
|
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 }, |
|
56 |
|
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 }; |
|
64 |
|
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 } |
|
70 |
|
71 return stats; |
|
72 }, |
|
73 |
|
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(); |
|
81 |
|
82 counts.set(this.localType, 1); |
|
83 |
|
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 } |
|
89 |
|
90 counts.set(type, counts.get(type) + 1); |
|
91 } |
|
92 |
|
93 return counts; |
|
94 }, |
|
95 |
|
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), |
|
102 |
|
103 get localName() { |
|
104 let localName = Svc.Prefs.get("client.name", ""); |
|
105 if (localName != "") |
|
106 return localName; |
|
107 |
|
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"); |
|
113 |
|
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; |
|
122 |
|
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; |
|
130 |
|
131 return this.localName = Str.sync.get("client.name2", [user, appName, system]); |
|
132 }, |
|
133 set localName(value) Svc.Prefs.set("client.name", value), |
|
134 |
|
135 get localType() Svc.Prefs.get("client.type", "desktop"), |
|
136 set localType(value) Svc.Prefs.set("client.type", value), |
|
137 |
|
138 isMobile: function isMobile(id) { |
|
139 if (this._store._remoteClients[id]) |
|
140 return this._store._remoteClients[id].type == "mobile"; |
|
141 return false; |
|
142 }, |
|
143 |
|
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 }, |
|
152 |
|
153 // Always process incoming items because they might have commands |
|
154 _reconcile: function _reconcile() { |
|
155 return true; |
|
156 }, |
|
157 |
|
158 // Treat reset the same as wiping for locally cached clients |
|
159 _resetClient: function _resetClient() this._wipeClient(), |
|
160 |
|
161 _wipeClient: function _wipeClient() { |
|
162 SyncEngine.prototype._resetClient.call(this); |
|
163 this._store.wipe(); |
|
164 }, |
|
165 |
|
166 removeClientData: function removeClientData() { |
|
167 let res = this.service.resource(this.engineURL + "/" + this.localID); |
|
168 res.delete(); |
|
169 }, |
|
170 |
|
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); |
|
174 |
|
175 let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry); |
|
176 if (base != SyncEngine.kRecoveryStrategy.error) |
|
177 return base; |
|
178 |
|
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); |
|
182 |
|
183 // Neither try again nor error; we're going to delete it. |
|
184 return SyncEngine.kRecoveryStrategy.ignore; |
|
185 }, |
|
186 |
|
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 }, |
|
200 |
|
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 }, |
|
208 |
|
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); |
|
218 |
|
219 let client = this._store._remoteClients[clientId]; |
|
220 if (!client) { |
|
221 throw new Error("Unknown remote client ID: '" + clientId + "'."); |
|
222 } |
|
223 |
|
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 }; |
|
228 |
|
229 let action = { |
|
230 command: command, |
|
231 args: args, |
|
232 }; |
|
233 |
|
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 } |
|
245 |
|
246 this._log.trace("Client " + clientId + " got a new action: " + [command, args]); |
|
247 this._tracker.addChangedID(clientId); |
|
248 }, |
|
249 |
|
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; |
|
258 |
|
259 // Immediately clear out the commands as we've got them locally. |
|
260 this.clearCommands(); |
|
261 |
|
262 // Process each command in order. |
|
263 for each ({command: command, args: args} in commands) { |
|
264 this._log.debug("Processing command: " + command + "(" + args + ")"); |
|
265 |
|
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 } |
|
291 |
|
292 return true; |
|
293 })(); |
|
294 }, |
|
295 |
|
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 } |
|
324 |
|
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 }, |
|
333 |
|
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); |
|
355 |
|
356 this._tracker.score += SCORE_INCREMENT_XLARGE; |
|
357 }, |
|
358 |
|
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); |
|
383 |
|
384 let subject = {uri: uri, client: clientId, title: title}; |
|
385 Svc.Obs.notify("weave:engine:clients:display-uri", subject); |
|
386 } |
|
387 }; |
|
388 |
|
389 function ClientStore(name, engine) { |
|
390 Store.call(this, name, engine); |
|
391 } |
|
392 ClientStore.prototype = { |
|
393 __proto__: Store.prototype, |
|
394 |
|
395 create: function create(record) this.update(record), |
|
396 |
|
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 }, |
|
404 |
|
405 createRecord: function createRecord(id, collection) { |
|
406 let record = new ClientsRec(collection, id); |
|
407 |
|
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]; |
|
418 |
|
419 return record; |
|
420 }, |
|
421 |
|
422 itemExists: function itemExists(id) id in this.getAllIDs(), |
|
423 |
|
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 }, |
|
431 |
|
432 wipe: function wipe() { |
|
433 this._remoteClients = {}; |
|
434 }, |
|
435 }; |
|
436 |
|
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, |
|
444 |
|
445 _enabled: false, |
|
446 |
|
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 }; |