services/datareporting/policy.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
     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;
  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.");
  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();
  1072       this.nextDataSubmissionDate = nextSubmissionDate;
  1073       this.currentDaySubmissionFailureCount = 0;
  1074       return;
  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;
  1084       this._log.info("No data was available to submit. May try later.");
  1085       this._handleSubmissionFailure();
  1086       return;
  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;
  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;
  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;
  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 });

mercurial