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