dom/alarm/AlarmService.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     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/. */
     5 "use strict";
     7 /* static functions */
     8 const DEBUG = false;
    10 function debug(aStr) {
    11   if (DEBUG)
    12     dump("AlarmService: " + aStr + "\n");
    13 }
    15 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
    17 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    18 Cu.import("resource://gre/modules/Services.jsm");
    19 Cu.import("resource://gre/modules/AlarmDB.jsm");
    21 this.EXPORTED_SYMBOLS = ["AlarmService"];
    23 XPCOMUtils.defineLazyGetter(this, "appsService", function() {
    24   return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
    25 });
    27 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
    28                                    "@mozilla.org/parentprocessmessagemanager;1",
    29                                    "nsIMessageListenerManager");
    31 XPCOMUtils.defineLazyGetter(this, "messenger", function() {
    32   return Cc["@mozilla.org/system-message-internal;1"].getService(Ci.nsISystemMessagesInternal);
    33 });
    35 XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() {
    36   return Cc["@mozilla.org/power/powermanagerservice;1"].getService(Ci.nsIPowerManagerService);
    37 });
    39 /**
    40  * AlarmService provides an API to schedule alarms using the device's RTC.
    41  *
    42  * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms)
    43  * which uses IPC to communicate with the service.
    44  *
    45  * AlarmService can also be used by Gecko code by importing the module and then
    46  * using AlarmService.add() and AlarmService.remove(). Only Gecko code running
    47  * in the parent process should do this.
    48  */
    50 this.AlarmService = {
    51   init: function init() {
    52     debug("init()");
    53     Services.obs.addObserver(this, "profile-change-teardown", false);
    54     Services.obs.addObserver(this, "webapps-clear-data",false);
    56     this._currentTimezoneOffset = (new Date()).getTimezoneOffset();
    58     let alarmHalService =
    59       this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"]
    60                               .getService(Ci.nsIAlarmHalService);
    62     alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this));
    63     alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this));
    65     // Add the messages to be listened to.
    66     this._messages = ["AlarmsManager:GetAll",
    67                       "AlarmsManager:Add",
    68                       "AlarmsManager:Remove"];
    69     this._messages.forEach(function addMessage(msgName) {
    70       ppmm.addMessageListener(msgName, this);
    71     }.bind(this));
    73     // Set the indexeddb database.
    74     this._db = new AlarmDB();
    75     this._db.init();
    77     // Variable to save alarms waiting to be set.
    78     this._alarmQueue = [];
    80     this._restoreAlarmsFromDb();
    81   },
    83   // Getter/setter to access the current alarm set in system.
    84   _alarm: null,
    85   get _currentAlarm() {
    86     return this._alarm;
    87   },
    88   set _currentAlarm(aAlarm) {
    89     this._alarm = aAlarm;
    90     if (!aAlarm) {
    91       return;
    92     }
    94     let alarmTimeInMs = this._getAlarmTime(aAlarm);
    95     let ns = (alarmTimeInMs % 1000) * 1000000;
    96     if (!this._alarmHalService.setAlarm(alarmTimeInMs / 1000, ns)) {
    97       throw Components.results.NS_ERROR_FAILURE;
    98     }
    99   },
   101   receiveMessage: function receiveMessage(aMessage) {
   102     debug("receiveMessage(): " + aMessage.name);
   103     let json = aMessage.json;
   105     // To prevent the hacked child process from sending commands to parent
   106     // to schedule alarms, we need to check its permission and manifest URL.
   107     if (this._messages.indexOf(aMessage.name) != -1) {
   108       if (!aMessage.target.assertPermission("alarms")) {
   109         debug("Got message from a child process with no 'alarms' permission.");
   110         return null;
   111       }
   112       if (!aMessage.target.assertContainApp(json.manifestURL)) {
   113         debug("Got message from a child process containing illegal manifest URL.");
   114         return null;
   115       }
   116     }
   118     let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
   119     switch (aMessage.name) {
   120       case "AlarmsManager:GetAll":
   121         this._db.getAll(
   122           json.manifestURL,
   123           function getAllSuccessCb(aAlarms) {
   124             debug("Callback after getting alarms from database: " +
   125                   JSON.stringify(aAlarms));
   126             this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms);
   127           }.bind(this),
   128           function getAllErrorCb(aErrorMsg) {
   129             this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg);
   130           }.bind(this)
   131         );
   132         break;
   134       case "AlarmsManager:Add":
   135         // Prepare a record for the new alarm to be added.
   136         let newAlarm = {
   137           date: json.date,
   138           ignoreTimezone: json.ignoreTimezone,
   139           data: json.data,
   140           pageURL: json.pageURL,
   141           manifestURL: json.manifestURL
   142         };
   144         this.add(newAlarm, null,
   145           // Receives the alarm ID as the last argument.
   146           this._sendAsyncMessage.bind(this, mm, "Add", true, json.requestId),
   147           // Receives the error message as the last argument.
   148           this._sendAsyncMessage.bind(this, mm, "Add", false, json.requestId)
   149         );
   150         break;
   152       case "AlarmsManager:Remove":
   153         this.remove(json.id, json.manifestURL);
   154         break;
   156       default:
   157         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   158         break;
   159     }
   160   },
   162   _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName,
   163                                                 aSuccess, aRequestId, aData) {
   164     debug("_sendAsyncMessage()");
   166     if (!aMessageManager) {
   167       debug("Invalid message manager: null");
   168       throw Components.results.NS_ERROR_FAILURE;
   169     }
   171     let json = null;
   172     switch (aMessageName)
   173     {
   174       case "Add":
   175         json = aSuccess ?
   176           { requestId: aRequestId, id: aData } :
   177           { requestId: aRequestId, errorMsg: aData };
   178         break;
   180       case "GetAll":
   181         json = aSuccess ?
   182           { requestId: aRequestId, alarms: aData } :
   183           { requestId: aRequestId, errorMsg: aData };
   184         break;
   186       default:
   187         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   188         break;
   189     }
   191     aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName +
   192                                      ":Return:" + (aSuccess ? "OK" : "KO"), json);
   193   },
   195   _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL,
   196                                                   aRemoveSuccessCb) {
   197     debug("_removeAlarmFromDb()");
   199     // If the aRemoveSuccessCb is undefined or null, set a dummy callback for
   200     // it which is needed for _db.remove().
   201     if (!aRemoveSuccessCb) {
   202       aRemoveSuccessCb = function removeSuccessCb() {
   203         debug("Remove alarm from DB successfully.");
   204       };
   205     }
   207     this._db.remove(
   208       aId,
   209       aManifestURL,
   210       aRemoveSuccessCb,
   211       function removeErrorCb(aErrorMsg) {
   212         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   213       }
   214     );
   215   },
   217   /**
   218    * Create a copy of the alarm that does not expose internal fields to
   219    * receivers and sticks to the public |respectTimezone| API rather than the
   220    * boolean |ignoreTimezone| field.
   221    */
   222   _publicAlarm: function _publicAlarm(aAlarm) {
   223     let alarm = {
   224       "id":              aAlarm.id,
   225       "date":            aAlarm.date,
   226       "respectTimezone": aAlarm.ignoreTimezone ?
   227                            "ignoreTimezone" : "honorTimezone",
   228       "data":            aAlarm.data
   229     };
   231     return alarm;
   232   },
   234   _fireSystemMessage: function _fireSystemMessage(aAlarm) {
   235     debug("Fire system message: " + JSON.stringify(aAlarm));
   237     let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null);
   238     let pageURI = Services.io.newURI(aAlarm.pageURL, null, null);
   240     messenger.sendMessage("alarm", this._publicAlarm(aAlarm),
   241                           pageURI, manifestURI);
   242   },
   244   _notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) {
   245     debug("_notifyAlarmObserver()");
   247     if (aAlarm.manifestURL) {
   248       this._fireSystemMessage(aAlarm);
   249     } else if (typeof aAlarm.alarmFiredCb === "function") {
   250       aAlarm.alarmFiredCb(this._publicAlarm(aAlarm));
   251     }
   252   },
   254   _onAlarmFired: function _onAlarmFired() {
   255     debug("_onAlarmFired()");
   257     if (this._currentAlarm) {
   258       this._removeAlarmFromDb(this._currentAlarm.id, null);
   259       this._notifyAlarmObserver(this._currentAlarm);
   260       this._currentAlarm = null;
   261     }
   263     // Reset the next alarm from the queue.
   264     let alarmQueue = this._alarmQueue;
   265     while (alarmQueue.length > 0) {
   266       let nextAlarm = alarmQueue.shift();
   267       let nextAlarmTime = this._getAlarmTime(nextAlarm);
   269       // If the next alarm has been expired, directly notify the observer.
   270       // it instead of setting it.
   271       if (nextAlarmTime <= Date.now()) {
   272         this._removeAlarmFromDb(nextAlarm.id, null);
   273         this._notifyAlarmObserver(nextAlarm);
   274       } else {
   275         this._currentAlarm = nextAlarm;
   276         break;
   277       }
   278     }
   279     this._debugCurrentAlarm();
   280   },
   282   _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) {
   283     debug("_onTimezoneChanged()");
   285     this._currentTimezoneOffset = aTimezoneOffset;
   286     this._restoreAlarmsFromDb();
   287   },
   289   _restoreAlarmsFromDb: function _restoreAlarmsFromDb() {
   290     debug("_restoreAlarmsFromDb()");
   292     this._db.getAll(
   293       null,
   294       function getAllSuccessCb(aAlarms) {
   295         debug("Callback after getting alarms from database: " +
   296               JSON.stringify(aAlarms));
   298         // Clear any alarms set or queued in the cache.
   299         let alarmQueue = this._alarmQueue;
   300         alarmQueue.length = 0;
   301         this._currentAlarm = null;
   303         // Only restore the alarm that's not yet expired; otherwise, remove it
   304         // from the database and notify the observer.
   305         aAlarms.forEach(function addAlarm(aAlarm) {
   306           if (this._getAlarmTime(aAlarm) > Date.now()) {
   307             alarmQueue.push(aAlarm);
   308           } else {
   309             this._removeAlarmFromDb(aAlarm.id, null);
   310             this._notifyAlarmObserver(aAlarm);
   311           }
   312         }.bind(this));
   314         // Set the next alarm from queue.
   315         if (alarmQueue.length) {
   316           alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
   317           this._currentAlarm = alarmQueue.shift();
   318         }
   320         this._debugCurrentAlarm();
   321       }.bind(this),
   322       function getAllErrorCb(aErrorMsg) {
   323         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   324       }
   325     );
   326   },
   328   _getAlarmTime: function _getAlarmTime(aAlarm) {
   329     // Avoid casting a Date object to a Date again to
   330     // preserve milliseconds. See bug 810973.
   331     let alarmTime;
   332     if (aAlarm.date instanceof Date) {
   333       alarmTime = aAlarm.date.getTime();
   334     } else {
   335       alarmTime = (new Date(aAlarm.date)).getTime();
   336     }
   338     // For an alarm specified with "ignoreTimezone", it must be fired respect
   339     // to the user's timezone.  Supposing an alarm was set at 7:00pm at Tokyo,
   340     // it must be gone off at 7:00pm respect to Paris' local time when the user
   341     // is located at Paris.  We can adjust the alarm UTC time by calculating
   342     // the difference of the orginal timezone and the current timezone.
   343     if (aAlarm.ignoreTimezone) {
   344        alarmTime += (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000;
   345     }
   346     return alarmTime;
   347   },
   349   _sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) {
   350     return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2);
   351   },
   353   _debugCurrentAlarm: function _debugCurrentAlarm() {
   354     debug("Current alarm: " + JSON.stringify(this._currentAlarm));
   355     debug("Alarm queue: " + JSON.stringify(this._alarmQueue));
   356   },
   358   /**
   359    *
   360    * Add a new alarm. This will set the RTC to fire at the selected date and
   361    * notify the caller. Notifications are delivered via System Messages if the
   362    * alarm is added on behalf of a app. Otherwise aAlarmFiredCb is called.
   363    *
   364    * @param object aNewAlarm
   365    *        Should contain the following literal properties:
   366    *          - |date| date: when the alarm should timeout.
   367    *          - |ignoreTimezone| boolean: See [1] for the details.
   368    *          - |manifestURL| string: Manifest of app on whose behalf the alarm
   369    *                                  is added.
   370    *          - |pageURL| string: The page in the app that receives the system
   371    *                              message.
   372    *          - |data| object [optional]: Data that can be stored in DB.
   373    * @param function aAlarmFiredCb
   374    *        Callback function invoked when the alarm is fired.
   375    *        It receives a single argument, the alarm object.
   376    *        May be null.
   377    * @param function aSuccessCb
   378    *        Callback function to receive an alarm ID (number).
   379    * @param function aErrorCb
   380    *        Callback function to receive an error message (string).
   381    * @returns void
   382    *
   383    * Notes:
   384    * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API
   385    */
   387   add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) {
   388     debug("add(" + aNewAlarm.date + ")");
   390     aSuccessCb = aSuccessCb || function() {};
   391     aErrorCb = aErrorCb || function() {};
   393     if (!aNewAlarm) {
   394       aErrorCb("alarm is null");
   395       return;
   396     }
   398     if (!aNewAlarm.date) {
   399       aErrorCb("alarm.date is null");
   400       return;
   401     }
   403     aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset;
   405     this._db.add(
   406       aNewAlarm,
   407       function addSuccessCb(aNewId) {
   408         debug("Callback after adding alarm in database.");
   410         aNewAlarm['id'] = aNewId;
   412         // Now that the alarm has been added to the database, we can tack on
   413         // the non-serializable callback to the in-memory object.
   414         aNewAlarm['alarmFiredCb'] = aAlarmFiredCb;
   416         // If there is no alarm being set in system, set the new alarm.
   417         if (this._currentAlarm == null) {
   418           this._currentAlarm = aNewAlarm;
   419           this._debugCurrentAlarm();
   420           aSuccessCb(aNewId);
   421           return;
   422         }
   424         // If the new alarm is earlier than the current alarm, swap them and
   425         // push the previous alarm back to queue.
   426         let alarmQueue = this._alarmQueue;
   427         let aNewAlarmTime = this._getAlarmTime(aNewAlarm);
   428         let currentAlarmTime = this._getAlarmTime(this._currentAlarm);
   429         if (aNewAlarmTime < currentAlarmTime) {
   430           alarmQueue.unshift(this._currentAlarm);
   431           this._currentAlarm = aNewAlarm;
   432           this._debugCurrentAlarm();
   433           aSuccessCb(aNewId);
   434           return;
   435         }
   437         // Push the new alarm in the queue.
   438         alarmQueue.push(aNewAlarm);
   439         alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
   440         this._debugCurrentAlarm();
   441         aSuccessCb(aNewId);
   442       }.bind(this),
   443       function addErrorCb(aErrorMsg) {
   444         aErrorCb(aErrorMsg);
   445       }.bind(this)
   446     );
   447   },
   449   /*
   450    * Remove the alarm associated with an ID.
   451    *
   452    * @param number aAlarmId
   453    *        The ID of the alarm to be removed.
   454    * @param string aManifestURL
   455    *        Manifest URL for application which added the alarm. (Optional)
   456    * @returns void
   457    */
   458   remove: function(aAlarmId, aManifestURL) {
   459     debug("remove(" + aAlarmId + ", " + aManifestURL + ")");
   460     this._removeAlarmFromDb(
   461       aAlarmId,
   462       aManifestURL,
   463       function removeSuccessCb() {
   464         debug("Callback after removing alarm from database.");
   466         // If there are no alarms set, nothing to do.
   467         if (!this._currentAlarm) {
   468           debug("No alarms set.");
   469           return;
   470         }
   472         // Check if the alarm to be removed is in the queue and whether it
   473         // belongs to the requesting app.
   474         let alarmQueue = this._alarmQueue;
   475         if (this._currentAlarm.id != aAlarmId ||
   476             this._currentAlarm.manifestURL != aManifestURL) {
   478           for (let i = 0; i < alarmQueue.length; i++) {
   479             if (alarmQueue[i].id == aAlarmId &&
   480                 alarmQueue[i].manifestURL == aManifestURL) {
   482               alarmQueue.splice(i, 1);
   483               break;
   484             }
   485           }
   486           this._debugCurrentAlarm();
   487           return;
   488         }
   490         // The alarm to be removed is the current alarm reset the next alarm
   491         // from queue if any.
   492         if (alarmQueue.length) {
   493           this._currentAlarm = alarmQueue.shift();
   494           this._debugCurrentAlarm();
   495           return;
   496         }
   498         // No alarm waiting to be set in the queue.
   499         this._currentAlarm = null;
   500         this._debugCurrentAlarm();
   501       }.bind(this)
   502     );
   503   },
   505   observe: function(aSubject, aTopic, aData) {
   506     switch (aTopic) {
   507       case "profile-change-teardown":
   508         this.uninit();
   509         break;
   510       case "webapps-clear-data":
   511         let params =
   512           aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
   513         if (!params) {
   514           debug("Error! Fail to remove alarms for an uninstalled app.");
   515           return;
   516         }
   518         // Only remove alarms for apps.
   519         if (params.browserOnly) {
   520           return;
   521         }
   523         let manifestURL = appsService.getManifestURLByLocalId(params.appId);
   524         if (!manifestURL) {
   525           debug("Error! Fail to remove alarms for an uninstalled app.");
   526           return;
   527         }
   529         this._db.getAll(
   530           manifestURL,
   531           function getAllSuccessCb(aAlarms) {
   532             aAlarms.forEach(function removeAlarm(aAlarm) {
   533               this.remove(aAlarm.id, manifestURL);
   534             }, this);
   535           }.bind(this),
   536           function getAllErrorCb(aErrorMsg) {
   537             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   538           }
   539         );
   540         break;
   541     }
   542   },
   544   uninit: function uninit() {
   545     debug("uninit()");
   546     Services.obs.removeObserver(this, "profile-change-teardown");
   547     Services.obs.removeObserver(this, "webapps-clear-data");
   549     this._messages.forEach(function(aMsgName) {
   550       ppmm.removeMessageListener(aMsgName, this);
   551     }.bind(this));
   552     ppmm = null;
   554     if (this._db) {
   555       this._db.close();
   556     }
   557     this._db = null;
   559     this._alarmHalService = null;
   560   }
   561 }
   563 AlarmService.init();

mercurial