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