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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6 * This file is in transition. It was originally conceived to fulfill the
7 * needs of only Firefox Health Report. It is slowly being morphed into
8 * fulfilling the needs of all data reporting facilities in Gecko applications.
9 * As a result, some things feel a bit weird.
10 *
11 * DataReportingPolicy is both a driver for data reporting notification
12 * (a true policy) and the driver for FHR data submission. The latter should
13 * eventually be split into its own type and module.
14 */
16 "use strict";
18 #ifndef MERGED_COMPARTMENT
20 this.EXPORTED_SYMBOLS = [
21 "DataSubmissionRequest", // For test use only.
22 "DataReportingPolicy",
23 ];
25 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
27 #endif
29 Cu.import("resource://gre/modules/Services.jsm");
30 Cu.import("resource://gre/modules/Promise.jsm");
31 Cu.import("resource://gre/modules/Log.jsm");
32 Cu.import("resource://services-common/utils.js");
33 Cu.import("resource://gre/modules/UpdateChannel.jsm");
35 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
37 // Used as a sanity lower bound for dates stored in prefs. This module was
38 // implemented in 2012, so any earlier dates indicate an incorrect clock.
39 const OLDEST_ALLOWED_YEAR = 2012;
41 /**
42 * Represents a request to display data policy.
43 *
44 * Instances of this are created when the policy is requesting the user's
45 * approval to agree to the data submission policy.
46 *
47 * Receivers of these instances are expected to call one or more of the on*
48 * functions when events occur.
49 *
50 * When one of these requests is received, the first thing a callee should do
51 * is present notification to the user of the data policy. When the notice
52 * is displayed to the user, the callee should call `onUserNotifyComplete`.
53 * This begins a countdown timer that upon completion will signal implicit
54 * acceptance of the policy. If for whatever reason the callee could not
55 * display a notice, it should call `onUserNotifyFailed`.
56 *
57 * Once the user is notified of the policy, the callee has the option of
58 * signaling explicit user acceptance or rejection of the policy. They do this
59 * by calling `onUserAccept` or `onUserReject`, respectively. These functions
60 * are essentially proxies to
61 * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
62 *
63 * If the user never explicitly accepts or rejects the policy, it will be
64 * implicitly accepted after a specified duration of time. The notice is
65 * expected to remain displayed even after implicit acceptance (in case the
66 * user is away from the device). So, no event signaling implicit acceptance
67 * is exposed.
68 *
69 * Receivers of instances of this type should treat it as a black box with
70 * the exception of the on* functions.
71 *
72 * @param policy
73 * (DataReportingPolicy) The policy instance this request came from.
74 * @param deferred
75 * (deferred) The promise that will be fulfilled when display occurs.
76 */
77 function NotifyPolicyRequest(policy, deferred) {
78 this.policy = policy;
79 this.deferred = deferred;
80 }
81 NotifyPolicyRequest.prototype = {
82 /**
83 * Called when the user is notified of the policy.
84 *
85 * This starts a countdown timer that will eventually signify implicit
86 * acceptance of the data policy.
87 */
88 onUserNotifyComplete: function onUserNotified() {
89 this.deferred.resolve();
90 return this.deferred.promise;
91 },
93 /**
94 * Called when there was an error notifying the user about the policy.
95 *
96 * @param error
97 * (Error) Explains what went wrong.
98 */
99 onUserNotifyFailed: function onUserNotifyFailed(error) {
100 this.deferred.reject(error);
101 },
103 /**
104 * Called when the user agreed to the data policy.
105 *
106 * @param reason
107 * (string) How the user agreed to the policy.
108 */
109 onUserAccept: function onUserAccept(reason) {
110 this.policy.recordUserAcceptance(reason);
111 },
113 /**
114 * Called when the user rejected the data policy.
115 *
116 * @param reason
117 * (string) How the user rejected the policy.
118 */
119 onUserReject: function onUserReject(reason) {
120 this.policy.recordUserRejection(reason);
121 },
122 };
124 Object.freeze(NotifyPolicyRequest.prototype);
126 /**
127 * Represents a request to submit data.
128 *
129 * Instances of this are created when the policy requests data upload or
130 * deletion.
131 *
132 * Receivers are expected to call one of the provided on* functions to signal
133 * completion of the request.
134 *
135 * Instances of this type should not be instantiated outside of this file.
136 * Receivers of instances of this type should not attempt to do anything with
137 * the instance except call one of the on* methods.
138 */
139 this.DataSubmissionRequest = function (promise, expiresDate, isDelete) {
140 this.promise = promise;
141 this.expiresDate = expiresDate;
142 this.isDelete = isDelete;
144 this.state = null;
145 this.reason = null;
146 }
148 this.DataSubmissionRequest.prototype = Object.freeze({
149 NO_DATA_AVAILABLE: "no-data-available",
150 SUBMISSION_SUCCESS: "success",
151 SUBMISSION_FAILURE_SOFT: "failure-soft",
152 SUBMISSION_FAILURE_HARD: "failure-hard",
153 UPLOAD_IN_PROGRESS: "upload-in-progress",
155 /**
156 * No submission was attempted because no data was available.
157 *
158 * In the case of upload, this means there is no data to upload (perhaps
159 * it isn't available yet). In case of remote deletion, it means that there
160 * is no remote data to delete.
161 */
162 onNoDataAvailable: function onNoDataAvailable() {
163 this.state = this.NO_DATA_AVAILABLE;
164 this.promise.resolve(this);
165 return this.promise.promise;
166 },
168 /**
169 * Data submission has completed successfully.
170 *
171 * In case of upload, this means the upload completed successfully. In case
172 * of deletion, the data was deleted successfully.
173 *
174 * @param date
175 * (Date) When data submission occurred.
176 */
177 onSubmissionSuccess: function onSubmissionSuccess(date) {
178 this.state = this.SUBMISSION_SUCCESS;
179 this.submissionDate = date;
180 this.promise.resolve(this);
181 return this.promise.promise;
182 },
184 /**
185 * There was a recoverable failure when submitting data.
186 *
187 * Perhaps the server was down. Perhaps the network wasn't available. The
188 * policy may request submission again after a short delay.
189 *
190 * @param reason
191 * (string) Why the failure occurred. For logging purposes only.
192 */
193 onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) {
194 this.state = this.SUBMISSION_FAILURE_SOFT;
195 this.reason = reason;
196 this.promise.resolve(this);
197 return this.promise.promise;
198 },
200 /**
201 * There was an unrecoverable failure when submitting data.
202 *
203 * Perhaps the client is misconfigured. Perhaps the server rejected the data.
204 * Attempts at performing submission again will yield the same result. So,
205 * the policy should not try again (until the next day).
206 *
207 * @param reason
208 * (string) Why the failure occurred. For logging purposes only.
209 */
210 onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) {
211 this.state = this.SUBMISSION_FAILURE_HARD;
212 this.reason = reason;
213 this.promise.resolve(this);
214 return this.promise.promise;
215 },
217 /**
218 * The request was aborted because an upload was already in progress.
219 */
220 onUploadInProgress: function (reason=null) {
221 this.state = this.UPLOAD_IN_PROGRESS;
222 this.reason = reason;
223 this.promise.resolve(this);
224 return this.promise.promise;
225 },
226 });
228 /**
229 * Manages scheduling of Firefox Health Report data submission.
230 *
231 * The rules of data submission are as follows:
232 *
233 * 1. Do not submit data more than once every 24 hours.
234 * 2. Try to submit as close to 24 hours apart as possible.
235 * 3. Do not submit too soon after application startup so as to not negatively
236 * impact performance at startup.
237 * 4. Before first ever data submission, the user should be notified about
238 * data collection practices.
239 * 5. User should have opportunity to react to this notification before
240 * data submission.
241 * 6. Display of notification without any explicit user action constitutes
242 * implicit consent after a certain duration of time.
243 * 7. If data submission fails, try at most 2 additional times before giving
244 * up on that day's submission.
245 *
246 * The listener passed into the instance must have the following properties
247 * (which are callbacks that will be invoked at certain key events):
248 *
249 * * onRequestDataUpload(request) - Called when the policy is requesting
250 * data to be submitted. The function is passed a `DataSubmissionRequest`.
251 * The listener should call one of the special resolving functions on that
252 * instance (see the documentation for that type).
253 *
254 * * onRequestRemoteDelete(request) - Called when the policy is requesting
255 * deletion of remotely stored data. The function is passed a
256 * `DataSubmissionRequest`. The listener should call one of the special
257 * resolving functions on that instance (just like `onRequestDataUpload`).
258 *
259 * * onNotifyDataPolicy(request) - Called when the policy is requesting the
260 * user to be notified that data submission will occur. The function
261 * receives a `NotifyPolicyRequest` instance. The callee should call one or
262 * more of the functions on that instance when specific events occur. See
263 * the documentation for that type for more.
264 *
265 * Note that the notification method is abstracted. Different applications
266 * can have different mechanisms by which they notify the user of data
267 * submission practices.
268 *
269 * @param policyPrefs
270 * (Preferences) Handle on preferences branch on which state will be
271 * queried and stored.
272 * @param healthReportPrefs
273 * (Preferences) Handle on preferences branch holding Health Report state.
274 * @param listener
275 * (object) Object with callbacks that will be invoked at certain key
276 * events.
277 */
278 this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) {
279 this._log = Log.repository.getLogger("Services.DataReporting.Policy");
280 this._log.level = Log.Level["Debug"];
282 for (let handler of this.REQUIRED_LISTENERS) {
283 if (!listener[handler]) {
284 throw new Error("Passed listener does not contain required handler: " +
285 handler);
286 }
287 }
289 this._prefs = prefs;
290 this._healthReportPrefs = healthReportPrefs;
291 this._listener = listener;
293 // If the policy version has changed, reset all preferences, so that
294 // the notification reappears.
295 let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion");
296 if (typeof(acceptedVersion) == "number" &&
297 acceptedVersion < this.minimumPolicyVersion) {
298 this._log.info("policy version has changed - resetting all prefs");
299 // We don't want to delay the notification in this case.
300 let firstRunToRestore = this.firstRunDate;
301 this._prefs.resetBranch();
302 this.firstRunDate = firstRunToRestore.getTime() ?
303 firstRunToRestore : this.now();
304 } else if (!this.firstRunDate.getTime()) {
305 // If we've never run before, record the current time.
306 this.firstRunDate = this.now();
307 }
309 // Install an observer so that we can act on changes from external
310 // code (such as Android UI).
311 // Use a function because this is the only place where the Preferences
312 // abstraction is way less usable than nsIPrefBranch.
313 //
314 // Hang on to the observer here so that tests can reach it.
315 this.uploadEnabledObserver = function onUploadEnabledChanged() {
316 if (this.pendingDeleteRemoteData || this.healthReportUploadEnabled) {
317 // Nothing to do: either we're already deleting because the caller
318 // came through the front door (rHRUE), or they set the flag to true.
319 return;
320 }
321 this._log.info("uploadEnabled pref changed. Scheduling deletion.");
322 this.deleteRemoteData();
323 }.bind(this);
325 healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
327 // Ensure we are scheduled to submit.
328 if (!this.nextDataSubmissionDate.getTime()) {
329 this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
330 }
332 // Date at which we performed user notification of acceptance.
333 // This is an instance variable because implicit acceptance should only
334 // carry forward through a single application instance.
335 this._dataSubmissionPolicyNotifiedDate = null;
337 // Record when we last requested for submitted data to be sent. This is
338 // to avoid having multiple outstanding requests.
339 this._inProgressSubmissionRequest = null;
340 };
342 this.DataReportingPolicy.prototype = Object.freeze({
343 /**
344 * How long after first run we should notify about data submission.
345 */
346 SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
348 /**
349 * Time that must elapse with no user action for implicit acceptance.
350 *
351 * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
352 * Privacy and/or Legal before modifying.
353 */
354 IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
356 /**
357 * How often to poll to see if we need to do something.
358 *
359 * The interval needs to be short enough such that short-lived applications
360 * have an opportunity to submit data. But, it also needs to be long enough
361 * to not negatively impact performance.
362 *
363 * The random bit is to ensure that other systems scheduling around the same
364 * interval don't all get scheduled together.
365 */
366 POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()),
368 /**
369 * How long individual data submission requests live before expiring.
370 *
371 * Data submission requests have this long to complete before we give up on
372 * them and try again.
373 *
374 * We want this to be short enough that we retry frequently enough but long
375 * enough to give slow networks and systems time to handle it.
376 */
377 SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000,
379 /**
380 * Our backoff schedule in case of submission failure.
381 *
382 * This dictates both the number of times we retry a daily submission and
383 * when to retry after each failure.
384 *
385 * Each element represents how long to wait after each recoverable failure.
386 * After the first failure, we wait the time in element 0 before trying
387 * again. After the second failure, we wait the time in element 1. Once
388 * we run out of values in this array, we give up on that day's submission
389 * and schedule for a day out.
390 */
391 FAILURE_BACKOFF_INTERVALS: [
392 15 * 60 * 1000,
393 60 * 60 * 1000,
394 ],
396 /**
397 * State of user notification of data submission.
398 */
399 STATE_NOTIFY_UNNOTIFIED: "not-notified",
400 STATE_NOTIFY_WAIT: "waiting",
401 STATE_NOTIFY_COMPLETE: "ok",
403 REQUIRED_LISTENERS: [
404 "onRequestDataUpload",
405 "onRequestRemoteDelete",
406 "onNotifyDataPolicy",
407 ],
409 /**
410 * The first time the health report policy came into existence.
411 *
412 * This is used for scheduling of the initial submission.
413 */
414 get firstRunDate() {
415 return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log,
416 OLDEST_ALLOWED_YEAR);
417 },
419 set firstRunDate(value) {
420 this._log.debug("Setting first-run date: " + value);
421 CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
422 OLDEST_ALLOWED_YEAR);
423 },
425 /**
426 * Short circuit policy checking and always assume acceptance.
427 *
428 * This shuld never be set by the user. Instead, it is a per-application or
429 * per-deployment default pref.
430 */
431 get dataSubmissionPolicyBypassAcceptance() {
432 return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
433 },
435 /**
436 * When the user was notified that data submission could occur.
437 *
438 * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
439 * is what's used internally.
440 */
441 get dataSubmissionPolicyNotifiedDate() {
442 return CommonUtils.getDatePref(this._prefs,
443 "dataSubmissionPolicyNotifiedTime", 0,
444 this._log, OLDEST_ALLOWED_YEAR);
445 },
447 set dataSubmissionPolicyNotifiedDate(value) {
448 this._log.debug("Setting user notified date: " + value);
449 CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
450 value, OLDEST_ALLOWED_YEAR);
451 },
453 /**
454 * When the user accepted or rejected the data submission policy.
455 *
456 * If there was implicit acceptance, this will be set to the time of that.
457 */
458 get dataSubmissionPolicyResponseDate() {
459 return CommonUtils.getDatePref(this._prefs,
460 "dataSubmissionPolicyResponseTime",
461 0, this._log, OLDEST_ALLOWED_YEAR);
462 },
464 set dataSubmissionPolicyResponseDate(value) {
465 this._log.debug("Setting user notified reaction date: " + value);
466 CommonUtils.setDatePref(this._prefs,
467 "dataSubmissionPolicyResponseTime",
468 value, OLDEST_ALLOWED_YEAR);
469 },
471 /**
472 * Records the result of user notification of data submission policy.
473 *
474 * This is used for logging and diagnostics purposes. It can answer the
475 * question "how was data submission agreed to on this profile?"
476 *
477 * Not all values are defined by this type and can come from other systems.
478 *
479 * The value must be a string and should be something machine readable. e.g.
480 * "accept-user-clicked-ok-button-in-info-bar"
481 */
482 get dataSubmissionPolicyResponseType() {
483 return this._prefs.get("dataSubmissionPolicyResponseType",
484 "none-recorded");
485 },
487 set dataSubmissionPolicyResponseType(value) {
488 if (typeof(value) != "string") {
489 throw new Error("Value must be a string. Got " + typeof(value));
490 }
492 this._prefs.set("dataSubmissionPolicyResponseType", value);
493 },
495 /**
496 * Whether submission of data is allowed.
497 *
498 * This is the master switch for remote server communication. If it is
499 * false, we never request upload or deletion.
500 */
501 get dataSubmissionEnabled() {
502 // Default is true because we are opt-out.
503 return this._prefs.get("dataSubmissionEnabled", true);
504 },
506 set dataSubmissionEnabled(value) {
507 this._prefs.set("dataSubmissionEnabled", !!value);
508 },
510 /**
511 * The minimum policy version which for dataSubmissionPolicyAccepted to
512 * to be valid.
513 */
514 get minimumPolicyVersion() {
515 // First check if the current channel has an ove
516 let channel = UpdateChannel.get(false);
517 let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel);
518 return channelPref !== undefined ?
519 channelPref : this._prefs.get("minimumPolicyVersion", 1);
520 },
522 /**
523 * Whether the user has accepted that data submission can occur.
524 *
525 * This overrides dataSubmissionEnabled.
526 */
527 get dataSubmissionPolicyAccepted() {
528 // Be conservative and default to false.
529 return this._prefs.get("dataSubmissionPolicyAccepted", false);
530 },
532 set dataSubmissionPolicyAccepted(value) {
533 this._prefs.set("dataSubmissionPolicyAccepted", !!value);
534 if (!!value) {
535 let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
536 this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
537 } else {
538 this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
539 }
540 },
542 /**
543 * The state of user notification of the data policy.
544 *
545 * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
546 * submission can occur.
547 *
548 * @return DataReportingPolicy.STATE_NOTIFY_* constant.
549 */
550 get notifyState() {
551 if (this.dataSubmissionPolicyResponseDate.getTime()) {
552 return this.STATE_NOTIFY_COMPLETE;
553 }
555 // We get the local state - not the state from prefs - because we don't want
556 // a value from a previous application run to interfere. This prevents
557 // a scenario where notification occurs just before application shutdown and
558 // notification is displayed for shorter than the policy requires.
559 if (!this._dataSubmissionPolicyNotifiedDate) {
560 return this.STATE_NOTIFY_UNNOTIFIED;
561 }
563 return this.STATE_NOTIFY_WAIT;
564 },
566 /**
567 * When this policy last requested data submission.
568 *
569 * This is used mainly for forensics purposes and should have no bearing
570 * on scheduling or run-time behavior.
571 */
572 get lastDataSubmissionRequestedDate() {
573 return CommonUtils.getDatePref(this._healthReportPrefs,
574 "lastDataSubmissionRequestedTime", 0,
575 this._log, OLDEST_ALLOWED_YEAR);
576 },
578 set lastDataSubmissionRequestedDate(value) {
579 CommonUtils.setDatePref(this._healthReportPrefs,
580 "lastDataSubmissionRequestedTime",
581 value, OLDEST_ALLOWED_YEAR);
582 },
584 /**
585 * When the last data submission actually occurred.
586 *
587 * This is used mainly for forensics purposes and should have no bearing on
588 * actual scheduling.
589 */
590 get lastDataSubmissionSuccessfulDate() {
591 return CommonUtils.getDatePref(this._healthReportPrefs,
592 "lastDataSubmissionSuccessfulTime", 0,
593 this._log, OLDEST_ALLOWED_YEAR);
594 },
596 set lastDataSubmissionSuccessfulDate(value) {
597 CommonUtils.setDatePref(this._healthReportPrefs,
598 "lastDataSubmissionSuccessfulTime",
599 value, OLDEST_ALLOWED_YEAR);
600 },
602 /**
603 * When we last encountered a submission failure.
604 *
605 * This is used for forensics purposes and should have no bearing on
606 * scheduling.
607 */
608 get lastDataSubmissionFailureDate() {
609 return CommonUtils.getDatePref(this._healthReportPrefs,
610 "lastDataSubmissionFailureTime",
611 0, this._log, OLDEST_ALLOWED_YEAR);
612 },
614 set lastDataSubmissionFailureDate(value) {
615 CommonUtils.setDatePref(this._healthReportPrefs,
616 "lastDataSubmissionFailureTime",
617 value, OLDEST_ALLOWED_YEAR);
618 },
620 /**
621 * When the next data submission is scheduled to occur.
622 *
623 * This is maintained internally by this type. External users should not
624 * mutate this value.
625 */
626 get nextDataSubmissionDate() {
627 return CommonUtils.getDatePref(this._healthReportPrefs,
628 "nextDataSubmissionTime", 0,
629 this._log, OLDEST_ALLOWED_YEAR);
630 },
632 set nextDataSubmissionDate(value) {
633 CommonUtils.setDatePref(this._healthReportPrefs,
634 "nextDataSubmissionTime", value,
635 OLDEST_ALLOWED_YEAR);
636 },
638 /**
639 * The number of submission failures for this day's upload.
640 *
641 * This is used to drive backoff and scheduling.
642 */
643 get currentDaySubmissionFailureCount() {
644 let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0);
646 if (!Number.isInteger(v)) {
647 v = 0;
648 }
650 return v;
651 },
653 set currentDaySubmissionFailureCount(value) {
654 if (!Number.isInteger(value)) {
655 throw new Error("Value must be integer: " + value);
656 }
658 this._healthReportPrefs.set("currentDaySubmissionFailureCount", value);
659 },
661 /**
662 * Whether a request to delete remote data is awaiting completion.
663 *
664 * If this is true, the policy will request that remote data be deleted.
665 * Furthermore, no new data will be uploaded (if it's even allowed) until
666 * the remote deletion is fulfilled.
667 */
668 get pendingDeleteRemoteData() {
669 return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false);
670 },
672 set pendingDeleteRemoteData(value) {
673 this._healthReportPrefs.set("pendingDeleteRemoteData", !!value);
674 },
676 /**
677 * Whether upload of Firefox Health Report data is enabled.
678 */
679 get healthReportUploadEnabled() {
680 return !!this._healthReportPrefs.get("uploadEnabled", true);
681 },
683 // External callers should update this via `recordHealthReportUploadEnabled`
684 // to ensure appropriate side-effects are performed.
685 set healthReportUploadEnabled(value) {
686 this._healthReportPrefs.set("uploadEnabled", !!value);
687 },
689 /**
690 * Whether the FHR upload enabled setting is locked and can't be changed.
691 */
692 get healthReportUploadLocked() {
693 return this._healthReportPrefs.locked("uploadEnabled");
694 },
696 /**
697 * Record user acceptance of data submission policy.
698 *
699 * Data submission will not be allowed to occur until this is called.
700 *
701 * This is typically called through the `onUserAccept` property attached to
702 * the promise passed to `onUserNotify` in the policy listener. But, it can
703 * be called through other interfaces at any time and the call will have
704 * an impact on future data submissions.
705 *
706 * @param reason
707 * (string) How the user accepted the data submission policy.
708 */
709 recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
710 this._log.info("User accepted data submission policy: " + reason);
711 this.dataSubmissionPolicyResponseDate = this.now();
712 this.dataSubmissionPolicyResponseType = "accepted-" + reason;
713 this.dataSubmissionPolicyAccepted = true;
714 },
716 /**
717 * Record user rejection of submission policy.
718 *
719 * Data submission will not be allowed to occur if this is called.
720 *
721 * This is typically called through the `onUserReject` property attached to
722 * the promise passed to `onUserNotify` in the policy listener. But, it can
723 * be called through other interfaces at any time and the call will have an
724 * impact on future data submissions.
725 */
726 recordUserRejection: function recordUserRejection(reason="no-reason") {
727 this._log.info("User rejected data submission policy: " + reason);
728 this.dataSubmissionPolicyResponseDate = this.now();
729 this.dataSubmissionPolicyResponseType = "rejected-" + reason;
730 this.dataSubmissionPolicyAccepted = false;
731 },
733 /**
734 * Record the user's intent for whether FHR should upload data.
735 *
736 * This is the preferred way for XUL applications to record a user's
737 * preference on whether Firefox Health Report should upload data to
738 * a server.
739 *
740 * If upload is disabled through this API, a request for remote data
741 * deletion is initiated automatically.
742 *
743 * If upload is being disabled and this operation is scheduled to
744 * occur immediately, a promise will be returned. This promise will be
745 * fulfilled when the deletion attempt finishes. If upload is being
746 * disabled and a promise is not returned, callers must poll
747 * `haveRemoteData` on the HealthReporter instance to see if remote
748 * data has been deleted.
749 *
750 * @param flag
751 * (bool) Whether data submission is enabled or disabled.
752 * @param reason
753 * (string) Why this value is being adjusted. For logging
754 * purposes only.
755 */
756 recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
757 let result = null;
758 if (!flag) {
759 result = this.deleteRemoteData(reason);
760 }
762 this.healthReportUploadEnabled = flag;
763 return result;
764 },
766 /**
767 * Request that remote data be deleted.
768 *
769 * This will record an intent that previously uploaded data is to be deleted.
770 * The policy will eventually issue a request to the listener for data
771 * deletion. It will keep asking for deletion until the listener acknowledges
772 * that data has been deleted.
773 */
774 deleteRemoteData: function deleteRemoteData(reason="no-reason") {
775 this._log.info("Remote data deletion requested: " + reason);
777 this.pendingDeleteRemoteData = true;
779 // We want delete deletion to occur as soon as possible. Move up any
780 // pending scheduled data submission and try to trigger.
781 this.nextDataSubmissionDate = this.now();
782 return this.checkStateAndTrigger();
783 },
785 /**
786 * Start background polling for activity.
787 *
788 * This will set up a recurring timer that will periodically check if
789 * activity is warranted.
790 *
791 * You typically call this function for each constructed instance.
792 */
793 startPolling: function startPolling() {
794 this.stopPolling();
796 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
797 this._timer.initWithCallback({
798 notify: function notify() {
799 this.checkStateAndTrigger();
800 }.bind(this)
801 }, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK);
802 },
804 /**
805 * Stop background polling for activity.
806 *
807 * This should be called when the instance is no longer needed.
808 */
809 stopPolling: function stopPolling() {
810 if (this._timer) {
811 this._timer.cancel();
812 this._timer = null;
813 }
814 },
816 /**
817 * Abstraction for obtaining current time.
818 *
819 * The purpose of this is to facilitate testing. Testing code can monkeypatch
820 * this on instances instead of modifying the singleton Date object.
821 */
822 now: function now() {
823 return new Date();
824 },
826 /**
827 * Check state and trigger actions, if necessary.
828 *
829 * This is what enforces the submission and notification policy detailed
830 * above. You can think of this as the driver for health report data
831 * submission.
832 *
833 * Typically this function is called automatically by the background polling.
834 * But, it can safely be called manually as needed.
835 */
836 checkStateAndTrigger: function checkStateAndTrigger() {
837 // If the master data submission kill switch is toggled, we have nothing
838 // to do. We don't notify about data policies because this would have
839 // no effect.
840 if (!this.dataSubmissionEnabled) {
841 this._log.debug("Data submission is disabled. Doing nothing.");
842 return;
843 }
845 let now = this.now();
846 let nowT = now.getTime();
847 let nextSubmissionDate = this.nextDataSubmissionDate;
849 // If the system clock were ever set to a time in the distant future,
850 // it's possible our next schedule date is far out as well. We know
851 // we shouldn't schedule for more than a day out, so we reset the next
852 // scheduled date appropriately. 3 days was chosen arbitrarily.
853 if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) {
854 this._log.warn("Next data submission time is far away. Was the system " +
855 "clock recently readjusted? " + nextSubmissionDate);
857 // It shouldn't really matter what we set this to. 1 day in the future
858 // should be pretty safe.
859 this._moveScheduleForward24h();
861 // Fall through since we may have other actions.
862 }
864 // Tend to any in progress work.
865 if (this._processInProgressSubmission()) {
866 return;
867 }
869 // Requests to delete remote data take priority above everything else.
870 if (this.pendingDeleteRemoteData) {
871 if (nowT < nextSubmissionDate.getTime()) {
872 this._log.debug("Deletion request is scheduled for the future: " +
873 nextSubmissionDate);
874 return;
875 }
877 return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
878 }
880 if (!this.healthReportUploadEnabled) {
881 this._log.debug("Data upload is disabled. Doing nothing.");
882 return;
883 }
885 // If the user hasn't responded to the data policy, don't do anything.
886 if (!this.ensureNotifyResponse(now)) {
887 return;
888 }
890 // User has opted out of data submission.
891 if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
892 this._log.debug("Data submission has been disabled per user request.");
893 return;
894 }
896 // User has responded to data policy and data submission is enabled. Now
897 // comes the scheduling part.
899 if (nowT < nextSubmissionDate.getTime()) {
900 this._log.debug("Next data submission is scheduled in the future: " +
901 nextSubmissionDate);
902 return;
903 }
905 return this._dispatchSubmissionRequest("onRequestDataUpload", false);
906 },
908 /**
909 * Ensure user has responded to data submission policy.
910 *
911 * This must be called before data submission. If the policy has not been
912 * responded to, data submission must not occur.
913 *
914 * @return bool Whether user has responded to data policy.
915 */
916 ensureNotifyResponse: function ensureNotifyResponse(now) {
917 if (this.dataSubmissionPolicyBypassAcceptance) {
918 return true;
919 }
921 let notifyState = this.notifyState;
923 if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
924 let notifyAt = new Date(this.firstRunDate.getTime() +
925 this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
927 if (now.getTime() < notifyAt.getTime()) {
928 this._log.debug("Don't have to notify about data submission yet.");
929 return false;
930 }
932 let onComplete = function onComplete() {
933 this._log.info("Data submission notification presented.");
934 let now = this.now();
936 this._dataSubmissionPolicyNotifiedDate = now;
937 this.dataSubmissionPolicyNotifiedDate = now;
938 }.bind(this);
940 let deferred = Promise.defer();
942 deferred.promise.then(onComplete, (error) => {
943 this._log.warn("Data policy notification presentation failed: " +
944 CommonUtils.exceptionStr(error));
945 });
947 this._log.info("Requesting display of data policy.");
948 let request = new NotifyPolicyRequest(this, deferred);
950 try {
951 this._listener.onNotifyDataPolicy(request);
952 } catch (ex) {
953 this._log.warn("Exception when calling onNotifyDataPolicy: " +
954 CommonUtils.exceptionStr(ex));
955 }
956 return false;
957 }
959 // We're waiting for user action or implicit acceptance after display.
960 if (notifyState == this.STATE_NOTIFY_WAIT) {
961 // Check for implicit acceptance.
962 let implicitAcceptance =
963 this._dataSubmissionPolicyNotifiedDate.getTime() +
964 this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
966 this._log.debug("Now: " + now.getTime());
967 this._log.debug("Will accept: " + implicitAcceptance);
968 if (now.getTime() < implicitAcceptance) {
969 this._log.debug("Still waiting for reaction or implicit acceptance. " +
970 "Now: " + now.getTime() + " < " +
971 "Accept: " + implicitAcceptance);
972 return false;
973 }
975 this.recordUserAcceptance("implicit-time-elapsed");
976 return true;
977 }
979 // If this happens, we have a coding error in this file.
980 if (notifyState != this.STATE_NOTIFY_COMPLETE) {
981 throw new Error("Unknown notification state: " + notifyState);
982 }
984 return true;
985 },
987 _processInProgressSubmission: function _processInProgressSubmission() {
988 if (!this._inProgressSubmissionRequest) {
989 return false;
990 }
992 let now = this.now().getTime();
993 if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) {
994 this._log.info("Waiting on in-progress submission request to finish.");
995 return true;
996 }
998 this._log.warn("Old submission request has expired from no activity.");
999 this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired."));
1000 this._inProgressSubmissionRequest = null;
1001 this._handleSubmissionFailure();
1003 return false;
1004 },
1006 _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) {
1007 let now = this.now();
1009 // We're past our scheduled next data submission date, so let's do it!
1010 this.lastDataSubmissionRequestedDate = now;
1011 let deferred = Promise.defer();
1012 let requestExpiresDate =
1013 this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC);
1014 this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred,
1015 requestExpiresDate,
1016 isDelete);
1018 let onSuccess = function onSuccess(result) {
1019 this._inProgressSubmissionRequest = null;
1020 this._handleSubmissionResult(result);
1021 }.bind(this);
1023 let onError = function onError(error) {
1024 this._log.error("Error when handling data submission result: " +
1025 CommonUtils.exceptionStr(error));
1026 this._inProgressSubmissionRequest = null;
1027 this._handleSubmissionFailure();
1028 }.bind(this);
1030 let chained = deferred.promise.then(onSuccess, onError);
1032 this._log.info("Requesting data submission. Will expire at " +
1033 requestExpiresDate);
1034 try {
1035 this._listener[handler](this._inProgressSubmissionRequest);
1036 } catch (ex) {
1037 this._log.warn("Exception when calling " + handler + ": " +
1038 CommonUtils.exceptionStr(ex));
1039 this._inProgressSubmissionRequest = null;
1040 this._handleSubmissionFailure();
1041 return;
1042 }
1044 return chained;
1045 },
1047 _handleSubmissionResult: function _handleSubmissionResult(request) {
1048 let state = request.state;
1049 let reason = request.reason || "no reason";
1050 this._log.info("Got submission request result: " + state);
1052 if (state == request.SUBMISSION_SUCCESS) {
1053 if (request.isDelete) {
1054 this.pendingDeleteRemoteData = false;
1055 this._log.info("Successful data delete reported.");
1056 } else {
1057 this._log.info("Successful data upload reported.");
1058 }
1060 this.lastDataSubmissionSuccessfulDate = request.submissionDate;
1062 let nextSubmissionDate =
1063 new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY);
1065 // Schedule pending deletes immediately. This has potential to overload
1066 // the server. However, the frequency of delete requests across all
1067 // clients should be low, so this shouldn't pose a problem.
1068 if (this.pendingDeleteRemoteData) {
1069 nextSubmissionDate = this.now();
1070 }
1072 this.nextDataSubmissionDate = nextSubmissionDate;
1073 this.currentDaySubmissionFailureCount = 0;
1074 return;
1075 }
1077 if (state == request.NO_DATA_AVAILABLE) {
1078 if (request.isDelete) {
1079 this._log.info("Remote data delete requested but no remote data was stored.");
1080 this.pendingDeleteRemoteData = false;
1081 return;
1082 }
1084 this._log.info("No data was available to submit. May try later.");
1085 this._handleSubmissionFailure();
1086 return;
1087 }
1089 // We don't special case request.isDelete for these failures because it
1090 // likely means there was a server error.
1092 if (state == request.SUBMISSION_FAILURE_SOFT) {
1093 this._log.warn("Soft error submitting data: " + reason);
1094 this.lastDataSubmissionFailureDate = this.now();
1095 this._handleSubmissionFailure();
1096 return;
1097 }
1099 if (state == request.SUBMISSION_FAILURE_HARD) {
1100 this._log.warn("Hard error submitting data: " + reason);
1101 this.lastDataSubmissionFailureDate = this.now();
1102 this._moveScheduleForward24h();
1103 return;
1104 }
1106 throw new Error("Unknown state on DataSubmissionRequest: " + request.state);
1107 },
1109 _handleSubmissionFailure: function _handleSubmissionFailure() {
1110 if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) {
1111 this._log.warn("Reached the limit of daily submission attempts. " +
1112 "Rescheduling for tomorrow.");
1113 this._moveScheduleForward24h();
1114 return false;
1115 }
1117 let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount];
1118 this.nextDataSubmissionDate = this._futureDate(offset);
1119 this.currentDaySubmissionFailureCount++;
1120 return true;
1121 },
1123 _moveScheduleForward24h: function _moveScheduleForward24h() {
1124 let d = this._futureDate(MILLISECONDS_PER_DAY);
1125 this._log.info("Setting next scheduled data submission for " + d);
1127 this.nextDataSubmissionDate = d;
1128 this.currentDaySubmissionFailureCount = 0;
1129 },
1131 _futureDate: function _futureDate(offset) {
1132 return new Date(this.now().getTime() + offset);
1133 },
1134 });