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// This module provides a facility for disconnecting Sync and FxA, optionally
6// sanitizing profile data as part of the process.
7
8const { XPCOMUtils } = ChromeUtils.import(
9  "resource://gre/modules/XPCOMUtils.jsm"
10);
11
12XPCOMUtils.defineLazyModuleGetters(this, {
13  Services: "resource://gre/modules/Services.jsm",
14  Log: "resource://gre/modules/Log.jsm",
15  Sanitizer: "resource:///modules/Sanitizer.jsm",
16  AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
17  fxAccounts: "resource://gre/modules/FxAccounts.jsm",
18  setTimeout: "resource://gre/modules/Timer.jsm",
19  Utils: "resource://services-sync/util.js",
20});
21
22XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
23  return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", {});
24});
25
26this.EXPORTED_SYMBOLS = ["SyncDisconnectInternal", "SyncDisconnect"];
27
28this.SyncDisconnectInternal = {
29  lockRetryInterval: 1000, // wait 1 seconds before trying for the lock again.
30  lockRetryCount: 120, // Try 120 times (==2 mins) before giving up in disgust.
31  promiseDisconnectFinished: null, // If we are sanitizing, a promise for completion.
32
33  // mocked by tests.
34  getWeave() {
35    return ChromeUtils.import("resource://services-sync/main.js", {}).Weave;
36  },
37
38  // Returns a promise that resolves when we are not syncing, waiting until
39  // a current Sync completes if necessary. Resolves with true if we
40  // successfully waited, in which case the sync lock will have been taken to
41  // ensure future syncs don't state, or resolves with false if we gave up
42  // waiting for the sync to complete (in which case we didn't take a lock -
43  // but note that Sync probably remains locked in this case regardless.)
44  async promiseNotSyncing(abortController) {
45    let weave = this.getWeave();
46    let log = Log.repository.getLogger("Sync.Service");
47    // We might be syncing - poll for up to 2 minutes waiting for the lock.
48    // (2 minutes seems extreme, but should be very rare.)
49    return new Promise(resolve => {
50      abortController.signal.onabort = () => {
51        resolve(false);
52      };
53
54      let attempts = 0;
55      let checkLock = () => {
56        if (abortController.signal.aborted) {
57          // We've already resolved, so don't want a new timer to ever start.
58          return;
59        }
60        if (weave.Service.lock()) {
61          resolve(true);
62          return;
63        }
64        attempts += 1;
65        if (attempts >= this.lockRetryCount) {
66          log.error(
67            "Gave up waiting for the sync lock - going ahead with sanitize anyway"
68          );
69          resolve(false);
70          return;
71        }
72        log.debug("Waiting a couple of seconds to get the sync lock");
73        setTimeout(checkLock, this.lockRetryInterval);
74      };
75      checkLock();
76    });
77  },
78
79  // Sanitize Sync-related data.
80  async doSanitizeSyncData() {
81    let weave = this.getWeave();
82    // Get the sync logger - if stuff goes wrong it can be useful to have that
83    // recorded in the sync logs.
84    let log = Log.repository.getLogger("Sync.Service");
85    log.info("Starting santitize of Sync data");
86    try {
87      // We clobber data for all Sync engines that are enabled.
88      await weave.Service.promiseInitialized;
89      weave.Service.enabled = false;
90
91      log.info("starting actual sanitization");
92      for (let engine of weave.Service.engineManager.getAll()) {
93        if (engine.enabled) {
94          try {
95            log.info("Wiping engine", engine.name);
96            await engine.wipeClient();
97          } catch (ex) {
98            log.error("Failed to wipe engine", ex);
99          }
100        }
101      }
102      // Reset the pref which is used to show a warning when a different user
103      // signs in - this is no longer a concern now that we've removed the
104      // data from the profile.
105      Services.prefs.clearUserPref(FxAccountsCommon.PREF_LAST_FXA_USER);
106
107      log.info("Finished wiping sync data");
108    } catch (ex) {
109      log.error("Failed to sanitize Sync data", ex);
110      console.error("Failed to sanitize Sync data", ex);
111    }
112    try {
113      // ensure any logs we wrote are flushed to disk.
114      await weave.Service.errorHandler.resetFileLog();
115    } catch (ex) {
116      console.log("Failed to flush the Sync log", ex);
117    }
118  },
119
120  // Sanitize all Browser data.
121  async doSanitizeBrowserData() {
122    try {
123      // sanitize everything other than "open windows" (and we don't do that
124      // because it may confuse the user - they probably want to see
125      // about:prefs with the disconnection reflected.
126      let itemsToClear = Object.keys(Sanitizer.items).filter(
127        k => k != "openWindows"
128      );
129      await Sanitizer.sanitize(itemsToClear);
130    } catch (ex) {
131      console.error("Failed to sanitize other data", ex);
132    }
133  },
134
135  async doSyncAndAccountDisconnect(shouldUnlock) {
136    // We do a startOver of Sync first - if we do the account first we end
137    // up with Sync configured but FxA not configured, which causes the browser
138    // UI to briefly enter a "needs reauth" state.
139    let Weave = this.getWeave();
140    await Weave.Service.promiseInitialized;
141    await Weave.Service.startOver();
142    await fxAccounts.signOut();
143    // Sync may have been disabled if we santized, so re-enable it now or
144    // else the user will be unable to resync should they sign in before a
145    // restart.
146    Weave.Service.enabled = true;
147
148    // and finally, if we managed to get the lock before, we should unlock it
149    // now.
150    if (shouldUnlock) {
151      Weave.Service.unlock();
152    }
153  },
154
155  // Start the sanitization process. Returns a promise that resolves when
156  // the sanitize is complete, and an AbortController which can be used to
157  // abort the process of waiting for a sync to complete.
158  async _startDisconnect(abortController, sanitizeData = false) {
159    // This is a bit convoluted - we want to wait for a sync to finish before
160    // sanitizing, but want to abort that wait if the browser shuts down while
161    // we are waiting (in which case we'll charge ahead anyway).
162    // So we do this by using an AbortController and passing that to the
163    // function that waits for the sync lock - it will immediately resolve
164    // if the abort controller is aborted.
165    let log = Log.repository.getLogger("Sync.Service");
166
167    // If the master-password is locked then we will fail to fully sanitize,
168    // so prompt for that now. If canceled, we just abort now.
169    log.info("checking master-password state");
170    if (!Utils.ensureMPUnlocked()) {
171      log.warn(
172        "The master-password needs to be unlocked to fully disconnect from sync"
173      );
174      return;
175    }
176
177    log.info("waiting for any existing syncs to complete");
178    let locked = await this.promiseNotSyncing(abortController);
179
180    if (sanitizeData) {
181      await this.doSanitizeSyncData();
182
183      // We disconnect before sanitizing the browser data - in a worst-case
184      // scenario where the sanitize takes so long that even the shutdown
185      // blocker doesn't allow it to finish, we should still at least be in
186      // a disconnected state on the next startup.
187      log.info("disconnecting account");
188      await this.doSyncAndAccountDisconnect(locked);
189
190      await this.doSanitizeBrowserData();
191    } else {
192      log.info("disconnecting account");
193      await this.doSyncAndAccountDisconnect(locked);
194    }
195  },
196
197  async disconnect(sanitizeData) {
198    if (this.promiseDisconnectFinished) {
199      throw new Error("A disconnect is already in progress");
200    }
201    let abortController = new AbortController();
202    let promiseDisconnectFinished = this._startDisconnect(
203      abortController,
204      sanitizeData
205    );
206    this.promiseDisconnectFinished = promiseDisconnectFinished;
207    let shutdownBlocker = () => {
208      // oh dear - we are sanitizing (probably stuck waiting for a sync to
209      // complete) and the browser is shutting down. Let's avoid the wait
210      // for sync to complete and continue the process anyway.
211      abortController.abort();
212      return promiseDisconnectFinished;
213    };
214    AsyncShutdown.quitApplicationGranted.addBlocker(
215      "SyncDisconnect: removing requested data",
216      shutdownBlocker
217    );
218
219    // wait for it to finish - hopefully without the blocker being called.
220    await promiseDisconnectFinished;
221    this.promiseDisconnectFinished = null;
222
223    // sanitize worked so remove our blocker - it's a noop if the blocker
224    // did call us.
225    AsyncShutdown.quitApplicationGranted.removeBlocker(shutdownBlocker);
226  },
227};
228
229this.SyncDisconnect = {
230  get promiseDisconnectFinished() {
231    return SyncDisconnectInternal.promiseDisconnectFinished;
232  },
233
234  disconnect(sanitizeData) {
235    return SyncDisconnectInternal.disconnect(sanitizeData);
236  },
237};
238