Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"]; |
michael@0 | 6 | |
michael@0 | 7 | const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
michael@0 | 8 | |
michael@0 | 9 | Cu.import("resource://gre/modules/Log.jsm"); |
michael@0 | 10 | Cu.import("resource://gre/modules/Promise.jsm"); |
michael@0 | 11 | Cu.import("resource://gre/modules/osfile.jsm"); |
michael@0 | 12 | Cu.import("resource://services-common/utils.js"); |
michael@0 | 13 | Cu.import("resource://services-crypto/utils.js"); |
michael@0 | 14 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 15 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 16 | Cu.import("resource://gre/modules/Timer.jsm"); |
michael@0 | 17 | Cu.import("resource://gre/modules/Task.jsm"); |
michael@0 | 18 | Cu.import("resource://gre/modules/FxAccountsCommon.js"); |
michael@0 | 19 | |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", |
michael@0 | 21 | "resource://gre/modules/FxAccountsClient.jsm"); |
michael@0 | 22 | |
michael@0 | 23 | XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", |
michael@0 | 24 | "resource://gre/modules/identity/jwcrypto.jsm"); |
michael@0 | 25 | |
michael@0 | 26 | // All properties exposed by the public FxAccounts API. |
michael@0 | 27 | let publicProperties = [ |
michael@0 | 28 | "getAccountsClient", |
michael@0 | 29 | "getAccountsSignInURI", |
michael@0 | 30 | "getAccountsSignUpURI", |
michael@0 | 31 | "getAssertion", |
michael@0 | 32 | "getKeys", |
michael@0 | 33 | "getSignedInUser", |
michael@0 | 34 | "loadAndPoll", |
michael@0 | 35 | "localtimeOffsetMsec", |
michael@0 | 36 | "now", |
michael@0 | 37 | "promiseAccountsForceSigninURI", |
michael@0 | 38 | "resendVerificationEmail", |
michael@0 | 39 | "setSignedInUser", |
michael@0 | 40 | "signOut", |
michael@0 | 41 | "version", |
michael@0 | 42 | "whenVerified" |
michael@0 | 43 | ]; |
michael@0 | 44 | |
michael@0 | 45 | // An AccountState object holds all state related to one specific account. |
michael@0 | 46 | // Only one AccountState is ever "current" in the FxAccountsInternal object - |
michael@0 | 47 | // whenever a user logs out or logs in, the current AccountState is discarded, |
michael@0 | 48 | // making it impossible for the wrong state or state data to be accidentally |
michael@0 | 49 | // used. |
michael@0 | 50 | // In addition, it has some promise-related helpers to ensure that if an |
michael@0 | 51 | // attempt is made to resolve a promise on a "stale" state (eg, if an |
michael@0 | 52 | // operation starts, but a different user logs in before the operation |
michael@0 | 53 | // completes), the promise will be rejected. |
michael@0 | 54 | // It is intended to be used thusly: |
michael@0 | 55 | // somePromiseBasedFunction: function() { |
michael@0 | 56 | // let currentState = this.currentAccountState; |
michael@0 | 57 | // return someOtherPromiseFunction().then( |
michael@0 | 58 | // data => currentState.resolve(data) |
michael@0 | 59 | // ); |
michael@0 | 60 | // } |
michael@0 | 61 | // If the state has changed between the function being called and the promise |
michael@0 | 62 | // being resolved, the .resolve() call will actually be rejected. |
michael@0 | 63 | AccountState = function(fxaInternal) { |
michael@0 | 64 | this.fxaInternal = fxaInternal; |
michael@0 | 65 | }; |
michael@0 | 66 | |
michael@0 | 67 | AccountState.prototype = { |
michael@0 | 68 | cert: null, |
michael@0 | 69 | keyPair: null, |
michael@0 | 70 | signedInUser: null, |
michael@0 | 71 | whenVerifiedDeferred: null, |
michael@0 | 72 | whenKeysReadyDeferred: null, |
michael@0 | 73 | |
michael@0 | 74 | get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, |
michael@0 | 75 | |
michael@0 | 76 | abort: function() { |
michael@0 | 77 | if (this.whenVerifiedDeferred) { |
michael@0 | 78 | this.whenVerifiedDeferred.reject( |
michael@0 | 79 | new Error("Verification aborted; Another user signing in")); |
michael@0 | 80 | this.whenVerifiedDeferred = null; |
michael@0 | 81 | } |
michael@0 | 82 | |
michael@0 | 83 | if (this.whenKeysReadyDeferred) { |
michael@0 | 84 | this.whenKeysReadyDeferred.reject( |
michael@0 | 85 | new Error("Verification aborted; Another user signing in")); |
michael@0 | 86 | this.whenKeysReadyDeferred = null; |
michael@0 | 87 | } |
michael@0 | 88 | this.cert = null; |
michael@0 | 89 | this.keyPair = null; |
michael@0 | 90 | this.signedInUser = null; |
michael@0 | 91 | this.fxaInternal = null; |
michael@0 | 92 | }, |
michael@0 | 93 | |
michael@0 | 94 | getUserAccountData: function() { |
michael@0 | 95 | // Skip disk if user is cached. |
michael@0 | 96 | if (this.signedInUser) { |
michael@0 | 97 | return this.resolve(this.signedInUser.accountData); |
michael@0 | 98 | } |
michael@0 | 99 | |
michael@0 | 100 | return this.fxaInternal.signedInUserStorage.get().then( |
michael@0 | 101 | user => { |
michael@0 | 102 | if (logPII) { |
michael@0 | 103 | // don't stringify unless it will be written. We should replace this |
michael@0 | 104 | // check with param substitutions added in bug 966674 |
michael@0 | 105 | log.debug("getUserAccountData -> " + JSON.stringify(user)); |
michael@0 | 106 | } |
michael@0 | 107 | if (user && user.version == this.version) { |
michael@0 | 108 | log.debug("setting signed in user"); |
michael@0 | 109 | this.signedInUser = user; |
michael@0 | 110 | } |
michael@0 | 111 | return this.resolve(user ? user.accountData : null); |
michael@0 | 112 | }, |
michael@0 | 113 | err => { |
michael@0 | 114 | if (err instanceof OS.File.Error && err.becauseNoSuchFile) { |
michael@0 | 115 | // File hasn't been created yet. That will be done |
michael@0 | 116 | // on the first call to getSignedInUser |
michael@0 | 117 | return this.resolve(null); |
michael@0 | 118 | } |
michael@0 | 119 | return this.reject(err); |
michael@0 | 120 | } |
michael@0 | 121 | ); |
michael@0 | 122 | }, |
michael@0 | 123 | |
michael@0 | 124 | setUserAccountData: function(accountData) { |
michael@0 | 125 | return this.fxaInternal.signedInUserStorage.get().then(record => { |
michael@0 | 126 | if (!this.isCurrent) { |
michael@0 | 127 | return this.reject(new Error("Another user has signed in")); |
michael@0 | 128 | } |
michael@0 | 129 | record.accountData = accountData; |
michael@0 | 130 | this.signedInUser = record; |
michael@0 | 131 | return this.fxaInternal.signedInUserStorage.set(record) |
michael@0 | 132 | .then(() => this.resolve(accountData)); |
michael@0 | 133 | }); |
michael@0 | 134 | }, |
michael@0 | 135 | |
michael@0 | 136 | |
michael@0 | 137 | getCertificate: function(data, keyPair, mustBeValidUntil) { |
michael@0 | 138 | if (logPII) { |
michael@0 | 139 | // don't stringify unless it will be written. We should replace this |
michael@0 | 140 | // check with param substitutions added in bug 966674 |
michael@0 | 141 | log.debug("getCertificate" + JSON.stringify(this.signedInUser)); |
michael@0 | 142 | } |
michael@0 | 143 | // TODO: get the lifetime from the cert's .exp field |
michael@0 | 144 | if (this.cert && this.cert.validUntil > mustBeValidUntil) { |
michael@0 | 145 | log.debug(" getCertificate already had one"); |
michael@0 | 146 | return this.resolve(this.cert.cert); |
michael@0 | 147 | } |
michael@0 | 148 | // else get our cert signed |
michael@0 | 149 | let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME; |
michael@0 | 150 | return this.fxaInternal.getCertificateSigned(data.sessionToken, |
michael@0 | 151 | keyPair.serializedPublicKey, |
michael@0 | 152 | CERT_LIFETIME).then( |
michael@0 | 153 | cert => { |
michael@0 | 154 | log.debug("getCertificate got a new one: " + !!cert); |
michael@0 | 155 | this.cert = { |
michael@0 | 156 | cert: cert, |
michael@0 | 157 | validUntil: willBeValidUntil |
michael@0 | 158 | }; |
michael@0 | 159 | return cert; |
michael@0 | 160 | } |
michael@0 | 161 | ).then(result => this.resolve(result)); |
michael@0 | 162 | }, |
michael@0 | 163 | |
michael@0 | 164 | getKeyPair: function(mustBeValidUntil) { |
michael@0 | 165 | if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) { |
michael@0 | 166 | log.debug("getKeyPair: already have a keyPair"); |
michael@0 | 167 | return this.resolve(this.keyPair.keyPair); |
michael@0 | 168 | } |
michael@0 | 169 | // Otherwse, create a keypair and set validity limit. |
michael@0 | 170 | let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME; |
michael@0 | 171 | let d = Promise.defer(); |
michael@0 | 172 | jwcrypto.generateKeyPair("DS160", (err, kp) => { |
michael@0 | 173 | if (err) { |
michael@0 | 174 | return this.reject(err); |
michael@0 | 175 | } |
michael@0 | 176 | this.keyPair = { |
michael@0 | 177 | keyPair: kp, |
michael@0 | 178 | validUntil: willBeValidUntil |
michael@0 | 179 | }; |
michael@0 | 180 | log.debug("got keyPair"); |
michael@0 | 181 | delete this.cert; |
michael@0 | 182 | d.resolve(this.keyPair.keyPair); |
michael@0 | 183 | }); |
michael@0 | 184 | return d.promise.then(result => this.resolve(result)); |
michael@0 | 185 | }, |
michael@0 | 186 | |
michael@0 | 187 | resolve: function(result) { |
michael@0 | 188 | if (!this.isCurrent) { |
michael@0 | 189 | log.info("An accountState promise was resolved, but was actually rejected" + |
michael@0 | 190 | " due to a different user being signed in. Originally resolved" + |
michael@0 | 191 | " with: " + result); |
michael@0 | 192 | return Promise.reject(new Error("A different user signed in")); |
michael@0 | 193 | } |
michael@0 | 194 | return Promise.resolve(result); |
michael@0 | 195 | }, |
michael@0 | 196 | |
michael@0 | 197 | reject: function(error) { |
michael@0 | 198 | // It could be argued that we should just let it reject with the original |
michael@0 | 199 | // error - but this runs the risk of the error being (eg) a 401, which |
michael@0 | 200 | // might cause the consumer to attempt some remediation and cause other |
michael@0 | 201 | // problems. |
michael@0 | 202 | if (!this.isCurrent) { |
michael@0 | 203 | log.info("An accountState promise was rejected, but we are ignoring that" + |
michael@0 | 204 | "reason and rejecting it due to a different user being signed in." + |
michael@0 | 205 | "Originally rejected with: " + reason); |
michael@0 | 206 | return Promise.reject(new Error("A different user signed in")); |
michael@0 | 207 | } |
michael@0 | 208 | return Promise.reject(error); |
michael@0 | 209 | }, |
michael@0 | 210 | |
michael@0 | 211 | } |
michael@0 | 212 | |
michael@0 | 213 | /** |
michael@0 | 214 | * Copies properties from a given object to another object. |
michael@0 | 215 | * |
michael@0 | 216 | * @param from (object) |
michael@0 | 217 | * The object we read property descriptors from. |
michael@0 | 218 | * @param to (object) |
michael@0 | 219 | * The object that we set property descriptors on. |
michael@0 | 220 | * @param options (object) (optional) |
michael@0 | 221 | * {keys: [...]} |
michael@0 | 222 | * Lets the caller pass the names of all properties they want to be |
michael@0 | 223 | * copied. Will copy all properties of the given source object by |
michael@0 | 224 | * default. |
michael@0 | 225 | * {bind: object} |
michael@0 | 226 | * Lets the caller specify the object that will be used to .bind() |
michael@0 | 227 | * all function properties we find to. Will bind to the given target |
michael@0 | 228 | * object by default. |
michael@0 | 229 | */ |
michael@0 | 230 | function copyObjectProperties(from, to, opts = {}) { |
michael@0 | 231 | let keys = (opts && opts.keys) || Object.keys(from); |
michael@0 | 232 | let thisArg = (opts && opts.bind) || to; |
michael@0 | 233 | |
michael@0 | 234 | for (let prop of keys) { |
michael@0 | 235 | let desc = Object.getOwnPropertyDescriptor(from, prop); |
michael@0 | 236 | |
michael@0 | 237 | if (typeof(desc.value) == "function") { |
michael@0 | 238 | desc.value = desc.value.bind(thisArg); |
michael@0 | 239 | } |
michael@0 | 240 | |
michael@0 | 241 | if (desc.get) { |
michael@0 | 242 | desc.get = desc.get.bind(thisArg); |
michael@0 | 243 | } |
michael@0 | 244 | |
michael@0 | 245 | if (desc.set) { |
michael@0 | 246 | desc.set = desc.set.bind(thisArg); |
michael@0 | 247 | } |
michael@0 | 248 | |
michael@0 | 249 | Object.defineProperty(to, prop, desc); |
michael@0 | 250 | } |
michael@0 | 251 | } |
michael@0 | 252 | |
michael@0 | 253 | /** |
michael@0 | 254 | * The public API's constructor. |
michael@0 | 255 | */ |
michael@0 | 256 | this.FxAccounts = function (mockInternal) { |
michael@0 | 257 | let internal = new FxAccountsInternal(); |
michael@0 | 258 | let external = {}; |
michael@0 | 259 | |
michael@0 | 260 | // Copy all public properties to the 'external' object. |
michael@0 | 261 | let prototype = FxAccountsInternal.prototype; |
michael@0 | 262 | let options = {keys: publicProperties, bind: internal}; |
michael@0 | 263 | copyObjectProperties(prototype, external, options); |
michael@0 | 264 | |
michael@0 | 265 | // Copy all of the mock's properties to the internal object. |
michael@0 | 266 | if (mockInternal && !mockInternal.onlySetInternal) { |
michael@0 | 267 | copyObjectProperties(mockInternal, internal); |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | if (mockInternal) { |
michael@0 | 271 | // Exposes the internal object for testing only. |
michael@0 | 272 | external.internal = internal; |
michael@0 | 273 | } |
michael@0 | 274 | |
michael@0 | 275 | return Object.freeze(external); |
michael@0 | 276 | } |
michael@0 | 277 | |
michael@0 | 278 | /** |
michael@0 | 279 | * The internal API's constructor. |
michael@0 | 280 | */ |
michael@0 | 281 | function FxAccountsInternal() { |
michael@0 | 282 | this.version = DATA_FORMAT_VERSION; |
michael@0 | 283 | |
michael@0 | 284 | // Make a local copy of these constants so we can mock it in testing |
michael@0 | 285 | this.POLL_STEP = POLL_STEP; |
michael@0 | 286 | this.POLL_SESSION = POLL_SESSION; |
michael@0 | 287 | // We will create this.pollTimeRemaining below; it will initially be |
michael@0 | 288 | // set to the value of POLL_SESSION. |
michael@0 | 289 | |
michael@0 | 290 | // We interact with the Firefox Accounts auth server in order to confirm that |
michael@0 | 291 | // a user's email has been verified and also to fetch the user's keys from |
michael@0 | 292 | // the server. We manage these processes in possibly long-lived promises |
michael@0 | 293 | // that are internal to this object (never exposed to callers). Because |
michael@0 | 294 | // Firefox Accounts allows for only one logged-in user, and because it's |
michael@0 | 295 | // conceivable that while we are waiting to verify one identity, a caller |
michael@0 | 296 | // could start verification on a second, different identity, we need to be |
michael@0 | 297 | // able to abort all work on the first sign-in process. The currentTimer and |
michael@0 | 298 | // currentAccountState are used for this purpose. |
michael@0 | 299 | // (XXX - should the timer be directly on the currentAccountState?) |
michael@0 | 300 | this.currentTimer = null; |
michael@0 | 301 | this.currentAccountState = new AccountState(this); |
michael@0 | 302 | |
michael@0 | 303 | // We don't reference |profileDir| in the top-level module scope |
michael@0 | 304 | // as we may be imported before we know where it is. |
michael@0 | 305 | this.signedInUserStorage = new JSONStorage({ |
michael@0 | 306 | filename: DEFAULT_STORAGE_FILENAME, |
michael@0 | 307 | baseDir: OS.Constants.Path.profileDir, |
michael@0 | 308 | }); |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | /** |
michael@0 | 312 | * The internal API's prototype. |
michael@0 | 313 | */ |
michael@0 | 314 | FxAccountsInternal.prototype = { |
michael@0 | 315 | |
michael@0 | 316 | /** |
michael@0 | 317 | * The current data format's version number. |
michael@0 | 318 | */ |
michael@0 | 319 | version: DATA_FORMAT_VERSION, |
michael@0 | 320 | |
michael@0 | 321 | _fxAccountsClient: null, |
michael@0 | 322 | |
michael@0 | 323 | get fxAccountsClient() { |
michael@0 | 324 | if (!this._fxAccountsClient) { |
michael@0 | 325 | this._fxAccountsClient = new FxAccountsClient(); |
michael@0 | 326 | } |
michael@0 | 327 | return this._fxAccountsClient; |
michael@0 | 328 | }, |
michael@0 | 329 | |
michael@0 | 330 | /** |
michael@0 | 331 | * Return the current time in milliseconds as an integer. Allows tests to |
michael@0 | 332 | * manipulate the date to simulate certificate expiration. |
michael@0 | 333 | */ |
michael@0 | 334 | now: function() { |
michael@0 | 335 | return this.fxAccountsClient.now(); |
michael@0 | 336 | }, |
michael@0 | 337 | |
michael@0 | 338 | getAccountsClient: function() { |
michael@0 | 339 | return this.fxAccountsClient; |
michael@0 | 340 | }, |
michael@0 | 341 | |
michael@0 | 342 | /** |
michael@0 | 343 | * Return clock offset in milliseconds, as reported by the fxAccountsClient. |
michael@0 | 344 | * This can be overridden for testing. |
michael@0 | 345 | * |
michael@0 | 346 | * The offset is the number of milliseconds that must be added to the client |
michael@0 | 347 | * clock to make it equal to the server clock. For example, if the client is |
michael@0 | 348 | * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. |
michael@0 | 349 | */ |
michael@0 | 350 | get localtimeOffsetMsec() { |
michael@0 | 351 | return this.fxAccountsClient.localtimeOffsetMsec; |
michael@0 | 352 | }, |
michael@0 | 353 | |
michael@0 | 354 | /** |
michael@0 | 355 | * Ask the server whether the user's email has been verified |
michael@0 | 356 | */ |
michael@0 | 357 | checkEmailStatus: function checkEmailStatus(sessionToken) { |
michael@0 | 358 | return this.fxAccountsClient.recoveryEmailStatus(sessionToken); |
michael@0 | 359 | }, |
michael@0 | 360 | |
michael@0 | 361 | /** |
michael@0 | 362 | * Once the user's email is verified, we can request the keys |
michael@0 | 363 | */ |
michael@0 | 364 | fetchKeys: function fetchKeys(keyFetchToken) { |
michael@0 | 365 | log.debug("fetchKeys: " + !!keyFetchToken); |
michael@0 | 366 | if (logPII) { |
michael@0 | 367 | log.debug("fetchKeys - the token is " + keyFetchToken); |
michael@0 | 368 | } |
michael@0 | 369 | return this.fxAccountsClient.accountKeys(keyFetchToken); |
michael@0 | 370 | }, |
michael@0 | 371 | |
michael@0 | 372 | // set() makes sure that polling is happening, if necessary. |
michael@0 | 373 | // get() does not wait for verification, and returns an object even if |
michael@0 | 374 | // unverified. The caller of get() must check .verified . |
michael@0 | 375 | // The "fxaccounts:onverified" event will fire only when the verified |
michael@0 | 376 | // state goes from false to true, so callers must register their observer |
michael@0 | 377 | // and then call get(). In particular, it will not fire when the account |
michael@0 | 378 | // was found to be verified in a previous boot: if our stored state says |
michael@0 | 379 | // the account is verified, the event will never fire. So callers must do: |
michael@0 | 380 | // register notification observer (go) |
michael@0 | 381 | // userdata = get() |
michael@0 | 382 | // if (userdata.verified()) {go()} |
michael@0 | 383 | |
michael@0 | 384 | /** |
michael@0 | 385 | * Get the user currently signed in to Firefox Accounts. |
michael@0 | 386 | * |
michael@0 | 387 | * @return Promise |
michael@0 | 388 | * The promise resolves to the credentials object of the signed-in user: |
michael@0 | 389 | * { |
michael@0 | 390 | * email: The user's email address |
michael@0 | 391 | * uid: The user's unique id |
michael@0 | 392 | * sessionToken: Session for the FxA server |
michael@0 | 393 | * kA: An encryption key from the FxA server |
michael@0 | 394 | * kB: An encryption key derived from the user's FxA password |
michael@0 | 395 | * verified: email verification status |
michael@0 | 396 | * authAt: The time (seconds since epoch) that this record was |
michael@0 | 397 | * authenticated |
michael@0 | 398 | * } |
michael@0 | 399 | * or null if no user is signed in. |
michael@0 | 400 | */ |
michael@0 | 401 | getSignedInUser: function getSignedInUser() { |
michael@0 | 402 | let currentState = this.currentAccountState; |
michael@0 | 403 | return currentState.getUserAccountData().then(data => { |
michael@0 | 404 | if (!data) { |
michael@0 | 405 | return null; |
michael@0 | 406 | } |
michael@0 | 407 | if (!this.isUserEmailVerified(data)) { |
michael@0 | 408 | // If the email is not verified, start polling for verification, |
michael@0 | 409 | // but return null right away. We don't want to return a promise |
michael@0 | 410 | // that might not be fulfilled for a long time. |
michael@0 | 411 | this.startVerifiedCheck(data); |
michael@0 | 412 | } |
michael@0 | 413 | return data; |
michael@0 | 414 | }).then(result => currentState.resolve(result)); |
michael@0 | 415 | }, |
michael@0 | 416 | |
michael@0 | 417 | /** |
michael@0 | 418 | * Set the current user signed in to Firefox Accounts. |
michael@0 | 419 | * |
michael@0 | 420 | * @param credentials |
michael@0 | 421 | * The credentials object obtained by logging in or creating |
michael@0 | 422 | * an account on the FxA server: |
michael@0 | 423 | * { |
michael@0 | 424 | * authAt: The time (seconds since epoch) that this record was |
michael@0 | 425 | * authenticated |
michael@0 | 426 | * email: The users email address |
michael@0 | 427 | * keyFetchToken: a keyFetchToken which has not yet been used |
michael@0 | 428 | * sessionToken: Session for the FxA server |
michael@0 | 429 | * uid: The user's unique id |
michael@0 | 430 | * unwrapBKey: used to unwrap kB, derived locally from the |
michael@0 | 431 | * password (not revealed to the FxA server) |
michael@0 | 432 | * verified: true/false |
michael@0 | 433 | * } |
michael@0 | 434 | * @return Promise |
michael@0 | 435 | * The promise resolves to null when the data is saved |
michael@0 | 436 | * successfully and is rejected on error. |
michael@0 | 437 | */ |
michael@0 | 438 | setSignedInUser: function setSignedInUser(credentials) { |
michael@0 | 439 | log.debug("setSignedInUser - aborting any existing flows"); |
michael@0 | 440 | this.abortExistingFlow(); |
michael@0 | 441 | |
michael@0 | 442 | let record = {version: this.version, accountData: credentials}; |
michael@0 | 443 | let currentState = this.currentAccountState; |
michael@0 | 444 | // Cache a clone of the credentials object. |
michael@0 | 445 | currentState.signedInUser = JSON.parse(JSON.stringify(record)); |
michael@0 | 446 | |
michael@0 | 447 | // This promise waits for storage, but not for verification. |
michael@0 | 448 | // We're telling the caller that this is durable now. |
michael@0 | 449 | return this.signedInUserStorage.set(record).then(() => { |
michael@0 | 450 | this.notifyObservers(ONLOGIN_NOTIFICATION); |
michael@0 | 451 | if (!this.isUserEmailVerified(credentials)) { |
michael@0 | 452 | this.startVerifiedCheck(credentials); |
michael@0 | 453 | } |
michael@0 | 454 | }).then(result => currentState.resolve(result)); |
michael@0 | 455 | }, |
michael@0 | 456 | |
michael@0 | 457 | /** |
michael@0 | 458 | * returns a promise that fires with the assertion. If there is no verified |
michael@0 | 459 | * signed-in user, fires with null. |
michael@0 | 460 | */ |
michael@0 | 461 | getAssertion: function getAssertion(audience) { |
michael@0 | 462 | log.debug("enter getAssertion()"); |
michael@0 | 463 | let currentState = this.currentAccountState; |
michael@0 | 464 | let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; |
michael@0 | 465 | return currentState.getUserAccountData().then(data => { |
michael@0 | 466 | if (!data) { |
michael@0 | 467 | // No signed-in user |
michael@0 | 468 | return null; |
michael@0 | 469 | } |
michael@0 | 470 | if (!this.isUserEmailVerified(data)) { |
michael@0 | 471 | // Signed-in user has not verified email |
michael@0 | 472 | return null; |
michael@0 | 473 | } |
michael@0 | 474 | return currentState.getKeyPair(mustBeValidUntil).then(keyPair => { |
michael@0 | 475 | return currentState.getCertificate(data, keyPair, mustBeValidUntil) |
michael@0 | 476 | .then(cert => { |
michael@0 | 477 | return this.getAssertionFromCert(data, keyPair, cert, audience); |
michael@0 | 478 | }); |
michael@0 | 479 | }); |
michael@0 | 480 | }).then(result => currentState.resolve(result)); |
michael@0 | 481 | }, |
michael@0 | 482 | |
michael@0 | 483 | /** |
michael@0 | 484 | * Resend the verification email fot the currently signed-in user. |
michael@0 | 485 | * |
michael@0 | 486 | */ |
michael@0 | 487 | resendVerificationEmail: function resendVerificationEmail() { |
michael@0 | 488 | let currentState = this.currentAccountState; |
michael@0 | 489 | return this.getSignedInUser().then(data => { |
michael@0 | 490 | // If the caller is asking for verification to be re-sent, and there is |
michael@0 | 491 | // no signed-in user to begin with, this is probably best regarded as an |
michael@0 | 492 | // error. |
michael@0 | 493 | if (data) { |
michael@0 | 494 | this.pollEmailStatus(currentState, data.sessionToken, "start"); |
michael@0 | 495 | return this.fxAccountsClient.resendVerificationEmail(data.sessionToken); |
michael@0 | 496 | } |
michael@0 | 497 | throw new Error("Cannot resend verification email; no signed-in user"); |
michael@0 | 498 | }); |
michael@0 | 499 | }, |
michael@0 | 500 | |
michael@0 | 501 | /* |
michael@0 | 502 | * Reset state such that any previous flow is canceled. |
michael@0 | 503 | */ |
michael@0 | 504 | abortExistingFlow: function abortExistingFlow() { |
michael@0 | 505 | if (this.currentTimer) { |
michael@0 | 506 | log.debug("Polling aborted; Another user signing in"); |
michael@0 | 507 | clearTimeout(this.currentTimer); |
michael@0 | 508 | this.currentTimer = 0; |
michael@0 | 509 | } |
michael@0 | 510 | this.currentAccountState.abort(); |
michael@0 | 511 | this.currentAccountState = new AccountState(this); |
michael@0 | 512 | }, |
michael@0 | 513 | |
michael@0 | 514 | signOut: function signOut(localOnly) { |
michael@0 | 515 | let currentState = this.currentAccountState; |
michael@0 | 516 | let sessionToken; |
michael@0 | 517 | return currentState.getUserAccountData().then(data => { |
michael@0 | 518 | // Save the session token for use in the call to signOut below. |
michael@0 | 519 | sessionToken = data && data.sessionToken; |
michael@0 | 520 | return this._signOutLocal(); |
michael@0 | 521 | }).then(() => { |
michael@0 | 522 | // FxAccountsManager calls here, then does its own call |
michael@0 | 523 | // to FxAccountsClient.signOut(). |
michael@0 | 524 | if (!localOnly) { |
michael@0 | 525 | // Wrap this in a promise so *any* errors in signOut won't |
michael@0 | 526 | // block the local sign out. This is *not* returned. |
michael@0 | 527 | Promise.resolve().then(() => { |
michael@0 | 528 | // This can happen in the background and shouldn't block |
michael@0 | 529 | // the user from signing out. The server must tolerate |
michael@0 | 530 | // clients just disappearing, so this call should be best effort. |
michael@0 | 531 | return this._signOutServer(sessionToken); |
michael@0 | 532 | }).then(null, err => { |
michael@0 | 533 | log.error("Error during remote sign out of Firefox Accounts: " + err); |
michael@0 | 534 | }); |
michael@0 | 535 | } |
michael@0 | 536 | }).then(() => { |
michael@0 | 537 | this.notifyObservers(ONLOGOUT_NOTIFICATION); |
michael@0 | 538 | }); |
michael@0 | 539 | }, |
michael@0 | 540 | |
michael@0 | 541 | /** |
michael@0 | 542 | * This function should be called in conjunction with a server-side |
michael@0 | 543 | * signOut via FxAccountsClient. |
michael@0 | 544 | */ |
michael@0 | 545 | _signOutLocal: function signOutLocal() { |
michael@0 | 546 | this.abortExistingFlow(); |
michael@0 | 547 | this.currentAccountState.signedInUser = null; // clear in-memory cache |
michael@0 | 548 | return this.signedInUserStorage.set(null); |
michael@0 | 549 | }, |
michael@0 | 550 | |
michael@0 | 551 | _signOutServer: function signOutServer(sessionToken) { |
michael@0 | 552 | return this.fxAccountsClient.signOut(sessionToken); |
michael@0 | 553 | }, |
michael@0 | 554 | |
michael@0 | 555 | /** |
michael@0 | 556 | * Fetch encryption keys for the signed-in-user from the FxA API server. |
michael@0 | 557 | * |
michael@0 | 558 | * Not for user consumption. Exists to cause the keys to be fetch. |
michael@0 | 559 | * |
michael@0 | 560 | * Returns user data so that it can be chained with other methods. |
michael@0 | 561 | * |
michael@0 | 562 | * @return Promise |
michael@0 | 563 | * The promise resolves to the credentials object of the signed-in user: |
michael@0 | 564 | * { |
michael@0 | 565 | * email: The user's email address |
michael@0 | 566 | * uid: The user's unique id |
michael@0 | 567 | * sessionToken: Session for the FxA server |
michael@0 | 568 | * kA: An encryption key from the FxA server |
michael@0 | 569 | * kB: An encryption key derived from the user's FxA password |
michael@0 | 570 | * verified: email verification status |
michael@0 | 571 | * } |
michael@0 | 572 | * or null if no user is signed in |
michael@0 | 573 | */ |
michael@0 | 574 | getKeys: function() { |
michael@0 | 575 | let currentState = this.currentAccountState; |
michael@0 | 576 | return currentState.getUserAccountData().then((userData) => { |
michael@0 | 577 | if (!userData) { |
michael@0 | 578 | throw new Error("Can't get keys; User is not signed in"); |
michael@0 | 579 | } |
michael@0 | 580 | if (userData.kA && userData.kB) { |
michael@0 | 581 | return userData; |
michael@0 | 582 | } |
michael@0 | 583 | if (!currentState.whenKeysReadyDeferred) { |
michael@0 | 584 | currentState.whenKeysReadyDeferred = Promise.defer(); |
michael@0 | 585 | if (userData.keyFetchToken) { |
michael@0 | 586 | this.fetchAndUnwrapKeys(userData.keyFetchToken).then( |
michael@0 | 587 | (dataWithKeys) => { |
michael@0 | 588 | if (!dataWithKeys.kA || !dataWithKeys.kB) { |
michael@0 | 589 | currentState.whenKeysReadyDeferred.reject( |
michael@0 | 590 | new Error("user data missing kA or kB") |
michael@0 | 591 | ); |
michael@0 | 592 | return; |
michael@0 | 593 | } |
michael@0 | 594 | currentState.whenKeysReadyDeferred.resolve(dataWithKeys); |
michael@0 | 595 | }, |
michael@0 | 596 | (err) => { |
michael@0 | 597 | currentState.whenKeysReadyDeferred.reject(err); |
michael@0 | 598 | } |
michael@0 | 599 | ); |
michael@0 | 600 | } else { |
michael@0 | 601 | currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); |
michael@0 | 602 | } |
michael@0 | 603 | } |
michael@0 | 604 | return currentState.whenKeysReadyDeferred.promise; |
michael@0 | 605 | }).then(result => currentState.resolve(result)); |
michael@0 | 606 | }, |
michael@0 | 607 | |
michael@0 | 608 | fetchAndUnwrapKeys: function(keyFetchToken) { |
michael@0 | 609 | if (logPII) { |
michael@0 | 610 | log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); |
michael@0 | 611 | } |
michael@0 | 612 | let currentState = this.currentAccountState; |
michael@0 | 613 | return Task.spawn(function* task() { |
michael@0 | 614 | // Sign out if we don't have a key fetch token. |
michael@0 | 615 | if (!keyFetchToken) { |
michael@0 | 616 | log.warn("improper fetchAndUnwrapKeys() call: token missing"); |
michael@0 | 617 | yield this.signOut(); |
michael@0 | 618 | return null; |
michael@0 | 619 | } |
michael@0 | 620 | |
michael@0 | 621 | let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); |
michael@0 | 622 | |
michael@0 | 623 | let data = yield currentState.getUserAccountData(); |
michael@0 | 624 | |
michael@0 | 625 | // Sanity check that the user hasn't changed out from under us |
michael@0 | 626 | if (data.keyFetchToken !== keyFetchToken) { |
michael@0 | 627 | throw new Error("Signed in user changed while fetching keys!"); |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | // Next statements must be synchronous until we setUserAccountData |
michael@0 | 631 | // so that we don't risk getting into a weird state. |
michael@0 | 632 | let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), |
michael@0 | 633 | wrapKB); |
michael@0 | 634 | |
michael@0 | 635 | if (logPII) { |
michael@0 | 636 | log.debug("kB_hex: " + kB_hex); |
michael@0 | 637 | } |
michael@0 | 638 | data.kA = CommonUtils.bytesAsHex(kA); |
michael@0 | 639 | data.kB = CommonUtils.bytesAsHex(kB_hex); |
michael@0 | 640 | |
michael@0 | 641 | delete data.keyFetchToken; |
michael@0 | 642 | |
michael@0 | 643 | log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB); |
michael@0 | 644 | if (logPII) { |
michael@0 | 645 | log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB); |
michael@0 | 646 | } |
michael@0 | 647 | |
michael@0 | 648 | yield currentState.setUserAccountData(data); |
michael@0 | 649 | // We are now ready for business. This should only be invoked once |
michael@0 | 650 | // per setSignedInUser(), regardless of whether we've rebooted since |
michael@0 | 651 | // setSignedInUser() was called. |
michael@0 | 652 | this.notifyObservers(ONVERIFIED_NOTIFICATION); |
michael@0 | 653 | return data; |
michael@0 | 654 | }.bind(this)).then(result => currentState.resolve(result)); |
michael@0 | 655 | }, |
michael@0 | 656 | |
michael@0 | 657 | getAssertionFromCert: function(data, keyPair, cert, audience) { |
michael@0 | 658 | log.debug("getAssertionFromCert"); |
michael@0 | 659 | let payload = {}; |
michael@0 | 660 | let d = Promise.defer(); |
michael@0 | 661 | let options = { |
michael@0 | 662 | duration: ASSERTION_LIFETIME, |
michael@0 | 663 | localtimeOffsetMsec: this.localtimeOffsetMsec, |
michael@0 | 664 | now: this.now() |
michael@0 | 665 | }; |
michael@0 | 666 | let currentState = this.currentAccountState; |
michael@0 | 667 | // "audience" should look like "http://123done.org". |
michael@0 | 668 | // The generated assertion will expire in two minutes. |
michael@0 | 669 | jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { |
michael@0 | 670 | if (err) { |
michael@0 | 671 | log.error("getAssertionFromCert: " + err); |
michael@0 | 672 | d.reject(err); |
michael@0 | 673 | } else { |
michael@0 | 674 | log.debug("getAssertionFromCert returning signed: " + !!signed); |
michael@0 | 675 | if (logPII) { |
michael@0 | 676 | log.debug("getAssertionFromCert returning signed: " + signed); |
michael@0 | 677 | } |
michael@0 | 678 | d.resolve(signed); |
michael@0 | 679 | } |
michael@0 | 680 | }); |
michael@0 | 681 | return d.promise.then(result => currentState.resolve(result)); |
michael@0 | 682 | }, |
michael@0 | 683 | |
michael@0 | 684 | getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { |
michael@0 | 685 | log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); |
michael@0 | 686 | if (logPII) { |
michael@0 | 687 | log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); |
michael@0 | 688 | } |
michael@0 | 689 | return this.fxAccountsClient.signCertificate( |
michael@0 | 690 | sessionToken, |
michael@0 | 691 | JSON.parse(serializedPublicKey), |
michael@0 | 692 | lifetime |
michael@0 | 693 | ); |
michael@0 | 694 | }, |
michael@0 | 695 | |
michael@0 | 696 | getUserAccountData: function() { |
michael@0 | 697 | return this.currentAccountState.getUserAccountData(); |
michael@0 | 698 | }, |
michael@0 | 699 | |
michael@0 | 700 | isUserEmailVerified: function isUserEmailVerified(data) { |
michael@0 | 701 | return !!(data && data.verified); |
michael@0 | 702 | }, |
michael@0 | 703 | |
michael@0 | 704 | /** |
michael@0 | 705 | * Setup for and if necessary do email verification polling. |
michael@0 | 706 | */ |
michael@0 | 707 | loadAndPoll: function() { |
michael@0 | 708 | let currentState = this.currentAccountState; |
michael@0 | 709 | return currentState.getUserAccountData() |
michael@0 | 710 | .then(data => { |
michael@0 | 711 | if (data && !this.isUserEmailVerified(data)) { |
michael@0 | 712 | this.pollEmailStatus(currentState, data.sessionToken, "start"); |
michael@0 | 713 | } |
michael@0 | 714 | return data; |
michael@0 | 715 | }); |
michael@0 | 716 | }, |
michael@0 | 717 | |
michael@0 | 718 | startVerifiedCheck: function(data) { |
michael@0 | 719 | log.debug("startVerifiedCheck " + JSON.stringify(data)); |
michael@0 | 720 | // Get us to the verified state, then get the keys. This returns a promise |
michael@0 | 721 | // that will fire when we are completely ready. |
michael@0 | 722 | // |
michael@0 | 723 | // Login is truly complete once keys have been fetched, so once getKeys() |
michael@0 | 724 | // obtains and stores kA and kB, it will fire the onverified observer |
michael@0 | 725 | // notification. |
michael@0 | 726 | return this.whenVerified(data) |
michael@0 | 727 | .then(() => this.getKeys()); |
michael@0 | 728 | }, |
michael@0 | 729 | |
michael@0 | 730 | whenVerified: function(data) { |
michael@0 | 731 | let currentState = this.currentAccountState; |
michael@0 | 732 | if (data.verified) { |
michael@0 | 733 | log.debug("already verified"); |
michael@0 | 734 | return currentState.resolve(data); |
michael@0 | 735 | } |
michael@0 | 736 | if (!currentState.whenVerifiedDeferred) { |
michael@0 | 737 | log.debug("whenVerified promise starts polling for verified email"); |
michael@0 | 738 | this.pollEmailStatus(currentState, data.sessionToken, "start"); |
michael@0 | 739 | } |
michael@0 | 740 | return currentState.whenVerifiedDeferred.promise.then( |
michael@0 | 741 | result => currentState.resolve(result) |
michael@0 | 742 | ); |
michael@0 | 743 | }, |
michael@0 | 744 | |
michael@0 | 745 | notifyObservers: function(topic) { |
michael@0 | 746 | log.debug("Notifying observers of " + topic); |
michael@0 | 747 | Services.obs.notifyObservers(null, topic, null); |
michael@0 | 748 | }, |
michael@0 | 749 | |
michael@0 | 750 | // XXX - pollEmailStatus should maybe be on the AccountState object? |
michael@0 | 751 | pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { |
michael@0 | 752 | log.debug("entering pollEmailStatus: " + why); |
michael@0 | 753 | if (why == "start") { |
michael@0 | 754 | // If we were already polling, stop and start again. This could happen |
michael@0 | 755 | // if the user requested the verification email to be resent while we |
michael@0 | 756 | // were already polling for receipt of an earlier email. |
michael@0 | 757 | this.pollTimeRemaining = this.POLL_SESSION; |
michael@0 | 758 | if (!currentState.whenVerifiedDeferred) { |
michael@0 | 759 | currentState.whenVerifiedDeferred = Promise.defer(); |
michael@0 | 760 | // This deferred might not end up with any handlers (eg, if sync |
michael@0 | 761 | // is yet to start up.) This might cause "A promise chain failed to |
michael@0 | 762 | // handle a rejection" messages, so add an error handler directly |
michael@0 | 763 | // on the promise to log the error. |
michael@0 | 764 | currentState.whenVerifiedDeferred.promise.then(null, err => { |
michael@0 | 765 | log.info("the wait for user verification was stopped: " + err); |
michael@0 | 766 | }); |
michael@0 | 767 | } |
michael@0 | 768 | } |
michael@0 | 769 | |
michael@0 | 770 | this.checkEmailStatus(sessionToken) |
michael@0 | 771 | .then((response) => { |
michael@0 | 772 | log.debug("checkEmailStatus -> " + JSON.stringify(response)); |
michael@0 | 773 | if (response && response.verified) { |
michael@0 | 774 | // Bug 947056 - Server should be able to tell FxAccounts.jsm to back |
michael@0 | 775 | // off or stop polling altogether |
michael@0 | 776 | currentState.getUserAccountData() |
michael@0 | 777 | .then((data) => { |
michael@0 | 778 | data.verified = true; |
michael@0 | 779 | return currentState.setUserAccountData(data); |
michael@0 | 780 | }) |
michael@0 | 781 | .then((data) => { |
michael@0 | 782 | // Now that the user is verified, we can proceed to fetch keys |
michael@0 | 783 | if (currentState.whenVerifiedDeferred) { |
michael@0 | 784 | currentState.whenVerifiedDeferred.resolve(data); |
michael@0 | 785 | delete currentState.whenVerifiedDeferred; |
michael@0 | 786 | } |
michael@0 | 787 | }); |
michael@0 | 788 | } else { |
michael@0 | 789 | log.debug("polling with step = " + this.POLL_STEP); |
michael@0 | 790 | this.pollTimeRemaining -= this.POLL_STEP; |
michael@0 | 791 | log.debug("time remaining: " + this.pollTimeRemaining); |
michael@0 | 792 | if (this.pollTimeRemaining > 0) { |
michael@0 | 793 | this.currentTimer = setTimeout(() => { |
michael@0 | 794 | this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP); |
michael@0 | 795 | log.debug("started timer " + this.currentTimer); |
michael@0 | 796 | } else { |
michael@0 | 797 | if (currentState.whenVerifiedDeferred) { |
michael@0 | 798 | currentState.whenVerifiedDeferred.reject( |
michael@0 | 799 | new Error("User email verification timed out.") |
michael@0 | 800 | ); |
michael@0 | 801 | delete currentState.whenVerifiedDeferred; |
michael@0 | 802 | } |
michael@0 | 803 | } |
michael@0 | 804 | } |
michael@0 | 805 | }); |
michael@0 | 806 | }, |
michael@0 | 807 | |
michael@0 | 808 | // Return the URI of the remote UI flows. |
michael@0 | 809 | getAccountsSignUpURI: function() { |
michael@0 | 810 | let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri"); |
michael@0 | 811 | if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
michael@0 | 812 | throw new Error("Firefox Accounts server must use HTTPS"); |
michael@0 | 813 | } |
michael@0 | 814 | return url; |
michael@0 | 815 | }, |
michael@0 | 816 | |
michael@0 | 817 | // Return the URI of the remote UI flows. |
michael@0 | 818 | getAccountsSignInURI: function() { |
michael@0 | 819 | let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri"); |
michael@0 | 820 | if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
michael@0 | 821 | throw new Error("Firefox Accounts server must use HTTPS"); |
michael@0 | 822 | } |
michael@0 | 823 | return url; |
michael@0 | 824 | }, |
michael@0 | 825 | |
michael@0 | 826 | // Returns a promise that resolves with the URL to use to force a re-signin |
michael@0 | 827 | // of the current account. |
michael@0 | 828 | promiseAccountsForceSigninURI: function() { |
michael@0 | 829 | let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); |
michael@0 | 830 | if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
michael@0 | 831 | throw new Error("Firefox Accounts server must use HTTPS"); |
michael@0 | 832 | } |
michael@0 | 833 | let currentState = this.currentAccountState; |
michael@0 | 834 | // but we need to append the email address onto a query string. |
michael@0 | 835 | return this.getSignedInUser().then(accountData => { |
michael@0 | 836 | if (!accountData) { |
michael@0 | 837 | return null; |
michael@0 | 838 | } |
michael@0 | 839 | let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; |
michael@0 | 840 | newQueryPortion += "email=" + encodeURIComponent(accountData.email); |
michael@0 | 841 | return url + newQueryPortion; |
michael@0 | 842 | }).then(result => currentState.resolve(result)); |
michael@0 | 843 | } |
michael@0 | 844 | }; |
michael@0 | 845 | |
michael@0 | 846 | /** |
michael@0 | 847 | * JSONStorage constructor that creates instances that may set/get |
michael@0 | 848 | * to a specified file, in a directory that will be created if it |
michael@0 | 849 | * doesn't exist. |
michael@0 | 850 | * |
michael@0 | 851 | * @param options { |
michael@0 | 852 | * filename: of the file to write to |
michael@0 | 853 | * baseDir: directory where the file resides |
michael@0 | 854 | * } |
michael@0 | 855 | * @return instance |
michael@0 | 856 | */ |
michael@0 | 857 | function JSONStorage(options) { |
michael@0 | 858 | this.baseDir = options.baseDir; |
michael@0 | 859 | this.path = OS.Path.join(options.baseDir, options.filename); |
michael@0 | 860 | }; |
michael@0 | 861 | |
michael@0 | 862 | JSONStorage.prototype = { |
michael@0 | 863 | set: function(contents) { |
michael@0 | 864 | return OS.File.makeDir(this.baseDir, {ignoreExisting: true}) |
michael@0 | 865 | .then(CommonUtils.writeJSON.bind(null, contents, this.path)); |
michael@0 | 866 | }, |
michael@0 | 867 | |
michael@0 | 868 | get: function() { |
michael@0 | 869 | return CommonUtils.readJSON(this.path); |
michael@0 | 870 | } |
michael@0 | 871 | }; |
michael@0 | 872 | |
michael@0 | 873 | // A getter for the instance to export |
michael@0 | 874 | XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { |
michael@0 | 875 | let a = new FxAccounts(); |
michael@0 | 876 | |
michael@0 | 877 | // XXX Bug 947061 - We need a strategy for resuming email verification after |
michael@0 | 878 | // browser restart |
michael@0 | 879 | a.loadAndPoll(); |
michael@0 | 880 | |
michael@0 | 881 | return a; |
michael@0 | 882 | }); |