1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/datareporting/policy.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1135 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * This file is in transition. It was originally conceived to fulfill the 1.10 + * needs of only Firefox Health Report. It is slowly being morphed into 1.11 + * fulfilling the needs of all data reporting facilities in Gecko applications. 1.12 + * As a result, some things feel a bit weird. 1.13 + * 1.14 + * DataReportingPolicy is both a driver for data reporting notification 1.15 + * (a true policy) and the driver for FHR data submission. The latter should 1.16 + * eventually be split into its own type and module. 1.17 + */ 1.18 + 1.19 +"use strict"; 1.20 + 1.21 +#ifndef MERGED_COMPARTMENT 1.22 + 1.23 +this.EXPORTED_SYMBOLS = [ 1.24 + "DataSubmissionRequest", // For test use only. 1.25 + "DataReportingPolicy", 1.26 +]; 1.27 + 1.28 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.29 + 1.30 +#endif 1.31 + 1.32 +Cu.import("resource://gre/modules/Services.jsm"); 1.33 +Cu.import("resource://gre/modules/Promise.jsm"); 1.34 +Cu.import("resource://gre/modules/Log.jsm"); 1.35 +Cu.import("resource://services-common/utils.js"); 1.36 +Cu.import("resource://gre/modules/UpdateChannel.jsm"); 1.37 + 1.38 +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; 1.39 + 1.40 +// Used as a sanity lower bound for dates stored in prefs. This module was 1.41 +// implemented in 2012, so any earlier dates indicate an incorrect clock. 1.42 +const OLDEST_ALLOWED_YEAR = 2012; 1.43 + 1.44 +/** 1.45 + * Represents a request to display data policy. 1.46 + * 1.47 + * Instances of this are created when the policy is requesting the user's 1.48 + * approval to agree to the data submission policy. 1.49 + * 1.50 + * Receivers of these instances are expected to call one or more of the on* 1.51 + * functions when events occur. 1.52 + * 1.53 + * When one of these requests is received, the first thing a callee should do 1.54 + * is present notification to the user of the data policy. When the notice 1.55 + * is displayed to the user, the callee should call `onUserNotifyComplete`. 1.56 + * This begins a countdown timer that upon completion will signal implicit 1.57 + * acceptance of the policy. If for whatever reason the callee could not 1.58 + * display a notice, it should call `onUserNotifyFailed`. 1.59 + * 1.60 + * Once the user is notified of the policy, the callee has the option of 1.61 + * signaling explicit user acceptance or rejection of the policy. They do this 1.62 + * by calling `onUserAccept` or `onUserReject`, respectively. These functions 1.63 + * are essentially proxies to 1.64 + * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}. 1.65 + * 1.66 + * If the user never explicitly accepts or rejects the policy, it will be 1.67 + * implicitly accepted after a specified duration of time. The notice is 1.68 + * expected to remain displayed even after implicit acceptance (in case the 1.69 + * user is away from the device). So, no event signaling implicit acceptance 1.70 + * is exposed. 1.71 + * 1.72 + * Receivers of instances of this type should treat it as a black box with 1.73 + * the exception of the on* functions. 1.74 + * 1.75 + * @param policy 1.76 + * (DataReportingPolicy) The policy instance this request came from. 1.77 + * @param deferred 1.78 + * (deferred) The promise that will be fulfilled when display occurs. 1.79 + */ 1.80 +function NotifyPolicyRequest(policy, deferred) { 1.81 + this.policy = policy; 1.82 + this.deferred = deferred; 1.83 +} 1.84 +NotifyPolicyRequest.prototype = { 1.85 + /** 1.86 + * Called when the user is notified of the policy. 1.87 + * 1.88 + * This starts a countdown timer that will eventually signify implicit 1.89 + * acceptance of the data policy. 1.90 + */ 1.91 + onUserNotifyComplete: function onUserNotified() { 1.92 + this.deferred.resolve(); 1.93 + return this.deferred.promise; 1.94 + }, 1.95 + 1.96 + /** 1.97 + * Called when there was an error notifying the user about the policy. 1.98 + * 1.99 + * @param error 1.100 + * (Error) Explains what went wrong. 1.101 + */ 1.102 + onUserNotifyFailed: function onUserNotifyFailed(error) { 1.103 + this.deferred.reject(error); 1.104 + }, 1.105 + 1.106 + /** 1.107 + * Called when the user agreed to the data policy. 1.108 + * 1.109 + * @param reason 1.110 + * (string) How the user agreed to the policy. 1.111 + */ 1.112 + onUserAccept: function onUserAccept(reason) { 1.113 + this.policy.recordUserAcceptance(reason); 1.114 + }, 1.115 + 1.116 + /** 1.117 + * Called when the user rejected the data policy. 1.118 + * 1.119 + * @param reason 1.120 + * (string) How the user rejected the policy. 1.121 + */ 1.122 + onUserReject: function onUserReject(reason) { 1.123 + this.policy.recordUserRejection(reason); 1.124 + }, 1.125 +}; 1.126 + 1.127 +Object.freeze(NotifyPolicyRequest.prototype); 1.128 + 1.129 +/** 1.130 + * Represents a request to submit data. 1.131 + * 1.132 + * Instances of this are created when the policy requests data upload or 1.133 + * deletion. 1.134 + * 1.135 + * Receivers are expected to call one of the provided on* functions to signal 1.136 + * completion of the request. 1.137 + * 1.138 + * Instances of this type should not be instantiated outside of this file. 1.139 + * Receivers of instances of this type should not attempt to do anything with 1.140 + * the instance except call one of the on* methods. 1.141 + */ 1.142 +this.DataSubmissionRequest = function (promise, expiresDate, isDelete) { 1.143 + this.promise = promise; 1.144 + this.expiresDate = expiresDate; 1.145 + this.isDelete = isDelete; 1.146 + 1.147 + this.state = null; 1.148 + this.reason = null; 1.149 +} 1.150 + 1.151 +this.DataSubmissionRequest.prototype = Object.freeze({ 1.152 + NO_DATA_AVAILABLE: "no-data-available", 1.153 + SUBMISSION_SUCCESS: "success", 1.154 + SUBMISSION_FAILURE_SOFT: "failure-soft", 1.155 + SUBMISSION_FAILURE_HARD: "failure-hard", 1.156 + UPLOAD_IN_PROGRESS: "upload-in-progress", 1.157 + 1.158 + /** 1.159 + * No submission was attempted because no data was available. 1.160 + * 1.161 + * In the case of upload, this means there is no data to upload (perhaps 1.162 + * it isn't available yet). In case of remote deletion, it means that there 1.163 + * is no remote data to delete. 1.164 + */ 1.165 + onNoDataAvailable: function onNoDataAvailable() { 1.166 + this.state = this.NO_DATA_AVAILABLE; 1.167 + this.promise.resolve(this); 1.168 + return this.promise.promise; 1.169 + }, 1.170 + 1.171 + /** 1.172 + * Data submission has completed successfully. 1.173 + * 1.174 + * In case of upload, this means the upload completed successfully. In case 1.175 + * of deletion, the data was deleted successfully. 1.176 + * 1.177 + * @param date 1.178 + * (Date) When data submission occurred. 1.179 + */ 1.180 + onSubmissionSuccess: function onSubmissionSuccess(date) { 1.181 + this.state = this.SUBMISSION_SUCCESS; 1.182 + this.submissionDate = date; 1.183 + this.promise.resolve(this); 1.184 + return this.promise.promise; 1.185 + }, 1.186 + 1.187 + /** 1.188 + * There was a recoverable failure when submitting data. 1.189 + * 1.190 + * Perhaps the server was down. Perhaps the network wasn't available. The 1.191 + * policy may request submission again after a short delay. 1.192 + * 1.193 + * @param reason 1.194 + * (string) Why the failure occurred. For logging purposes only. 1.195 + */ 1.196 + onSubmissionFailureSoft: function onSubmissionFailureSoft(reason=null) { 1.197 + this.state = this.SUBMISSION_FAILURE_SOFT; 1.198 + this.reason = reason; 1.199 + this.promise.resolve(this); 1.200 + return this.promise.promise; 1.201 + }, 1.202 + 1.203 + /** 1.204 + * There was an unrecoverable failure when submitting data. 1.205 + * 1.206 + * Perhaps the client is misconfigured. Perhaps the server rejected the data. 1.207 + * Attempts at performing submission again will yield the same result. So, 1.208 + * the policy should not try again (until the next day). 1.209 + * 1.210 + * @param reason 1.211 + * (string) Why the failure occurred. For logging purposes only. 1.212 + */ 1.213 + onSubmissionFailureHard: function onSubmissionFailureHard(reason=null) { 1.214 + this.state = this.SUBMISSION_FAILURE_HARD; 1.215 + this.reason = reason; 1.216 + this.promise.resolve(this); 1.217 + return this.promise.promise; 1.218 + }, 1.219 + 1.220 + /** 1.221 + * The request was aborted because an upload was already in progress. 1.222 + */ 1.223 + onUploadInProgress: function (reason=null) { 1.224 + this.state = this.UPLOAD_IN_PROGRESS; 1.225 + this.reason = reason; 1.226 + this.promise.resolve(this); 1.227 + return this.promise.promise; 1.228 + }, 1.229 +}); 1.230 + 1.231 +/** 1.232 + * Manages scheduling of Firefox Health Report data submission. 1.233 + * 1.234 + * The rules of data submission are as follows: 1.235 + * 1.236 + * 1. Do not submit data more than once every 24 hours. 1.237 + * 2. Try to submit as close to 24 hours apart as possible. 1.238 + * 3. Do not submit too soon after application startup so as to not negatively 1.239 + * impact performance at startup. 1.240 + * 4. Before first ever data submission, the user should be notified about 1.241 + * data collection practices. 1.242 + * 5. User should have opportunity to react to this notification before 1.243 + * data submission. 1.244 + * 6. Display of notification without any explicit user action constitutes 1.245 + * implicit consent after a certain duration of time. 1.246 + * 7. If data submission fails, try at most 2 additional times before giving 1.247 + * up on that day's submission. 1.248 + * 1.249 + * The listener passed into the instance must have the following properties 1.250 + * (which are callbacks that will be invoked at certain key events): 1.251 + * 1.252 + * * onRequestDataUpload(request) - Called when the policy is requesting 1.253 + * data to be submitted. The function is passed a `DataSubmissionRequest`. 1.254 + * The listener should call one of the special resolving functions on that 1.255 + * instance (see the documentation for that type). 1.256 + * 1.257 + * * onRequestRemoteDelete(request) - Called when the policy is requesting 1.258 + * deletion of remotely stored data. The function is passed a 1.259 + * `DataSubmissionRequest`. The listener should call one of the special 1.260 + * resolving functions on that instance (just like `onRequestDataUpload`). 1.261 + * 1.262 + * * onNotifyDataPolicy(request) - Called when the policy is requesting the 1.263 + * user to be notified that data submission will occur. The function 1.264 + * receives a `NotifyPolicyRequest` instance. The callee should call one or 1.265 + * more of the functions on that instance when specific events occur. See 1.266 + * the documentation for that type for more. 1.267 + * 1.268 + * Note that the notification method is abstracted. Different applications 1.269 + * can have different mechanisms by which they notify the user of data 1.270 + * submission practices. 1.271 + * 1.272 + * @param policyPrefs 1.273 + * (Preferences) Handle on preferences branch on which state will be 1.274 + * queried and stored. 1.275 + * @param healthReportPrefs 1.276 + * (Preferences) Handle on preferences branch holding Health Report state. 1.277 + * @param listener 1.278 + * (object) Object with callbacks that will be invoked at certain key 1.279 + * events. 1.280 + */ 1.281 +this.DataReportingPolicy = function (prefs, healthReportPrefs, listener) { 1.282 + this._log = Log.repository.getLogger("Services.DataReporting.Policy"); 1.283 + this._log.level = Log.Level["Debug"]; 1.284 + 1.285 + for (let handler of this.REQUIRED_LISTENERS) { 1.286 + if (!listener[handler]) { 1.287 + throw new Error("Passed listener does not contain required handler: " + 1.288 + handler); 1.289 + } 1.290 + } 1.291 + 1.292 + this._prefs = prefs; 1.293 + this._healthReportPrefs = healthReportPrefs; 1.294 + this._listener = listener; 1.295 + 1.296 + // If the policy version has changed, reset all preferences, so that 1.297 + // the notification reappears. 1.298 + let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion"); 1.299 + if (typeof(acceptedVersion) == "number" && 1.300 + acceptedVersion < this.minimumPolicyVersion) { 1.301 + this._log.info("policy version has changed - resetting all prefs"); 1.302 + // We don't want to delay the notification in this case. 1.303 + let firstRunToRestore = this.firstRunDate; 1.304 + this._prefs.resetBranch(); 1.305 + this.firstRunDate = firstRunToRestore.getTime() ? 1.306 + firstRunToRestore : this.now(); 1.307 + } else if (!this.firstRunDate.getTime()) { 1.308 + // If we've never run before, record the current time. 1.309 + this.firstRunDate = this.now(); 1.310 + } 1.311 + 1.312 + // Install an observer so that we can act on changes from external 1.313 + // code (such as Android UI). 1.314 + // Use a function because this is the only place where the Preferences 1.315 + // abstraction is way less usable than nsIPrefBranch. 1.316 + // 1.317 + // Hang on to the observer here so that tests can reach it. 1.318 + this.uploadEnabledObserver = function onUploadEnabledChanged() { 1.319 + if (this.pendingDeleteRemoteData || this.healthReportUploadEnabled) { 1.320 + // Nothing to do: either we're already deleting because the caller 1.321 + // came through the front door (rHRUE), or they set the flag to true. 1.322 + return; 1.323 + } 1.324 + this._log.info("uploadEnabled pref changed. Scheduling deletion."); 1.325 + this.deleteRemoteData(); 1.326 + }.bind(this); 1.327 + 1.328 + healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver); 1.329 + 1.330 + // Ensure we are scheduled to submit. 1.331 + if (!this.nextDataSubmissionDate.getTime()) { 1.332 + this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY); 1.333 + } 1.334 + 1.335 + // Date at which we performed user notification of acceptance. 1.336 + // This is an instance variable because implicit acceptance should only 1.337 + // carry forward through a single application instance. 1.338 + this._dataSubmissionPolicyNotifiedDate = null; 1.339 + 1.340 + // Record when we last requested for submitted data to be sent. This is 1.341 + // to avoid having multiple outstanding requests. 1.342 + this._inProgressSubmissionRequest = null; 1.343 +}; 1.344 + 1.345 +this.DataReportingPolicy.prototype = Object.freeze({ 1.346 + /** 1.347 + * How long after first run we should notify about data submission. 1.348 + */ 1.349 + SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000, 1.350 + 1.351 + /** 1.352 + * Time that must elapse with no user action for implicit acceptance. 1.353 + * 1.354 + * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with 1.355 + * Privacy and/or Legal before modifying. 1.356 + */ 1.357 + IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000, 1.358 + 1.359 + /** 1.360 + * How often to poll to see if we need to do something. 1.361 + * 1.362 + * The interval needs to be short enough such that short-lived applications 1.363 + * have an opportunity to submit data. But, it also needs to be long enough 1.364 + * to not negatively impact performance. 1.365 + * 1.366 + * The random bit is to ensure that other systems scheduling around the same 1.367 + * interval don't all get scheduled together. 1.368 + */ 1.369 + POLL_INTERVAL_MSEC: (60 * 1000) + Math.floor(2.5 * 1000 * Math.random()), 1.370 + 1.371 + /** 1.372 + * How long individual data submission requests live before expiring. 1.373 + * 1.374 + * Data submission requests have this long to complete before we give up on 1.375 + * them and try again. 1.376 + * 1.377 + * We want this to be short enough that we retry frequently enough but long 1.378 + * enough to give slow networks and systems time to handle it. 1.379 + */ 1.380 + SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC: 10 * 60 * 1000, 1.381 + 1.382 + /** 1.383 + * Our backoff schedule in case of submission failure. 1.384 + * 1.385 + * This dictates both the number of times we retry a daily submission and 1.386 + * when to retry after each failure. 1.387 + * 1.388 + * Each element represents how long to wait after each recoverable failure. 1.389 + * After the first failure, we wait the time in element 0 before trying 1.390 + * again. After the second failure, we wait the time in element 1. Once 1.391 + * we run out of values in this array, we give up on that day's submission 1.392 + * and schedule for a day out. 1.393 + */ 1.394 + FAILURE_BACKOFF_INTERVALS: [ 1.395 + 15 * 60 * 1000, 1.396 + 60 * 60 * 1000, 1.397 + ], 1.398 + 1.399 + /** 1.400 + * State of user notification of data submission. 1.401 + */ 1.402 + STATE_NOTIFY_UNNOTIFIED: "not-notified", 1.403 + STATE_NOTIFY_WAIT: "waiting", 1.404 + STATE_NOTIFY_COMPLETE: "ok", 1.405 + 1.406 + REQUIRED_LISTENERS: [ 1.407 + "onRequestDataUpload", 1.408 + "onRequestRemoteDelete", 1.409 + "onNotifyDataPolicy", 1.410 + ], 1.411 + 1.412 + /** 1.413 + * The first time the health report policy came into existence. 1.414 + * 1.415 + * This is used for scheduling of the initial submission. 1.416 + */ 1.417 + get firstRunDate() { 1.418 + return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log, 1.419 + OLDEST_ALLOWED_YEAR); 1.420 + }, 1.421 + 1.422 + set firstRunDate(value) { 1.423 + this._log.debug("Setting first-run date: " + value); 1.424 + CommonUtils.setDatePref(this._prefs, "firstRunTime", value, 1.425 + OLDEST_ALLOWED_YEAR); 1.426 + }, 1.427 + 1.428 + /** 1.429 + * Short circuit policy checking and always assume acceptance. 1.430 + * 1.431 + * This shuld never be set by the user. Instead, it is a per-application or 1.432 + * per-deployment default pref. 1.433 + */ 1.434 + get dataSubmissionPolicyBypassAcceptance() { 1.435 + return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false); 1.436 + }, 1.437 + 1.438 + /** 1.439 + * When the user was notified that data submission could occur. 1.440 + * 1.441 + * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate 1.442 + * is what's used internally. 1.443 + */ 1.444 + get dataSubmissionPolicyNotifiedDate() { 1.445 + return CommonUtils.getDatePref(this._prefs, 1.446 + "dataSubmissionPolicyNotifiedTime", 0, 1.447 + this._log, OLDEST_ALLOWED_YEAR); 1.448 + }, 1.449 + 1.450 + set dataSubmissionPolicyNotifiedDate(value) { 1.451 + this._log.debug("Setting user notified date: " + value); 1.452 + CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime", 1.453 + value, OLDEST_ALLOWED_YEAR); 1.454 + }, 1.455 + 1.456 + /** 1.457 + * When the user accepted or rejected the data submission policy. 1.458 + * 1.459 + * If there was implicit acceptance, this will be set to the time of that. 1.460 + */ 1.461 + get dataSubmissionPolicyResponseDate() { 1.462 + return CommonUtils.getDatePref(this._prefs, 1.463 + "dataSubmissionPolicyResponseTime", 1.464 + 0, this._log, OLDEST_ALLOWED_YEAR); 1.465 + }, 1.466 + 1.467 + set dataSubmissionPolicyResponseDate(value) { 1.468 + this._log.debug("Setting user notified reaction date: " + value); 1.469 + CommonUtils.setDatePref(this._prefs, 1.470 + "dataSubmissionPolicyResponseTime", 1.471 + value, OLDEST_ALLOWED_YEAR); 1.472 + }, 1.473 + 1.474 + /** 1.475 + * Records the result of user notification of data submission policy. 1.476 + * 1.477 + * This is used for logging and diagnostics purposes. It can answer the 1.478 + * question "how was data submission agreed to on this profile?" 1.479 + * 1.480 + * Not all values are defined by this type and can come from other systems. 1.481 + * 1.482 + * The value must be a string and should be something machine readable. e.g. 1.483 + * "accept-user-clicked-ok-button-in-info-bar" 1.484 + */ 1.485 + get dataSubmissionPolicyResponseType() { 1.486 + return this._prefs.get("dataSubmissionPolicyResponseType", 1.487 + "none-recorded"); 1.488 + }, 1.489 + 1.490 + set dataSubmissionPolicyResponseType(value) { 1.491 + if (typeof(value) != "string") { 1.492 + throw new Error("Value must be a string. Got " + typeof(value)); 1.493 + } 1.494 + 1.495 + this._prefs.set("dataSubmissionPolicyResponseType", value); 1.496 + }, 1.497 + 1.498 + /** 1.499 + * Whether submission of data is allowed. 1.500 + * 1.501 + * This is the master switch for remote server communication. If it is 1.502 + * false, we never request upload or deletion. 1.503 + */ 1.504 + get dataSubmissionEnabled() { 1.505 + // Default is true because we are opt-out. 1.506 + return this._prefs.get("dataSubmissionEnabled", true); 1.507 + }, 1.508 + 1.509 + set dataSubmissionEnabled(value) { 1.510 + this._prefs.set("dataSubmissionEnabled", !!value); 1.511 + }, 1.512 + 1.513 + /** 1.514 + * The minimum policy version which for dataSubmissionPolicyAccepted to 1.515 + * to be valid. 1.516 + */ 1.517 + get minimumPolicyVersion() { 1.518 + // First check if the current channel has an ove 1.519 + let channel = UpdateChannel.get(false); 1.520 + let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel); 1.521 + return channelPref !== undefined ? 1.522 + channelPref : this._prefs.get("minimumPolicyVersion", 1); 1.523 + }, 1.524 + 1.525 + /** 1.526 + * Whether the user has accepted that data submission can occur. 1.527 + * 1.528 + * This overrides dataSubmissionEnabled. 1.529 + */ 1.530 + get dataSubmissionPolicyAccepted() { 1.531 + // Be conservative and default to false. 1.532 + return this._prefs.get("dataSubmissionPolicyAccepted", false); 1.533 + }, 1.534 + 1.535 + set dataSubmissionPolicyAccepted(value) { 1.536 + this._prefs.set("dataSubmissionPolicyAccepted", !!value); 1.537 + if (!!value) { 1.538 + let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1); 1.539 + this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion); 1.540 + } else { 1.541 + this._prefs.reset("dataSubmissionPolicyAcceptedVersion"); 1.542 + } 1.543 + }, 1.544 + 1.545 + /** 1.546 + * The state of user notification of the data policy. 1.547 + * 1.548 + * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data 1.549 + * submission can occur. 1.550 + * 1.551 + * @return DataReportingPolicy.STATE_NOTIFY_* constant. 1.552 + */ 1.553 + get notifyState() { 1.554 + if (this.dataSubmissionPolicyResponseDate.getTime()) { 1.555 + return this.STATE_NOTIFY_COMPLETE; 1.556 + } 1.557 + 1.558 + // We get the local state - not the state from prefs - because we don't want 1.559 + // a value from a previous application run to interfere. This prevents 1.560 + // a scenario where notification occurs just before application shutdown and 1.561 + // notification is displayed for shorter than the policy requires. 1.562 + if (!this._dataSubmissionPolicyNotifiedDate) { 1.563 + return this.STATE_NOTIFY_UNNOTIFIED; 1.564 + } 1.565 + 1.566 + return this.STATE_NOTIFY_WAIT; 1.567 + }, 1.568 + 1.569 + /** 1.570 + * When this policy last requested data submission. 1.571 + * 1.572 + * This is used mainly for forensics purposes and should have no bearing 1.573 + * on scheduling or run-time behavior. 1.574 + */ 1.575 + get lastDataSubmissionRequestedDate() { 1.576 + return CommonUtils.getDatePref(this._healthReportPrefs, 1.577 + "lastDataSubmissionRequestedTime", 0, 1.578 + this._log, OLDEST_ALLOWED_YEAR); 1.579 + }, 1.580 + 1.581 + set lastDataSubmissionRequestedDate(value) { 1.582 + CommonUtils.setDatePref(this._healthReportPrefs, 1.583 + "lastDataSubmissionRequestedTime", 1.584 + value, OLDEST_ALLOWED_YEAR); 1.585 + }, 1.586 + 1.587 + /** 1.588 + * When the last data submission actually occurred. 1.589 + * 1.590 + * This is used mainly for forensics purposes and should have no bearing on 1.591 + * actual scheduling. 1.592 + */ 1.593 + get lastDataSubmissionSuccessfulDate() { 1.594 + return CommonUtils.getDatePref(this._healthReportPrefs, 1.595 + "lastDataSubmissionSuccessfulTime", 0, 1.596 + this._log, OLDEST_ALLOWED_YEAR); 1.597 + }, 1.598 + 1.599 + set lastDataSubmissionSuccessfulDate(value) { 1.600 + CommonUtils.setDatePref(this._healthReportPrefs, 1.601 + "lastDataSubmissionSuccessfulTime", 1.602 + value, OLDEST_ALLOWED_YEAR); 1.603 + }, 1.604 + 1.605 + /** 1.606 + * When we last encountered a submission failure. 1.607 + * 1.608 + * This is used for forensics purposes and should have no bearing on 1.609 + * scheduling. 1.610 + */ 1.611 + get lastDataSubmissionFailureDate() { 1.612 + return CommonUtils.getDatePref(this._healthReportPrefs, 1.613 + "lastDataSubmissionFailureTime", 1.614 + 0, this._log, OLDEST_ALLOWED_YEAR); 1.615 + }, 1.616 + 1.617 + set lastDataSubmissionFailureDate(value) { 1.618 + CommonUtils.setDatePref(this._healthReportPrefs, 1.619 + "lastDataSubmissionFailureTime", 1.620 + value, OLDEST_ALLOWED_YEAR); 1.621 + }, 1.622 + 1.623 + /** 1.624 + * When the next data submission is scheduled to occur. 1.625 + * 1.626 + * This is maintained internally by this type. External users should not 1.627 + * mutate this value. 1.628 + */ 1.629 + get nextDataSubmissionDate() { 1.630 + return CommonUtils.getDatePref(this._healthReportPrefs, 1.631 + "nextDataSubmissionTime", 0, 1.632 + this._log, OLDEST_ALLOWED_YEAR); 1.633 + }, 1.634 + 1.635 + set nextDataSubmissionDate(value) { 1.636 + CommonUtils.setDatePref(this._healthReportPrefs, 1.637 + "nextDataSubmissionTime", value, 1.638 + OLDEST_ALLOWED_YEAR); 1.639 + }, 1.640 + 1.641 + /** 1.642 + * The number of submission failures for this day's upload. 1.643 + * 1.644 + * This is used to drive backoff and scheduling. 1.645 + */ 1.646 + get currentDaySubmissionFailureCount() { 1.647 + let v = this._healthReportPrefs.get("currentDaySubmissionFailureCount", 0); 1.648 + 1.649 + if (!Number.isInteger(v)) { 1.650 + v = 0; 1.651 + } 1.652 + 1.653 + return v; 1.654 + }, 1.655 + 1.656 + set currentDaySubmissionFailureCount(value) { 1.657 + if (!Number.isInteger(value)) { 1.658 + throw new Error("Value must be integer: " + value); 1.659 + } 1.660 + 1.661 + this._healthReportPrefs.set("currentDaySubmissionFailureCount", value); 1.662 + }, 1.663 + 1.664 + /** 1.665 + * Whether a request to delete remote data is awaiting completion. 1.666 + * 1.667 + * If this is true, the policy will request that remote data be deleted. 1.668 + * Furthermore, no new data will be uploaded (if it's even allowed) until 1.669 + * the remote deletion is fulfilled. 1.670 + */ 1.671 + get pendingDeleteRemoteData() { 1.672 + return !!this._healthReportPrefs.get("pendingDeleteRemoteData", false); 1.673 + }, 1.674 + 1.675 + set pendingDeleteRemoteData(value) { 1.676 + this._healthReportPrefs.set("pendingDeleteRemoteData", !!value); 1.677 + }, 1.678 + 1.679 + /** 1.680 + * Whether upload of Firefox Health Report data is enabled. 1.681 + */ 1.682 + get healthReportUploadEnabled() { 1.683 + return !!this._healthReportPrefs.get("uploadEnabled", true); 1.684 + }, 1.685 + 1.686 + // External callers should update this via `recordHealthReportUploadEnabled` 1.687 + // to ensure appropriate side-effects are performed. 1.688 + set healthReportUploadEnabled(value) { 1.689 + this._healthReportPrefs.set("uploadEnabled", !!value); 1.690 + }, 1.691 + 1.692 + /** 1.693 + * Whether the FHR upload enabled setting is locked and can't be changed. 1.694 + */ 1.695 + get healthReportUploadLocked() { 1.696 + return this._healthReportPrefs.locked("uploadEnabled"); 1.697 + }, 1.698 + 1.699 + /** 1.700 + * Record user acceptance of data submission policy. 1.701 + * 1.702 + * Data submission will not be allowed to occur until this is called. 1.703 + * 1.704 + * This is typically called through the `onUserAccept` property attached to 1.705 + * the promise passed to `onUserNotify` in the policy listener. But, it can 1.706 + * be called through other interfaces at any time and the call will have 1.707 + * an impact on future data submissions. 1.708 + * 1.709 + * @param reason 1.710 + * (string) How the user accepted the data submission policy. 1.711 + */ 1.712 + recordUserAcceptance: function recordUserAcceptance(reason="no-reason") { 1.713 + this._log.info("User accepted data submission policy: " + reason); 1.714 + this.dataSubmissionPolicyResponseDate = this.now(); 1.715 + this.dataSubmissionPolicyResponseType = "accepted-" + reason; 1.716 + this.dataSubmissionPolicyAccepted = true; 1.717 + }, 1.718 + 1.719 + /** 1.720 + * Record user rejection of submission policy. 1.721 + * 1.722 + * Data submission will not be allowed to occur if this is called. 1.723 + * 1.724 + * This is typically called through the `onUserReject` property attached to 1.725 + * the promise passed to `onUserNotify` in the policy listener. But, it can 1.726 + * be called through other interfaces at any time and the call will have an 1.727 + * impact on future data submissions. 1.728 + */ 1.729 + recordUserRejection: function recordUserRejection(reason="no-reason") { 1.730 + this._log.info("User rejected data submission policy: " + reason); 1.731 + this.dataSubmissionPolicyResponseDate = this.now(); 1.732 + this.dataSubmissionPolicyResponseType = "rejected-" + reason; 1.733 + this.dataSubmissionPolicyAccepted = false; 1.734 + }, 1.735 + 1.736 + /** 1.737 + * Record the user's intent for whether FHR should upload data. 1.738 + * 1.739 + * This is the preferred way for XUL applications to record a user's 1.740 + * preference on whether Firefox Health Report should upload data to 1.741 + * a server. 1.742 + * 1.743 + * If upload is disabled through this API, a request for remote data 1.744 + * deletion is initiated automatically. 1.745 + * 1.746 + * If upload is being disabled and this operation is scheduled to 1.747 + * occur immediately, a promise will be returned. This promise will be 1.748 + * fulfilled when the deletion attempt finishes. If upload is being 1.749 + * disabled and a promise is not returned, callers must poll 1.750 + * `haveRemoteData` on the HealthReporter instance to see if remote 1.751 + * data has been deleted. 1.752 + * 1.753 + * @param flag 1.754 + * (bool) Whether data submission is enabled or disabled. 1.755 + * @param reason 1.756 + * (string) Why this value is being adjusted. For logging 1.757 + * purposes only. 1.758 + */ 1.759 + recordHealthReportUploadEnabled: function (flag, reason="no-reason") { 1.760 + let result = null; 1.761 + if (!flag) { 1.762 + result = this.deleteRemoteData(reason); 1.763 + } 1.764 + 1.765 + this.healthReportUploadEnabled = flag; 1.766 + return result; 1.767 + }, 1.768 + 1.769 + /** 1.770 + * Request that remote data be deleted. 1.771 + * 1.772 + * This will record an intent that previously uploaded data is to be deleted. 1.773 + * The policy will eventually issue a request to the listener for data 1.774 + * deletion. It will keep asking for deletion until the listener acknowledges 1.775 + * that data has been deleted. 1.776 + */ 1.777 + deleteRemoteData: function deleteRemoteData(reason="no-reason") { 1.778 + this._log.info("Remote data deletion requested: " + reason); 1.779 + 1.780 + this.pendingDeleteRemoteData = true; 1.781 + 1.782 + // We want delete deletion to occur as soon as possible. Move up any 1.783 + // pending scheduled data submission and try to trigger. 1.784 + this.nextDataSubmissionDate = this.now(); 1.785 + return this.checkStateAndTrigger(); 1.786 + }, 1.787 + 1.788 + /** 1.789 + * Start background polling for activity. 1.790 + * 1.791 + * This will set up a recurring timer that will periodically check if 1.792 + * activity is warranted. 1.793 + * 1.794 + * You typically call this function for each constructed instance. 1.795 + */ 1.796 + startPolling: function startPolling() { 1.797 + this.stopPolling(); 1.798 + 1.799 + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.800 + this._timer.initWithCallback({ 1.801 + notify: function notify() { 1.802 + this.checkStateAndTrigger(); 1.803 + }.bind(this) 1.804 + }, this.POLL_INTERVAL_MSEC, this._timer.TYPE_REPEATING_SLACK); 1.805 + }, 1.806 + 1.807 + /** 1.808 + * Stop background polling for activity. 1.809 + * 1.810 + * This should be called when the instance is no longer needed. 1.811 + */ 1.812 + stopPolling: function stopPolling() { 1.813 + if (this._timer) { 1.814 + this._timer.cancel(); 1.815 + this._timer = null; 1.816 + } 1.817 + }, 1.818 + 1.819 + /** 1.820 + * Abstraction for obtaining current time. 1.821 + * 1.822 + * The purpose of this is to facilitate testing. Testing code can monkeypatch 1.823 + * this on instances instead of modifying the singleton Date object. 1.824 + */ 1.825 + now: function now() { 1.826 + return new Date(); 1.827 + }, 1.828 + 1.829 + /** 1.830 + * Check state and trigger actions, if necessary. 1.831 + * 1.832 + * This is what enforces the submission and notification policy detailed 1.833 + * above. You can think of this as the driver for health report data 1.834 + * submission. 1.835 + * 1.836 + * Typically this function is called automatically by the background polling. 1.837 + * But, it can safely be called manually as needed. 1.838 + */ 1.839 + checkStateAndTrigger: function checkStateAndTrigger() { 1.840 + // If the master data submission kill switch is toggled, we have nothing 1.841 + // to do. We don't notify about data policies because this would have 1.842 + // no effect. 1.843 + if (!this.dataSubmissionEnabled) { 1.844 + this._log.debug("Data submission is disabled. Doing nothing."); 1.845 + return; 1.846 + } 1.847 + 1.848 + let now = this.now(); 1.849 + let nowT = now.getTime(); 1.850 + let nextSubmissionDate = this.nextDataSubmissionDate; 1.851 + 1.852 + // If the system clock were ever set to a time in the distant future, 1.853 + // it's possible our next schedule date is far out as well. We know 1.854 + // we shouldn't schedule for more than a day out, so we reset the next 1.855 + // scheduled date appropriately. 3 days was chosen arbitrarily. 1.856 + if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) { 1.857 + this._log.warn("Next data submission time is far away. Was the system " + 1.858 + "clock recently readjusted? " + nextSubmissionDate); 1.859 + 1.860 + // It shouldn't really matter what we set this to. 1 day in the future 1.861 + // should be pretty safe. 1.862 + this._moveScheduleForward24h(); 1.863 + 1.864 + // Fall through since we may have other actions. 1.865 + } 1.866 + 1.867 + // Tend to any in progress work. 1.868 + if (this._processInProgressSubmission()) { 1.869 + return; 1.870 + } 1.871 + 1.872 + // Requests to delete remote data take priority above everything else. 1.873 + if (this.pendingDeleteRemoteData) { 1.874 + if (nowT < nextSubmissionDate.getTime()) { 1.875 + this._log.debug("Deletion request is scheduled for the future: " + 1.876 + nextSubmissionDate); 1.877 + return; 1.878 + } 1.879 + 1.880 + return this._dispatchSubmissionRequest("onRequestRemoteDelete", true); 1.881 + } 1.882 + 1.883 + if (!this.healthReportUploadEnabled) { 1.884 + this._log.debug("Data upload is disabled. Doing nothing."); 1.885 + return; 1.886 + } 1.887 + 1.888 + // If the user hasn't responded to the data policy, don't do anything. 1.889 + if (!this.ensureNotifyResponse(now)) { 1.890 + return; 1.891 + } 1.892 + 1.893 + // User has opted out of data submission. 1.894 + if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) { 1.895 + this._log.debug("Data submission has been disabled per user request."); 1.896 + return; 1.897 + } 1.898 + 1.899 + // User has responded to data policy and data submission is enabled. Now 1.900 + // comes the scheduling part. 1.901 + 1.902 + if (nowT < nextSubmissionDate.getTime()) { 1.903 + this._log.debug("Next data submission is scheduled in the future: " + 1.904 + nextSubmissionDate); 1.905 + return; 1.906 + } 1.907 + 1.908 + return this._dispatchSubmissionRequest("onRequestDataUpload", false); 1.909 + }, 1.910 + 1.911 + /** 1.912 + * Ensure user has responded to data submission policy. 1.913 + * 1.914 + * This must be called before data submission. If the policy has not been 1.915 + * responded to, data submission must not occur. 1.916 + * 1.917 + * @return bool Whether user has responded to data policy. 1.918 + */ 1.919 + ensureNotifyResponse: function ensureNotifyResponse(now) { 1.920 + if (this.dataSubmissionPolicyBypassAcceptance) { 1.921 + return true; 1.922 + } 1.923 + 1.924 + let notifyState = this.notifyState; 1.925 + 1.926 + if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) { 1.927 + let notifyAt = new Date(this.firstRunDate.getTime() + 1.928 + this.SUBMISSION_NOTIFY_INTERVAL_MSEC); 1.929 + 1.930 + if (now.getTime() < notifyAt.getTime()) { 1.931 + this._log.debug("Don't have to notify about data submission yet."); 1.932 + return false; 1.933 + } 1.934 + 1.935 + let onComplete = function onComplete() { 1.936 + this._log.info("Data submission notification presented."); 1.937 + let now = this.now(); 1.938 + 1.939 + this._dataSubmissionPolicyNotifiedDate = now; 1.940 + this.dataSubmissionPolicyNotifiedDate = now; 1.941 + }.bind(this); 1.942 + 1.943 + let deferred = Promise.defer(); 1.944 + 1.945 + deferred.promise.then(onComplete, (error) => { 1.946 + this._log.warn("Data policy notification presentation failed: " + 1.947 + CommonUtils.exceptionStr(error)); 1.948 + }); 1.949 + 1.950 + this._log.info("Requesting display of data policy."); 1.951 + let request = new NotifyPolicyRequest(this, deferred); 1.952 + 1.953 + try { 1.954 + this._listener.onNotifyDataPolicy(request); 1.955 + } catch (ex) { 1.956 + this._log.warn("Exception when calling onNotifyDataPolicy: " + 1.957 + CommonUtils.exceptionStr(ex)); 1.958 + } 1.959 + return false; 1.960 + } 1.961 + 1.962 + // We're waiting for user action or implicit acceptance after display. 1.963 + if (notifyState == this.STATE_NOTIFY_WAIT) { 1.964 + // Check for implicit acceptance. 1.965 + let implicitAcceptance = 1.966 + this._dataSubmissionPolicyNotifiedDate.getTime() + 1.967 + this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC; 1.968 + 1.969 + this._log.debug("Now: " + now.getTime()); 1.970 + this._log.debug("Will accept: " + implicitAcceptance); 1.971 + if (now.getTime() < implicitAcceptance) { 1.972 + this._log.debug("Still waiting for reaction or implicit acceptance. " + 1.973 + "Now: " + now.getTime() + " < " + 1.974 + "Accept: " + implicitAcceptance); 1.975 + return false; 1.976 + } 1.977 + 1.978 + this.recordUserAcceptance("implicit-time-elapsed"); 1.979 + return true; 1.980 + } 1.981 + 1.982 + // If this happens, we have a coding error in this file. 1.983 + if (notifyState != this.STATE_NOTIFY_COMPLETE) { 1.984 + throw new Error("Unknown notification state: " + notifyState); 1.985 + } 1.986 + 1.987 + return true; 1.988 + }, 1.989 + 1.990 + _processInProgressSubmission: function _processInProgressSubmission() { 1.991 + if (!this._inProgressSubmissionRequest) { 1.992 + return false; 1.993 + } 1.994 + 1.995 + let now = this.now().getTime(); 1.996 + if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) { 1.997 + this._log.info("Waiting on in-progress submission request to finish."); 1.998 + return true; 1.999 + } 1.1000 + 1.1001 + this._log.warn("Old submission request has expired from no activity."); 1.1002 + this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired.")); 1.1003 + this._inProgressSubmissionRequest = null; 1.1004 + this._handleSubmissionFailure(); 1.1005 + 1.1006 + return false; 1.1007 + }, 1.1008 + 1.1009 + _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) { 1.1010 + let now = this.now(); 1.1011 + 1.1012 + // We're past our scheduled next data submission date, so let's do it! 1.1013 + this.lastDataSubmissionRequestedDate = now; 1.1014 + let deferred = Promise.defer(); 1.1015 + let requestExpiresDate = 1.1016 + this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC); 1.1017 + this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred, 1.1018 + requestExpiresDate, 1.1019 + isDelete); 1.1020 + 1.1021 + let onSuccess = function onSuccess(result) { 1.1022 + this._inProgressSubmissionRequest = null; 1.1023 + this._handleSubmissionResult(result); 1.1024 + }.bind(this); 1.1025 + 1.1026 + let onError = function onError(error) { 1.1027 + this._log.error("Error when handling data submission result: " + 1.1028 + CommonUtils.exceptionStr(error)); 1.1029 + this._inProgressSubmissionRequest = null; 1.1030 + this._handleSubmissionFailure(); 1.1031 + }.bind(this); 1.1032 + 1.1033 + let chained = deferred.promise.then(onSuccess, onError); 1.1034 + 1.1035 + this._log.info("Requesting data submission. Will expire at " + 1.1036 + requestExpiresDate); 1.1037 + try { 1.1038 + this._listener[handler](this._inProgressSubmissionRequest); 1.1039 + } catch (ex) { 1.1040 + this._log.warn("Exception when calling " + handler + ": " + 1.1041 + CommonUtils.exceptionStr(ex)); 1.1042 + this._inProgressSubmissionRequest = null; 1.1043 + this._handleSubmissionFailure(); 1.1044 + return; 1.1045 + } 1.1046 + 1.1047 + return chained; 1.1048 + }, 1.1049 + 1.1050 + _handleSubmissionResult: function _handleSubmissionResult(request) { 1.1051 + let state = request.state; 1.1052 + let reason = request.reason || "no reason"; 1.1053 + this._log.info("Got submission request result: " + state); 1.1054 + 1.1055 + if (state == request.SUBMISSION_SUCCESS) { 1.1056 + if (request.isDelete) { 1.1057 + this.pendingDeleteRemoteData = false; 1.1058 + this._log.info("Successful data delete reported."); 1.1059 + } else { 1.1060 + this._log.info("Successful data upload reported."); 1.1061 + } 1.1062 + 1.1063 + this.lastDataSubmissionSuccessfulDate = request.submissionDate; 1.1064 + 1.1065 + let nextSubmissionDate = 1.1066 + new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY); 1.1067 + 1.1068 + // Schedule pending deletes immediately. This has potential to overload 1.1069 + // the server. However, the frequency of delete requests across all 1.1070 + // clients should be low, so this shouldn't pose a problem. 1.1071 + if (this.pendingDeleteRemoteData) { 1.1072 + nextSubmissionDate = this.now(); 1.1073 + } 1.1074 + 1.1075 + this.nextDataSubmissionDate = nextSubmissionDate; 1.1076 + this.currentDaySubmissionFailureCount = 0; 1.1077 + return; 1.1078 + } 1.1079 + 1.1080 + if (state == request.NO_DATA_AVAILABLE) { 1.1081 + if (request.isDelete) { 1.1082 + this._log.info("Remote data delete requested but no remote data was stored."); 1.1083 + this.pendingDeleteRemoteData = false; 1.1084 + return; 1.1085 + } 1.1086 + 1.1087 + this._log.info("No data was available to submit. May try later."); 1.1088 + this._handleSubmissionFailure(); 1.1089 + return; 1.1090 + } 1.1091 + 1.1092 + // We don't special case request.isDelete for these failures because it 1.1093 + // likely means there was a server error. 1.1094 + 1.1095 + if (state == request.SUBMISSION_FAILURE_SOFT) { 1.1096 + this._log.warn("Soft error submitting data: " + reason); 1.1097 + this.lastDataSubmissionFailureDate = this.now(); 1.1098 + this._handleSubmissionFailure(); 1.1099 + return; 1.1100 + } 1.1101 + 1.1102 + if (state == request.SUBMISSION_FAILURE_HARD) { 1.1103 + this._log.warn("Hard error submitting data: " + reason); 1.1104 + this.lastDataSubmissionFailureDate = this.now(); 1.1105 + this._moveScheduleForward24h(); 1.1106 + return; 1.1107 + } 1.1108 + 1.1109 + throw new Error("Unknown state on DataSubmissionRequest: " + request.state); 1.1110 + }, 1.1111 + 1.1112 + _handleSubmissionFailure: function _handleSubmissionFailure() { 1.1113 + if (this.currentDaySubmissionFailureCount >= this.FAILURE_BACKOFF_INTERVALS.length) { 1.1114 + this._log.warn("Reached the limit of daily submission attempts. " + 1.1115 + "Rescheduling for tomorrow."); 1.1116 + this._moveScheduleForward24h(); 1.1117 + return false; 1.1118 + } 1.1119 + 1.1120 + let offset = this.FAILURE_BACKOFF_INTERVALS[this.currentDaySubmissionFailureCount]; 1.1121 + this.nextDataSubmissionDate = this._futureDate(offset); 1.1122 + this.currentDaySubmissionFailureCount++; 1.1123 + return true; 1.1124 + }, 1.1125 + 1.1126 + _moveScheduleForward24h: function _moveScheduleForward24h() { 1.1127 + let d = this._futureDate(MILLISECONDS_PER_DAY); 1.1128 + this._log.info("Setting next scheduled data submission for " + d); 1.1129 + 1.1130 + this.nextDataSubmissionDate = d; 1.1131 + this.currentDaySubmissionFailureCount = 0; 1.1132 + }, 1.1133 + 1.1134 + _futureDate: function _futureDate(offset) { 1.1135 + return new Date(this.now().getTime() + offset); 1.1136 + }, 1.1137 +}); 1.1138 +