Tue, 06 Jan 2015 21:39:09 +0100
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();