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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
10 //dump('DEBUG RequestSyncService: ' + s + '\n');
13 const RSYNCDB_VERSION = 1;
14 const RSYNCDB_NAME = "requestSync";
15 const RSYNC_MIN_INTERVAL = 100;
17 const RSYNC_OPERATION_TIMEOUT = 120000 // 2 minutes
19 const RSYNC_STATE_ENABLED = "enabled";
20 const RSYNC_STATE_DISABLED = "disabled";
21 const RSYNC_STATE_WIFIONLY = "wifiOnly";
23 Cu.import('resource://gre/modules/IndexedDBHelper.jsm');
24 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
25 Cu.import("resource://gre/modules/Services.jsm");
26 Cu.importGlobalProperties(["indexedDB"]);
29 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
30 "@mozilla.org/AppsService;1",
33 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
34 "@mozilla.org/childprocessmessagemanager;1",
35 "nsISyncMessageSender");
37 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
38 "@mozilla.org/parentprocessmessagemanager;1",
39 "nsIMessageBroadcaster");
41 XPCOMUtils.defineLazyServiceGetter(this, "systemMessenger",
42 "@mozilla.org/system-message-internal;1",
43 "nsISystemMessagesInternal");
45 XPCOMUtils.defineLazyServiceGetter(this, "secMan",
46 "@mozilla.org/scriptsecuritymanager;1",
47 "nsIScriptSecurityManager");
49 XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
50 "resource://gre/modules/AlarmService.jsm");
52 this.RequestSyncService = {
53 __proto__: IndexedDBHelper.prototype,
57 _messages: [ "RequestSync:Register",
58 "RequestSync:Unregister",
59 "RequestSync:Registrations",
60 "RequestSync:Registration",
61 "RequestSyncManager:Registrations",
62 "RequestSyncManager:SetPolicy",
63 "RequestSyncManager:RunTask" ],
65 _pendingOperation: false,
78 // Initialization of the RequestSyncService.
82 this._messages.forEach((function(msgName) {
83 ppmm.addMessageListener(msgName, this);
86 Services.obs.addObserver(this, 'xpcom-shutdown', false);
87 Services.obs.addObserver(this, 'webapps-clear-data', false);
88 Services.obs.addObserver(this, 'wifi-state-changed', false);
90 this.initDBHelper("requestSync", RSYNCDB_VERSION, [RSYNCDB_NAME]);
92 // Loading all the data from the database into the _registrations map.
93 // Any incoming message will be stored and processed when the async
94 // operation is completed.
97 this.dbTxn("readonly", function(aStore) {
98 aStore.openCursor().onsuccess = function(event) {
99 let cursor = event.target.result;
101 self.addRegistration(cursor.value);
107 debug("initialization done");
110 dump("ERROR!! RequestSyncService - Failed to retrieve data from the database.\n");
114 // Shutdown the RequestSyncService.
115 shutdown: function() {
118 this._messages.forEach((function(msgName) {
119 ppmm.removeMessageListener(msgName, this);
122 Services.obs.removeObserver(this, 'xpcom-shutdown');
123 Services.obs.removeObserver(this, 'webapps-clear-data');
124 Services.obs.removeObserver(this, 'wifi-state-changed');
128 // Removing all the registrations will delete the pending timers.
130 this.forEachRegistration(function(aObj) {
131 let key = self.principalToKey(aObj.principal);
132 self.removeRegistrationInternal(aObj.data.task, key);
136 observe: function(aSubject, aTopic, aData) {
140 case 'xpcom-shutdown':
144 case 'webapps-clear-data':
145 this.clearData(aSubject);
148 case 'wifi-state-changed':
149 this.wifiStateChanged(aSubject == 'enabled');
153 debug("Wrong observer topic: " + aTopic);
158 // When an app is uninstalled, we have to clean all its tasks.
159 clearData: function(aData) {
167 aData.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
172 // At this point we don't have the origin, so we cannot create the full
173 // key. Using the partial one is enough to detect the uninstalled app.
174 let partialKey = params.appId + '|' + params.browserOnly + '|';
177 for (let key in this._registrations) {
178 if (key.indexOf(partialKey) != 0) {
182 for (let task in this._registrations[key]) {
183 dbKeys = this._registrations[key][task].dbKey;
184 this.removeRegistrationInternal(task, key);
188 if (dbKeys.length == 0) {
192 // Remove the tasks from the database.
193 this.dbTxn('readwrite', function(aStore) {
194 for (let i = 0; i < dbKeys.length; ++i) {
195 aStore.delete(dbKeys[i]);
199 debug("ClearData completed");
201 debug("ClearData failed");
205 // Creation of the schema for the database.
206 upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
207 debug('updateSchema');
208 aDb.createObjectStore(RSYNCDB_NAME, { autoIncrement: true });
211 // This method generates the key for the indexedDB object storage.
212 principalToKey: function(aPrincipal) {
213 return aPrincipal.appId + '|' +
214 aPrincipal.isInBrowserElement + '|' +
218 // Add a task to the _registrations map and create the timer if it's needed.
219 addRegistration: function(aObj) {
220 debug('addRegistration');
222 let key = this.principalToKey(aObj.principal);
223 if (!(key in this._registrations)) {
224 this._registrations[key] = {};
227 this.scheduleTimer(aObj);
228 this._registrations[key][aObj.data.task] = aObj;
231 // Remove a task from the _registrations map and delete the timer if it's
232 // needed. It also checks if the principal is correct before doing the real
234 removeRegistration: function(aTaskName, aKey, aPrincipal) {
235 debug('removeRegistration');
237 if (!(aKey in this._registrations) ||
238 !(aTaskName in this._registrations[aKey])) {
242 // Additional security check.
243 if (!aPrincipal.equals(this._registrations[aKey][aTaskName].principal)) {
247 this.removeRegistrationInternal(aTaskName, aKey);
251 removeRegistrationInternal: function(aTaskName, aKey) {
252 debug('removeRegistrationInternal');
254 let obj = this._registrations[aKey][aTaskName];
256 this.removeTimer(obj);
258 // It can be that this task has been already schedulated.
259 this.removeTaskFromQueue(obj);
261 // It can be that this object is already in scheduled, or in the queue of a
262 // iDB transacation. In order to avoid rescheduling it, we must disable it.
265 delete this._registrations[aKey][aTaskName];
267 // Lets remove the key in case there are not tasks registered.
268 for (let key in this._registrations[aKey]) {
271 delete this._registrations[aKey];
274 removeTaskFromQueue: function(aObj) {
275 let pos = this._queuedTasks.indexOf(aObj);
277 this._queuedTasks.splice(pos, 1);
281 // The communication from the exposed objects and the service is done using
282 // messages. This function receives and processes them.
283 receiveMessage: function(aMessage) {
284 debug("receiveMessage");
286 // We cannot process this request now.
287 if (this._pendingOperation) {
288 this._pendingMessages.push(aMessage);
292 // The principal is used to validate the message.
293 if (!aMessage.principal) {
297 let uri = Services.io.newURI(aMessage.principal.origin, null, null);
301 principal = secMan.getAppCodebasePrincipal(uri,
302 aMessage.principal.appId, aMessage.principal.isInBrowserElement);
311 switch (aMessage.name) {
312 case "RequestSync:Register":
313 this.register(aMessage.target, aMessage.data, principal);
316 case "RequestSync:Unregister":
317 this.unregister(aMessage.target, aMessage.data, principal);
320 case "RequestSync:Registrations":
321 this.registrations(aMessage.target, aMessage.data, principal);
324 case "RequestSync:Registration":
325 this.registration(aMessage.target, aMessage.data, principal);
328 case "RequestSyncManager:Registrations":
329 this.managerRegistrations(aMessage.target, aMessage.data, principal);
332 case "RequestSyncManager:SetPolicy":
333 this.managerSetPolicy(aMessage.target, aMessage.data, principal);
336 case "RequestSyncManager:RunTask":
337 this.managerRunTask(aMessage.target, aMessage.data, principal);
341 debug("Wrong message: " + aMessage.name);
347 validateRegistrationParams: function(aParams) {
348 if (aParams === null) {
352 // We must have a page.
353 if (!("wakeUpPage" in aParams) ||
354 aParams.wakeUpPage.length == 0) {
358 let minInterval = RSYNC_MIN_INTERVAL;
360 minInterval = Services.prefs.getIntPref("dom.requestSync.minInterval");
363 if (!("minInterval" in aParams) ||
364 aParams.minInterval < minInterval) {
371 // Registration of a new task.
372 register: function(aTarget, aData, aPrincipal) {
375 if (!this.validateRegistrationParams(aData.params)) {
376 aTarget.sendAsyncMessage("RequestSync:Register:Return",
377 { requestID: aData.requestID,
378 error: "ParamsError" } );
382 let key = this.principalToKey(aPrincipal);
383 if (key in this._registrations &&
384 aData.task in this._registrations[key]) {
385 // if this task already exists we overwrite it.
386 this.removeRegistrationInternal(aData.task, key);
389 // This creates a RequestTaskFull object.
390 aData.params.task = aData.task;
391 aData.params.lastSync = 0;
392 aData.params.principal = aPrincipal;
394 aData.params.state = RSYNC_STATE_ENABLED;
395 if (aData.params.wifiOnly) {
396 aData.params.state = RSYNC_STATE_WIFIONLY;
399 aData.params.overwrittenMinInterval = 0;
401 let dbKey = aData.task + "|" +
402 aPrincipal.appId + '|' +
403 aPrincipal.isInBrowserElement + '|' +
406 let data = { principal: aPrincipal,
412 this.dbTxn('readwrite', function(aStore) {
413 aStore.put(data, data.dbKey);
416 self.addRegistration(data);
417 aTarget.sendAsyncMessage("RequestSync:Register:Return",
418 { requestID: aData.requestID });
421 aTarget.sendAsyncMessage("RequestSync:Register:Return",
422 { requestID: aData.requestID,
423 error: "IndexDBError" } );
427 // Unregister a task.
428 unregister: function(aTarget, aData, aPrincipal) {
431 let key = this.principalToKey(aPrincipal);
432 if (!(key in this._registrations) ||
433 !(aData.task in this._registrations[key])) {
434 aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
435 { requestID: aData.requestID,
436 error: "UnknownTaskError" });
440 let dbKey = this._registrations[key][aData.task].dbKey;
441 this.removeRegistration(aData.task, key, aPrincipal);
444 this.dbTxn('readwrite', function(aStore) {
445 aStore.delete(dbKey);
448 aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
449 { requestID: aData.requestID });
452 aTarget.sendAsyncMessage("RequestSync:Unregister:Return",
453 { requestID: aData.requestID,
454 error: "IndexDBError" } );
458 // Get the list of registered tasks for this principal.
459 registrations: function(aTarget, aData, aPrincipal) {
460 debug("registrations");
463 let key = this.principalToKey(aPrincipal);
464 if (key in this._registrations) {
465 for (let i in this._registrations[key]) {
466 results.push(this.createPartialTaskObject(
467 this._registrations[key][i].data));
471 aTarget.sendAsyncMessage("RequestSync:Registrations:Return",
472 { requestID: aData.requestID,
476 // Get a particular registered task for this principal.
477 registration: function(aTarget, aData, aPrincipal) {
478 debug("registration");
481 let key = this.principalToKey(aPrincipal);
482 if (key in this._registrations &&
483 aData.task in this._registrations[key]) {
484 results = this.createPartialTaskObject(
485 this._registrations[key][aData.task].data);
488 aTarget.sendAsyncMessage("RequestSync:Registration:Return",
489 { requestID: aData.requestID,
493 // Get the list of the registered tasks.
494 managerRegistrations: function(aTarget, aData, aPrincipal) {
495 debug("managerRegistrations");
499 this.forEachRegistration(function(aObj) {
500 results.push(self.createFullTaskObject(aObj.data));
503 aTarget.sendAsyncMessage("RequestSyncManager:Registrations:Return",
504 { requestID: aData.requestID,
508 // Set a policy to a task.
509 managerSetPolicy: function(aTarget, aData, aPrincipal) {
510 debug("managerSetPolicy");
514 this.forEachRegistration(function(aObj) {
515 if (aObj.data.task != aData.task) {
519 if (aObj.principal.isInBrowserElement != aData.isInBrowserElement ||
520 aObj.principal.origin != aData.origin) {
524 let app = appsService.getAppByLocalId(aObj.principal.appId);
525 if (app && app.manifestURL != aData.manifestURL ||
526 (!app && aData.manifestURL != "")) {
530 if ("overwrittenMinInterval" in aData) {
531 aObj.data.overwrittenMinInterval = aData.overwrittenMinInterval;
534 aObj.data.state = aData.state;
537 dump("ERROR!! RequestSyncService - SetPolicy matches more than 1 task.\n");
545 aTarget.sendAsyncMessage("RequestSyncManager:SetPolicy:Return",
546 { requestID: aData.requestID, error: "UnknownTaskError" });
550 this.updateObjectInDB(toSave, function() {
551 self.scheduleTimer(toSave);
552 aTarget.sendAsyncMessage("RequestSyncManager:SetPolicy:Return",
553 { requestID: aData.requestID });
558 managerRunTask: function(aTarget, aData, aPrincipal) {
562 this.forEachRegistration(function(aObj) {
563 if (aObj.data.task != aData.task) {
567 if (aObj.principal.isInBrowserElement != aData.isInBrowserElement ||
568 aObj.principal.origin != aData.origin) {
572 let app = appsService.getAppByLocalId(aObj.principal.appId);
573 if (app && app.manifestURL != aData.manifestURL ||
574 (!app && aData.manifestURL != "")) {
579 dump("ERROR!! RequestSyncService - RunTask matches more than 1 task.\n");
587 aTarget.sendAsyncMessage("RequestSyncManager:RunTask:Return",
588 { requestID: aData.requestID, error: "UnknownTaskError" });
592 // Storing the requestID into the task for the callback.
593 this.storePendingRequest(task, aTarget, aData.requestID);
597 // We cannot expose the full internal object to content but just a subset.
598 // This method creates this subset.
599 createPartialTaskObject: function(aObj) {
600 return { task: aObj.task,
601 lastSync: aObj.lastSync,
602 oneShot: aObj.oneShot,
603 minInterval: aObj.minInterval,
604 wakeUpPage: aObj.wakeUpPage,
605 wifiOnly: aObj.wifiOnly,
609 createFullTaskObject: function(aObj) {
610 let obj = this.createPartialTaskObject(aObj);
612 obj.app = { manifestURL: '',
613 origin: aObj.principal.origin,
614 isInBrowserElement: aObj.principal.isInBrowserElement };
616 let app = appsService.getAppByLocalId(aObj.principal.appId);
618 obj.app.manifestURL = app.manifestURL;
621 obj.state = aObj.state;
622 obj.overwrittenMinInterval = aObj.overwrittenMinInterval;
626 // Creation of the timer for a particular task object.
627 scheduleTimer: function(aObj) {
628 debug("scheduleTimer");
630 this.removeTimer(aObj);
632 // A registration can be already inactive if it was 1 shot.
637 if (aObj.data.state == RSYNC_STATE_DISABLED) {
642 if (aObj.data.state == RSYNC_STATE_WIFIONLY && !this._wifi) {
646 this.createTimer(aObj);
649 timeout: function(aObj) {
652 if (this._activeTask) {
653 debug("queueing tasks");
654 // We have an active task, let's queue this as next task.
655 if (this._queuedTasks.indexOf(aObj) == -1) {
656 this._queuedTasks.push(aObj);
661 let app = appsService.getAppByLocalId(aObj.principal.appId);
663 dump("ERROR!! RequestSyncService - Failed to retrieve app data from a principal.\n");
665 this.updateObjectInDB(aObj);
669 let manifestURL = Services.io.newURI(app.manifestURL, null, null);
670 let pageURL = Services.io.newURI(aObj.data.wakeUpPage, null, aObj.principal.URI);
672 // Maybe need to be rescheduled?
673 if (this.hasPendingMessages('request-sync', manifestURL, pageURL)) {
674 this.scheduleTimer(aObj);
678 this.removeTimer(aObj);
679 this._activeTask = aObj;
681 if (!manifestURL || !pageURL) {
682 dump("ERROR!! RequestSyncService - Failed to create URI for the page or the manifest\n");
684 this.updateObjectInDB(aObj);
688 // We don't want to run more than 1 task at the same time. We do this using
689 // the promise created by sendMessage(). But if the task takes more than
690 // RSYNC_OPERATION_TIMEOUT millisecs, we have to ignore the promise and
691 // continue processing other tasks.
693 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
697 function taskCompleted() {
698 debug("promise or timeout for task calls taskCompleted");
702 self.operationCompleted();
709 let timeout = RSYNC_OPERATION_TIMEOUT;
711 let tmp = Services.prefs.getIntPref("dom.requestSync.maxTaskTimeout");
715 timer.initWithCallback(function() {
716 debug("Task is taking too much, let's ignore the promise.");
718 }, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
720 // Sending the message.
721 debug("Sending message.");
723 systemMessenger.sendMessage('request-sync',
724 this.createPartialTaskObject(aObj.data),
725 pageURL, manifestURL);
727 promise.then(function() {
728 debug("promise resolved");
731 debug("promise rejected");
736 operationCompleted: function() {
737 debug("operationCompleted");
739 if (!this._activeTask) {
740 dump("ERROR!! RequestSyncService - OperationCompleted called without an active task\n");
744 // One shot? Then this is not active.
745 this._activeTask.active = !this._activeTask.data.oneShot;
746 this._activeTask.data.lastSync = new Date();
748 let pendingRequests = this.stealPendingRequests(this._activeTask);
749 for (let i = 0; i < pendingRequests.length; ++i) {
751 .target.sendAsyncMessage("RequestSyncManager:RunTask:Return",
752 { requestID: pendingRequests[i].requestID });
756 this.updateObjectInDB(this._activeTask, function() {
757 // SchedulerTimer creates a timer and a nsITimer cannot be cloned. This
758 // is the reason why this operation has to be done after storing the task
760 if (!self._activeTask.data.oneShot) {
761 self.scheduleTimer(self._activeTask);
764 self.processNextTask();
768 processNextTask: function() {
769 debug("processNextTask");
771 this._activeTask = null;
773 if (this._queuedTasks.length == 0) {
777 let task = this._queuedTasks.shift();
781 hasPendingMessages: function(aMessageName, aManifestURL, aPageURL) {
782 let hasPendingMessages =
783 cpmm.sendSyncMessage("SystemMessageManager:HasPendingMessages",
784 { type: aMessageName,
785 pageURL: aPageURL.spec,
786 manifestURL: aManifestURL.spec })[0];
788 debug("Pending messages: " + hasPendingMessages);
789 return hasPendingMessages;
792 // Update the object into the database.
793 updateObjectInDB: function(aObj, aCb) {
794 debug("updateObjectInDB");
796 this.dbTxn('readwrite', function(aStore) {
797 aStore.put(aObj, aObj.dbKey);
803 debug("UpdateObjectInDB completed");
805 debug("UpdateObjectInDB failed");
809 pendingOperationStarted: function() {
810 debug('pendingOperationStarted');
811 this._pendingOperation = true;
814 pendingOperationDone: function() {
815 debug('pendingOperationDone');
817 this._pendingOperation = false;
819 // managing the pending messages now that the initialization is completed.
820 while (this._pendingMessages.length && !this._pendingOperation) {
821 this.receiveMessage(this._pendingMessages.shift());
825 // This method creates a transaction and runs callbacks. Plus it manages the
826 // pending operations system.
827 dbTxn: function(aType, aCb, aSuccessCb, aErrorCb) {
830 this.pendingOperationStarted();
833 this.newTxn(aType, RSYNCDB_NAME, function(aTxn, aStore) {
837 self.pendingOperationDone();
841 self.pendingOperationDone();
846 forEachRegistration: function(aCb) {
847 // This method is used also to remove registations from the map, so we have
848 // to make a new list and let _registations free to be used.
850 for (let key in this._registrations) {
851 for (let task in this._registrations[key]) {
852 list.push(this._registrations[key][task]);
856 for (let i = 0; i < list.length; ++i) {
861 wifiStateChanged: function(aEnabled) {
862 debug("onWifiStateChanged");
863 this._wifi = aEnabled;
866 // Disable all the wifiOnly tasks.
868 this.forEachRegistration(function(aObj) {
869 if (aObj.data.state == RSYNC_STATE_WIFIONLY && self.hasTimer(aObj)) {
870 self.removeTimer(aObj);
872 // It can be that this task has been already schedulated.
873 self.removeTaskFromQueue(aObj);
879 // Enable all the tasks.
881 this.forEachRegistration(function(aObj) {
882 if (aObj.active && !self.hasTimer(aObj)) {
883 if (!aObj.data.wifiOnly) {
884 dump("ERROR - Found a disabled task that is not wifiOnly.");
887 self.scheduleTimer(aObj);
892 createTimer: function(aObj) {
893 let interval = aObj.data.minInterval;
894 if (aObj.data.overwrittenMinInterval > 0) {
895 interval = aObj.data.overwrittenMinInterval;
899 { date: new Date(Date.now() + interval * 1000),
900 ignoreTimezone: false },
901 () => this.timeout(aObj),
902 aTimerId => this._timers[aObj.dbKey] = aTimerId);
905 hasTimer: function(aObj) {
906 return (aObj.dbKey in this._timers);
909 removeTimer: function(aObj) {
910 if (aObj.dbKey in this._timers) {
911 AlarmService.remove(this._timers[aObj.dbKey]);
912 delete this._timers[aObj.dbKey];
916 storePendingRequest: function(aObj, aTarget, aRequestID) {
917 if (!(aObj.dbKey in this._pendingRequests)) {
918 this._pendingRequests[aObj.dbKey] = [];
921 this._pendingRequests[aObj.dbKey].push({ target: aTarget,
922 requestID: aRequestID });
925 stealPendingRequests: function(aObj) {
926 if (!(aObj.dbKey in this._pendingRequests)) {
930 let requests = this._pendingRequests[aObj.dbKey];
931 delete this._pendingRequests[aObj.dbKey];
936 RequestSyncService.init();
938 this.EXPORTED_SYMBOLS = [""];