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