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.

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

mercurial