diff options
439 files changed, 16 insertions, 53530 deletions
diff --git a/application/basilisk/app/profile/basilisk.js b/application/basilisk/app/profile/basilisk.js index 4700eac44..fc3b4546b 100644 --- a/application/basilisk/app/profile/basilisk.js +++ b/application/basilisk/app/profile/basilisk.js @@ -1171,53 +1171,11 @@ pref("browser.uiCustomization.debug", false); // CustomizableUI state of the browser's user interface pref("browser.uiCustomization.state", ""); -// The remote content URL shown for FxA signup. Must use HTTPS. -pref("identity.fxaccounts.remote.signup.uri", "https://accounts.firefox.com/signup?service=sync&context=fx_desktop_v3"); - -// The URL where remote content that forces re-authentication for Firefox Accounts -// should be fetched. Must use HTTPS. -pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v3"); - -// The remote content URL shown for signin in. Must use HTTPS. -pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v3"); - -// The remote content URL where FxAccountsWebChannel messages originate. -pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com/"); - -// The value of the context query parameter passed in some fxa requests when config -// discovery is enabled. -pref("identity.fxaccounts.contextParam", "fx_desktop_v3"); - -// The URL we take the user to when they opt to "manage" their Firefox Account. -// Note that this will always need to be in the same TLD as the -// "identity.fxaccounts.remote.signup.uri" pref. -pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings?service=sync&context=fx_desktop_v3"); - -// The remote URL of the FxA Profile Server -pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1"); - -// The remote URL of the FxA OAuth Server -pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1"); - -// Whether we display profile images in the UI or not. -pref("identity.fxaccounts.profile_image.enabled", true); - -// Token server used by the FxA Sync identity. -pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5"); - // URLs for promo links to mobile browsers. Note that consumers are expected to // append a value for utm_campaign. pref("identity.mobilepromo.android", "https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign="); pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign="); -// Migrate any existing Firefox Account data from the default profile to the -// Developer Edition profile. -#ifdef MOZ_DEV_EDITION -pref("identity.fxaccounts.migrateToDevEdition", true); -#else -pref("identity.fxaccounts.migrateToDevEdition", false); -#endif - // On GTK, we now default to showing the menubar only when alt is pressed: #ifdef MOZ_WIDGET_GTK pref("ui.key.menuAccessKeyFocuses", true); diff --git a/application/basilisk/base/content/global-scripts.inc b/application/basilisk/base/content/global-scripts.inc index 6417a1d95..db8496cfc 100644 --- a/application/basilisk/base/content/global-scripts.inc +++ b/application/basilisk/base/content/global-scripts.inc @@ -35,4 +35,3 @@ <script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/> #endif -<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/> diff --git a/application/basilisk/base/content/web-panels.xul b/application/basilisk/base/content/web-panels.xul index ed868c24a..78f8954c1 100644 --- a/application/basilisk/base/content/web-panels.xul +++ b/application/basilisk/base/content/web-panels.xul @@ -23,7 +23,6 @@ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> <script type="application/javascript" src="chrome://browser/content/browser.js"/> <script type="application/javascript" src="chrome://browser/content/browser-places.js"/> - <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/> <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> <script type="application/javascript" src="chrome://browser/content/web-panels.js"/> diff --git a/application/basilisk/installer/allowed-dupes.mn b/application/basilisk/installer/allowed-dupes.mn index 7baa6ebed..a3780bf5a 100644 --- a/application/basilisk/installer/allowed-dupes.mn +++ b/application/basilisk/installer/allowed-dupes.mn @@ -211,13 +211,11 @@ chrome/toolkit/skin/classic/mozapps/update/buttons.png chrome/toolkit/skin/classic/mozapps/update/downloadButtons.png chrome/toolkit/skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png -components/FxAccountsPush.js crashreporter.app/Contents/Resources/English.lproj/MainMenu.nib/classes.nib crashreporter.app/Contents/Resources/English.lproj/MainMenuRTL.nib/classes.nib # firefox/firefox-bin is bug 658850 @MOZ_APP_NAME@ @MOZ_APP_NAME@-bin -modules/FxAccountsPush.js modules/commonjs/index.js modules/commonjs/sdk/ui/button/view/events.js modules/commonjs/sdk/ui/state/events.js diff --git a/application/basilisk/installer/package-manifest.in b/application/basilisk/installer/package-manifest.in index 4ea50408a..22655bc33 100644 --- a/application/basilisk/installer/package-manifest.in +++ b/application/basilisk/installer/package-manifest.in @@ -479,8 +479,6 @@ #endif @RESPATH@/components/SyncComponents.manifest @RESPATH@/components/Weave.js -@RESPATH@/components/FxAccountsComponents.manifest -@RESPATH@/components/FxAccountsPush.js @RESPATH@/components/CaptivePortalDetectComponents.manifest @RESPATH@/components/captivedetect.js @RESPATH@/components/servicesComponents.manifest diff --git a/application/basilisk/modules/AboutHome.jsm b/application/basilisk/modules/AboutHome.jsm index 639194c20..671448480 100644 --- a/application/basilisk/modules/AboutHome.jsm +++ b/application/basilisk/modules/AboutHome.jsm @@ -17,8 +17,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate", "resource:///modules/AutoMigrate.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index ef4764d88..abd30a8df 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -901,18 +901,6 @@ pref("dom.push.maxRecentMessageIDsPerSubscription", 0); pref("dom.push.enabled", false); #endif -// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS. -pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com"); - -// The remote URL of the Firefox Account profile server. -pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1"); - -// The remote URL of the Firefox Account oauth server. -pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1"); - -// Token server used by Firefox Account-authenticated Sync. -pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5"); - // Enable Presentation API pref("dom.presentation.enabled", false); pref("dom.presentation.discovery.enabled", true); diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index 118a0c44c..ca266542a 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -780,21 +780,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil 'background/common/telemetry/TelemetryWrapper.java', 'background/db/CursorDumper.java', 'background/db/Tab.java', - 'background/fxa/FxAccount20CreateDelegate.java', - 'background/fxa/FxAccount20LoginDelegate.java', - 'background/fxa/FxAccountClient.java', - 'background/fxa/FxAccountClient20.java', - 'background/fxa/FxAccountClientException.java', - 'background/fxa/FxAccountRemoteError.java', - 'background/fxa/FxAccountUtils.java', - 'background/fxa/oauth/FxAccountAbstractClient.java', - 'background/fxa/oauth/FxAccountAbstractClientException.java', - 'background/fxa/oauth/FxAccountOAuthClient10.java', - 'background/fxa/oauth/FxAccountOAuthRemoteError.java', - 'background/fxa/PasswordStretcher.java', - 'background/fxa/profile/FxAccountProfileClient10.java', - 'background/fxa/QuickPasswordStretcher.java', - 'background/fxa/SkewHandler.java', 'background/nativecode/NativeCrypto.java', 'background/preferences/PreferenceFragment.java', 'background/preferences/PreferenceManagerCompat.java', @@ -813,52 +798,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil 'browserid/verifier/BrowserIDVerifierDelegate.java', 'browserid/verifier/BrowserIDVerifierException.java', 'browserid/VerifyingPublicKey.java', - 'fxa/AccountLoader.java', - 'fxa/activities/CustomColorPreference.java', - 'fxa/activities/FxAccountAbstractActivity.java', - 'fxa/activities/FxAccountConfirmAccountActivityWeb.java', - 'fxa/activities/FxAccountFinishMigratingActivityWeb.java', - 'fxa/activities/FxAccountGetStartedActivityWeb.java', - 'fxa/activities/FxAccountStatusActivity.java', - 'fxa/activities/FxAccountStatusFragment.java', - 'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java', - 'fxa/activities/FxAccountWebFlowActivity.java', - 'fxa/activities/PicassoPreferenceIconTarget.java', - 'fxa/authenticator/AccountPickler.java', - 'fxa/authenticator/AndroidFxAccount.java', - 'fxa/authenticator/FxAccountAuthenticator.java', - 'fxa/authenticator/FxAccountAuthenticatorService.java', - 'fxa/authenticator/FxAccountLoginDelegate.java', - 'fxa/authenticator/FxAccountLoginException.java', - 'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java', - 'fxa/FirefoxAccounts.java', - 'fxa/FxAccountConstants.java', - 'fxa/FxAccountDevice.java', - 'fxa/FxAccountDeviceRegistrator.java', - 'fxa/FxAccountPushHandler.java', - 'fxa/login/BaseRequestDelegate.java', - 'fxa/login/Cohabiting.java', - 'fxa/login/Doghouse.java', - 'fxa/login/Engaged.java', - 'fxa/login/FxAccountLoginStateMachine.java', - 'fxa/login/FxAccountLoginTransition.java', - 'fxa/login/Married.java', - 'fxa/login/MigratedFromSync11.java', - 'fxa/login/Separated.java', - 'fxa/login/State.java', - 'fxa/login/StateFactory.java', - 'fxa/login/TokensAndKeysState.java', - 'fxa/receivers/FxAccountDeletedService.java', - 'fxa/receivers/FxAccountUpgradeReceiver.java', - 'fxa/sync/FxAccountNotificationManager.java', - 'fxa/sync/FxAccountProfileService.java', - 'fxa/sync/FxAccountSchedulePolicy.java', - 'fxa/sync/FxAccountSyncAdapter.java', - 'fxa/sync/FxAccountSyncDelegate.java', - 'fxa/sync/FxAccountSyncService.java', - 'fxa/sync/FxAccountSyncStatusHelper.java', - 'fxa/sync/SchedulePolicy.java', - 'fxa/SyncStatusListener.java', 'push/autopush/AutopushClient.java', 'push/autopush/AutopushClientException.java', 'push/RegisterUserAgentResponse.java', diff --git a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java index 8d3a92e48..7c3a9434f 100644 --- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java +++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java @@ -22,7 +22,6 @@ import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.annotation.ReflectionTarget; import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.fxa.FxAccountPushHandler; import org.mozilla.gecko.gcm.GcmTokenClient; import org.mozilla.gecko.push.autopush.AutopushClientException; import org.mozilla.gecko.util.BundleEventListener; @@ -66,13 +65,10 @@ public class PushService implements BundleEventListener { "PushServiceAndroidGCM:UnregisterUserAgent", "PushServiceAndroidGCM:SubscribeChannel", "PushServiceAndroidGCM:UnsubscribeChannel", - "FxAccountsPush:Initialized", - "FxAccountsPush:ReceivedPushMessageToDecode:Response", "History:GetPrePathLastVisitedTimeMilliseconds", }; private enum GeckoComponent { - FxAccountsPush, PushServiceAndroidGCM } @@ -98,7 +94,6 @@ public class PushService implements BundleEventListener { // NB: These are not thread-safe, we're depending on these being access from the same background thread. private boolean isReadyPushServiceAndroidGCM = false; - private boolean isReadyFxAccountsPush = false; private final List<JSONObject> pendingPushMessages; public PushService(Context context) { @@ -238,9 +233,6 @@ public class PushService implements BundleEventListener { protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) { Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!"); - GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode", - message.toString(), - GeckoThread.State.PROFILE_READY); } protected void registerGeckoEventListener() { @@ -279,11 +271,6 @@ public class PushService implements BundleEventListener { callback.sendSuccess(null); return; } - if ("FxAccountsPush:Initialized".equals(event)) { - processComponentState(GeckoComponent.FxAccountsPush, true); - callback.sendSuccess(null); - return; - } if ("PushServiceAndroidGCM:Configure".equals(event)) { final String endpoint = message.getString("endpoint"); if (endpoint == null) { @@ -390,10 +377,6 @@ public class PushService implements BundleEventListener { callback.sendError("Could not unsubscribe from channel: " + channelID); return; } - if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) { - FxAccountPushHandler.handleFxAPushMessage(context, message); - return; - } if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) { if (callback == null) { Log.e(LOG_TAG, "callback must not be null in " + event); @@ -420,10 +403,7 @@ public class PushService implements BundleEventListener { } private void processComponentState(@NonNull GeckoComponent component, boolean isReady) { - if (component == GeckoComponent.FxAccountsPush) { - isReadyFxAccountsPush = isReady; - - } else if (component == GeckoComponent.PushServiceAndroidGCM) { + if (component == GeckoComponent.PushServiceAndroidGCM) { isReadyPushServiceAndroidGCM = isReady; } @@ -435,7 +415,7 @@ public class PushService implements BundleEventListener { } private boolean canSendPushMessagesToGecko() { - return isReadyFxAccountsPush && isReadyPushServiceAndroidGCM; + return isReadyPushServiceAndroidGCM; } private static void sendPushMessagesToGecko(@NonNull List<JSONObject> messages) { diff --git a/mobile/android/base/resources/values-v21/themes.xml b/mobile/android/base/resources/values-v21/themes.xml index ddb08d052..140f066c4 100644 --- a/mobile/android/base/resources/values-v21/themes.xml +++ b/mobile/android/base/resources/values-v21/themes.xml @@ -21,11 +21,6 @@ <item name="android:colorAccent">@color/fennec_ui_orange</item> </style> - <style name="ActionBar.FxAccountStatusActivity" parent="@android:style/Widget.Material.ActionBar.Solid"> - <item name="android:displayOptions">homeAsUp|showTitle</item> - <item name="android:titleTextStyle">@style/ActionBarTitleTextStyle</item> - </style> - <style name="GeckoAppBase" parent="Gecko"> <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item> <item name="android:listViewStyle">@style/Widget.ListView</item> diff --git a/mobile/android/chrome/content/aboutAccounts.js b/mobile/android/chrome/content/aboutAccounts.js deleted file mode 100644 index 4801a76a1..000000000 --- a/mobile/android/chrome/content/aboutAccounts.js +++ /dev/null @@ -1,351 +0,0 @@ -// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Wrap a remote fxa-content-server. - * - * An about:accounts tab loads and displays an fxa-content-server page, - * depending on the current Android Account status and an optional 'action' - * parameter. - * - * We show a spinner while the remote iframe is loading. We expect the - * WebChannel message listening to the fxa-content-server to send this tab's - * <browser>'s messageManager a LOADED message when the remote iframe provides - * the WebChannel LOADED message. See the messageManager registration and the - * |loadedDeferred| promise. This loosely couples the WebChannel implementation - * and about:accounts! (We need this coupling in order to distinguish - * WebChannel LOADED messages produced by multiple about:accounts tabs.) - * - * We capture error conditions by accessing the inner nsIWebNavigation of the - * iframe directly. - */ - -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */ - -Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */ -Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */ -Cu.import("resource://gre/modules/Services.jsm"); /*global Services */ -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */ - -const ACTION_URL_PARAM = "action"; - -const COMMAND_LOADED = "fxaccounts:loaded"; - -const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts"); - -XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", - "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService"); - -// Shows the toplevel element with |id| to be shown - all other top-level -// elements are hidden. -// If |id| is 'spinner', then 'remote' is also shown, with opacity 0. -function show(id) { - let allTop = document.querySelectorAll(".toplevel"); - for (let elt of allTop) { - if (elt.getAttribute("id") == id) { - elt.style.display = 'block'; - } else { - elt.style.display = 'none'; - } - } - if (id == 'spinner') { - document.getElementById('remote').style.display = 'block'; - document.getElementById('remote').style.opacity = 0; - } -} - -// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It -// is resolved by a LOADED message, and rejected by a failure to load. -var loadedDeferred = null; - -// We have a new load starting. Replace the existing promise with a new one, -// and queue up the transition to remote content. -function deferTransitionToRemoteAfterLoaded() { - log.d('Waiting for LOADED message.'); - - loadedDeferred = PromiseUtils.defer(); - loadedDeferred.promise.then(() => { - log.d('Got LOADED message!'); - document.getElementById("remote").style.opacity = 0; - show("remote"); - document.getElementById("remote").style.opacity = 1; - }) - .catch((e) => { - log.w('Did not get LOADED message: ' + e.toString()); - }); -} - -function handleLoadedMessage(message) { - loadedDeferred.resolve(); -}; - -var wrapper = { - iframe: null, - - url: null, - - init: function (url) { - this.url = url; - deferTransitionToRemoteAfterLoaded(); - - let iframe = document.getElementById("remote"); - this.iframe = iframe; - this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner); - let docShell = this.iframe.frameLoader.docShell; - docShell.QueryInterface(Ci.nsIWebProgress); - docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); - - // Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to - // avoid having a new history entry being added. - let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation); - webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null); - }, - - retry: function () { - deferTransitionToRemoteAfterLoaded(); - - let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation); - webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null); - }, - - iframeListener: { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference, - Ci.nsISupports]), - - onStateChange: function(aWebProgress, aRequest, aState, aStatus) { - let failure = false; - - // Captive portals sometimes redirect users - if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) { - failure = true; - } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) { - if (aRequest instanceof Ci.nsIHttpChannel) { - try { - failure = aRequest.responseStatus != 200; - } catch (e) { - failure = aStatus != Components.results.NS_OK; - } - } - } - - // Calling cancel() will raise some OnStateChange notifications by itself, - // so avoid doing that more than once - if (failure && aStatus != Components.results.NS_BINDING_ABORTED) { - aRequest.cancel(Components.results.NS_BINDING_ABORTED); - // Since after a promise is fulfilled, subsequent fulfillments are - // treated as no-ops, we don't care that we might see multiple failures - // due to multiple listener callbacks. (It's not easy to extract this - // from the Promises spec, but it is widely quoted. Start with - // http://stackoverflow.com/a/18218542.) - loadedDeferred.reject(new Error("Failed in onStateChange!")); - show("networkError"); - } - }, - - onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { - if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { - aRequest.cancel(Components.results.NS_BINDING_ABORTED); - // As above, we're not concerned by multiple listener callbacks. - loadedDeferred.reject(new Error("Failed in onLocationChange!")); - show("networkError"); - } - }, - - onProgressChange: function() {}, - onStatusChange: function() {}, - onSecurityChange: function() {}, - }, -}; - - -function retry() { - log.i("Retrying."); - show("spinner"); - wrapper.retry(); -} - -function openPrefs() { - log.i("Opening Sync preferences."); - // If an Android Account exists, this will open the Status Activity. - // Otherwise, it will begin the Get Started flow. This should only be shown - // when an Account actually exists. - Accounts.launchSetup(); -} - -function getURLForAction(action, urlParams) { - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri"); - url = url + (url.endsWith("/") ? "" : "/") + action; - const CONTEXT = "fx_fennec_v1"; - // The only service managed by Fennec, to date, is Firefox Sync. - const SERVICE = "sync"; - urlParams = urlParams || new URLSearchParams(""); - urlParams.set('service', SERVICE); - urlParams.set('context', CONTEXT); - // Ideally we'd just merge urlParams with new URL(url).searchParams, but our - // URLSearchParams implementation doesn't support iteration (bug 1085284). - let urlParamStr = urlParams.toString(); - if (urlParamStr) { - url += (url.includes("?") ? "&" : "?") + urlParamStr; - } - return url; -} - -function updateDisplayedEmail(user) { - let emailDiv = document.getElementById("email"); - if (emailDiv && user) { - emailDiv.textContent = user.email; - } -} - -function init() { - // Test for restrictions before getFirefoxAccount(), since that will fail if - // we are restricted. - if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) { - // It's better to log and show an error message than to invite user - // confusion by removing about:accounts entirely. That is, if the user is - // restricted, this way they'll discover as much and may be able to get - // out of their restricted profile. If we remove about:accounts entirely, - // it will look like Fennec is buggy, and the user will be very confused. - log.e("This profile cannot connect to Firefox Accounts: showing restricted error."); - show("restrictedError"); - return; - } - - Accounts.getFirefoxAccount().then(user => { - // It's possible for the window to start closing before getting the user - // completes. Tests in particular can cause this. - if (window.closed) { - return; - } - - updateDisplayedEmail(user); - - // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs, - // searchParams is empty. - let urlParams = new URLSearchParams(document.URL.split("?")[1] || ""); - let action = urlParams.get(ACTION_URL_PARAM); - urlParams.delete(ACTION_URL_PARAM); - - switch (action) { - case "signup": - if (user) { - // Asking to sign-up when already signed in just shows prefs. - show("prefs"); - } else { - show("spinner"); - wrapper.init(getURLForAction("signup", urlParams)); - } - break; - case "signin": - if (user) { - // Asking to sign-in when already signed in just shows prefs. - show("prefs"); - } else { - show("spinner"); - wrapper.init(getURLForAction("signin", urlParams)); - } - break; - case "force_auth": - if (user) { - show("spinner"); - urlParams.set("email", user.email); // In future, pin using the UID. - wrapper.init(getURLForAction("force_auth", urlParams)); - } else { - show("spinner"); - wrapper.init(getURLForAction("signup", urlParams)); - } - break; - case "manage": - if (user) { - show("spinner"); - urlParams.set("email", user.email); // In future, pin using the UID. - wrapper.init(getURLForAction("settings", urlParams)); - } else { - show("spinner"); - wrapper.init(getURLForAction("signup", urlParams)); - } - break; - case "avatar": - if (user) { - show("spinner"); - urlParams.set("email", user.email); // In future, pin using the UID. - wrapper.init(getURLForAction("settings/avatar/change", urlParams)); - } else { - show("spinner"); - wrapper.init(getURLForAction("signup", urlParams)); - } - break; - default: - // Unrecognized or no action specified. - if (action) { - log.w("Ignoring unrecognized action: " + action); - } - if (user) { - show("prefs"); - } else { - show("spinner"); - wrapper.init(getURLForAction("signup", urlParams)); - } - break; - } - }).catch(e => { - log.e("Failed to get the signed in user: " + e.toString()); - }); -} - -document.addEventListener("DOMContentLoaded", function onload() { - document.removeEventListener("DOMContentLoaded", onload, true); - init(); - var buttonRetry = document.getElementById('buttonRetry'); - buttonRetry.addEventListener('click', retry); - - var buttonOpenPrefs = document.getElementById('buttonOpenPrefs'); - buttonOpenPrefs.addEventListener('click', openPrefs); -}, true); - -// This window is contained in a XUL <browser> element. Return the -// messageManager of that <browser> element, or null. -function getBrowserMessageManager() { - let browser = window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) - .QueryInterface(Ci.nsIDOMChromeWindow) - .BrowserApp - .getBrowserForDocument(document); - if (browser) { - return browser.messageManager; - } - return null; -} - -// Add a single listener for 'loaded' messages from the iframe in this -// <browser>. These 'loaded' messages are ferried from the WebChannel to just -// this <browser>. -var mm = getBrowserMessageManager(); -if (mm) { - mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage); -} else { - log.e('No messageManager, not listening for LOADED message!'); -} - -window.addEventListener("unload", function(event) { - try { - let mm = getBrowserMessageManager(); - if (mm) { - mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage); - } - } catch (e) { - // This could fail if the page is being torn down, the tab is being - // destroyed, etc. - log.w('Not removing listener for LOADED message: ' + e.toString()); - } -}); diff --git a/mobile/android/chrome/content/aboutAccounts.xhtml b/mobile/android/chrome/content/aboutAccounts.xhtml deleted file mode 100644 index b988741d5..000000000 --- a/mobile/android/chrome/content/aboutAccounts.xhtml +++ /dev/null @@ -1,83 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" - "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > -%brandDTD; -<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > -%globalDTD; -<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd"> -%aboutDTD; -]> - -<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;"> - <head> - <title>Firefox Sync</title> - <meta name="viewport" content="width=device-width; user-scalable=0" /> - <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" /> - <link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/> - <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/> - <link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/> - </head> - <body> - <div id="spinner" class="toplevel"> - <div class="container flex-column"> - <!-- Empty text-container for spacing. --> - <div class="text-container flex-column" /> - - <div class="mui-refresh-main"> - <div class="mui-refresh-wrapper"> - <div class="mui-spinner-wrapper"> - <div class="mui-spinner-main"> - <div class="mui-spinner-left"> - <div class="mui-half-circle-left" /> - </div> - <div class="mui-spinner-right"> - <div class="mui-half-circle-right" /> - </div> - </div> - </div> - </div> - </div> - - </div> - </div> - - <iframe mozframetype="content" id="remote" class="toplevel" /> - - <div id="prefs" class="toplevel"> - <div class="container flex-column"> - <div class="text-container flex-column"> - <div class="text">&aboutAccounts.connected.title;</div> - <div class="hint">&aboutAccounts.connected.description;</div> - <div id="email" class="hint"></div> - </div> - <a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a> - </div> - </div> - - <div id="networkError" class="toplevel"> - <div class="container flex-column"> - <div class="text-container flex-column"> - <div class="text">&aboutAccounts.noConnection.title;</div> - </div> - <div class="button-row"> - <button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button> - </div> - </div> - </div> - - <div id="restrictedError" class="toplevel"> - <div class="container flex-column"> - <div class="text-container flex-column"> - <div class="text">&aboutAccounts.restrictedError.title;</div> - <div class="hint">&aboutAccounts.restrictedError.description;</div> - </div> - </div> - </div> - - <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script> - </body> -</html> diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index e2706f4b2..93eb2addc 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -489,20 +489,6 @@ var BrowserApp = { let mm = window.getGroupMessageManager("browsers"); mm.loadFrameScript("chrome://browser/content/content.js", true); - // We can't delay registering WebChannel listeners: if the first page is - // about:accounts, which can happen when starting the Firefox Account flow - // from the first run experience, or via the Firefox Account Status - // Activity, we can and do miss messages from the fxa-content-server. - // However, we never allow suitably restricted profiles from listening to - // fxa-content-server messages. - if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) { - console.log("browser.js: loading Firefox Accounts WebChannel"); - Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm"); - EnsureFxAccountsWebChannel(); - } else { - console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts."); - } - // Notify Java that Gecko has loaded. Messaging.sendRequest({ type: "Gecko:Ready" }); diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn index 538a025bd..183defe38 100644 --- a/mobile/android/chrome/jar.mn +++ b/mobile/android/chrome/jar.mn @@ -56,8 +56,6 @@ chrome.jar: content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml) content/aboutHealthReport.js (content/aboutHealthReport.js) #endif - content/aboutAccounts.xhtml (content/aboutAccounts.xhtml) - content/aboutAccounts.js (content/aboutAccounts.js) content/aboutLogins.xhtml (content/aboutLogins.xhtml) content/aboutLogins.js (content/aboutLogins.js) #ifndef RELEASE_OR_BETA diff --git a/mobile/android/components/FxAccountsPush.js b/mobile/android/components/FxAccountsPush.js deleted file mode 100644 index e6054a2de..000000000 --- a/mobile/android/components/FxAccountsPush.js +++ /dev/null @@ -1,164 +0,0 @@ -/* jshint moz: true, esnext: true */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Messaging.jsm"); -const { - PushCrypto, - getCryptoParams, -} = Cu.import("resource://gre/modules/PushCrypto.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "PushService", - "@mozilla.org/push/Service;1", "nsIPushService"); -XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder()); - -const FXA_PUSH_SCOPE = "chrome://fxa-push"; -const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush"); - -function FxAccountsPush() { - Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false); - - Messaging.sendRequestForResult({ - type: "FxAccountsPush:Initialized" - }); -} - -FxAccountsPush.prototype = { - observe: function (subject, topic, data) { - switch (topic) { - case "android-push-service": - if (data === "android-fxa-subscribe") { - this._subscribe(); - } else if (data === "android-fxa-unsubscribe") { - this._unsubscribe(); - } - break; - case "FxAccountsPush:ReceivedPushMessageToDecode": - this._decodePushMessage(data); - break; - } - }, - - _subscribe() { - Log.i("FxAccountsPush _subscribe"); - return new Promise((resolve, reject) => { - PushService.subscribe(FXA_PUSH_SCOPE, - Services.scriptSecurityManager.getSystemPrincipal(), - (result, subscription) => { - if (Components.isSuccessCode(result)) { - Log.d("FxAccountsPush got subscription"); - resolve(subscription); - } else { - Log.w("FxAccountsPush failed to subscribe", result); - reject(new Error("FxAccountsPush failed to subscribe")); - } - }); - }) - .then(subscription => { - Messaging.sendRequest({ - type: "FxAccountsPush:Subscribe:Response", - subscription: { - pushCallback: subscription.endpoint, - pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')), - pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth')) - } - }); - }) - .catch(err => { - Log.i("Error when registering FxA push endpoint " + err); - }); - }, - - _unsubscribe() { - Log.i("FxAccountsPush _unsubscribe"); - return new Promise((resolve) => { - PushService.unsubscribe(FXA_PUSH_SCOPE, - Services.scriptSecurityManager.getSystemPrincipal(), - (result, ok) => { - if (Components.isSuccessCode(result)) { - if (ok === true) { - Log.d("FxAccountsPush unsubscribed"); - } else { - Log.d("FxAccountsPush had no subscription to unsubscribe"); - } - } else { - Log.w("FxAccountsPush failed to unsubscribe", result); - } - return resolve(ok); - }); - }).catch(err => { - Log.e("Error during unsubscribe", err); - }); - }, - - _decodePushMessage(data) { - Log.i("FxAccountsPush _decodePushMessage"); - data = JSON.parse(data); - let { headers, message } = this._messageAndHeaders(data); - return new Promise((resolve, reject) => { - PushService.getSubscription(FXA_PUSH_SCOPE, - Services.scriptSecurityManager.getSystemPrincipal(), - (result, subscription) => { - if (!subscription) { - return reject(new Error("No subscription found")); - } - return resolve(subscription); - }); - }).then(subscription => { - return PushCrypto.decrypt(subscription.p256dhPrivateKey, - new Uint8Array(subscription.getKey("p256dh")), - new Uint8Array(subscription.getKey("auth")), - headers, message); - }) - .then(plaintext => { - let decryptedMessage = plaintext ? _decoder.decode(plaintext) : ""; - Messaging.sendRequestForResult({ - type: "FxAccountsPush:ReceivedPushMessageToDecode:Response", - message: decryptedMessage - }); - }) - .catch(err => { - Log.d("Error while decoding incoming message : " + err); - }); - }, - - // Copied from PushServiceAndroidGCM - _messageAndHeaders(data) { - // Default is no data (and no encryption). - let message = null; - let headers = null; - - if (data.message && data.enc && (data.enckey || data.cryptokey)) { - headers = { - encryption_key: data.enckey, - crypto_key: data.cryptokey, - encryption: data.enc, - encoding: data.con, - }; - // Ciphertext is (urlsafe) Base 64 encoded. - message = ChromeUtils.base64URLDecode(data.message, { - // The Push server may append padding. - padding: "ignore", - }); - } - return { headers, message }; - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - - classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}") -}; - -function urlsafeBase64Encode(key) { - return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false }); -} - -var components = [ FxAccountsPush ]; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest index 5194de48f..fe5deb95f 100644 --- a/mobile/android/components/MobileComponents.manifest +++ b/mobile/android/components/MobileComponents.manifest @@ -100,11 +100,6 @@ contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18 component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f} -# FxAccountsPush.js -component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js -contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} -category android-push-service FxAccountsPush @mozilla.org/fxa-push;1 - #ifndef RELEASE_OR_BETA # TabSource.js component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build index cac34b603..9e6683662 100644 --- a/mobile/android/components/moz.build +++ b/mobile/android/components/moz.build @@ -20,7 +20,6 @@ EXTRA_COMPONENTS += [ 'ContentPermissionPrompt.js', 'DirectoryProvider.js', 'FilePicker.js', - 'FxAccountsPush.js', 'HelperAppDialog.js', 'ImageBlockingPolicy.js', 'LoginManagerPrompter.js', diff --git a/mobile/android/config/proguard/proguard.cfg b/mobile/android/config/proguard/proguard.cfg index f44730e72..03485565d 100644 --- a/mobile/android/config/proguard/proguard.cfg +++ b/mobile/android/config/proguard/proguard.cfg @@ -16,8 +16,6 @@ -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.preference.Preference --keep public class * extends org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter --keep class org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter -keep public class * extends android.support.v4.app.Fragment diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh index 869d0332e..d1bfd1d26 100644 --- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -57,4 +57,4 @@ MOZ_WEBGL_CONFORMANT=1 export JS_GC_SMALL_CHUNK_SIZE=1 # Enable checking that add-ons are signed by the trusted root -MOZ_ADDON_SIGNING=1 +MOZ_ADDON_SIGNING= diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index af4a155a9..55fcacdda 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -503,7 +503,6 @@ @BINPATH@/components/ImageBlockingPolicy.js @BINPATH@/components/DirectoryProvider.js @BINPATH@/components/FilePicker.js -@BINPATH@/components/FxAccountsPush.js @BINPATH@/components/HelperAppDialog.js @BINPATH@/components/LoginManagerPrompter.js @BINPATH@/components/MobileComponents.manifest diff --git a/mobile/android/modules/Accounts.jsm b/mobile/android/modules/Accounts.jsm index a611f3c58..ee0fc0c5b 100644 --- a/mobile/android/modules/Accounts.jsm +++ b/mobile/android/modules/Accounts.jsm @@ -40,14 +40,8 @@ var Accounts = Object.freeze({ }).then(data => data.exists); }, - firefoxAccountsExist: function () { - return this._accountsExist("fxa"); - }, - syncAccountsExist: function () { - Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android. " + - "Please use `firefoxAccountsExist` instead.", - "https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/Accounts.jsm"); + Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android."); return Promise.resolve(false); }, @@ -73,106 +67,13 @@ var Accounts = Object.freeze({ }, _addDefaultEndpoints: function (json) { + // Empty without FxA let newData = Cu.cloneInto(json, {}, { cloneFunctions: false }); - let associations = { - authServerEndpoint: 'identity.fxaccounts.auth.uri', - profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri', - tokenServerEndpoint: 'identity.sync.tokenserver.uri' - }; - for (let key in associations) { - newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]); - } return newData; }, - /** - * Create a new Android Account corresponding to the given - * fxa-content-server "login" JSON datum. The new account will be - * in the "Engaged" state, and will start syncing immediately. - * - * It is an error if an Android Account already exists. - * - * Returns a Promise that resolves to a boolean indicating success. - */ - createFirefoxAccountFromJSON: function (json) { - return Messaging.sendRequestForResult({ - type: "Accounts:CreateFirefoxAccountFromJSON", - json: this._addDefaultEndpoints(json) - }); - }, - - /** - * Move an existing Android Account to the "Engaged" state with the given - * fxa-content-server "login" JSON datum. The account will (re)start - * syncing immediately, unless the user has manually configured the account - * to not Sync. - * - * It is an error if no Android Account exists. - * - * Returns a Promise that resolves to a boolean indicating success. - */ - updateFirefoxAccountFromJSON: function (json) { - return Messaging.sendRequestForResult({ - type: "Accounts:UpdateFirefoxAccountFromJSON", - json: this._addDefaultEndpoints(json) - }); - }, - - /** - * Notify that profile for Android Account has updated. - * The account will re-fetch the profile image. - * - * It is an error if no Android Account exists. - * - * There is no return value from this method. - */ - notifyFirefoxAccountProfileChanged: function () { - Messaging.sendRequest({ - type: "Accounts:ProfileUpdated", - }); - }, - - /** - * Fetch information about an existing Android Firefox Account. - * - * Returns a Promise that resolves to null if no Android Firefox Account - * exists, or an object including at least a string-valued 'email' key. - */ - getFirefoxAccount: function () { - return Messaging.sendRequestForResult({ - type: "Accounts:Exist", - kind: "fxa", - }).then(data => { - if (!data || !data.exists) { - return null; - } - delete data.exists; - return data; - }); - }, - - /** - * Delete an existing Android Firefox Account. - * - * It is an error if no Android Account exists. - * - * Returns a Promise that resolves to a boolean indicating success. - */ - deleteFirefoxAccount: function () { - return Messaging.sendRequestForResult({ - type: "Accounts:DeleteFirefoxAccount", - }); - }, - showSyncPreferences: function () { // Only show Sync preferences of an existing Android Account. - return Accounts.getFirefoxAccount().then(account => { - if (!account) { - throw new Error("Can't show Sync preferences of non-existent Firefox Account!"); - } - return Messaging.sendRequestForResult({ - type: "Accounts:ShowSyncPreferences" - }); - }); + throw new Error("Can't show Sync preferences without accounts!"); } }); diff --git a/mobile/android/modules/FxAccountsWebChannel.jsm b/mobile/android/modules/FxAccountsWebChannel.jsm deleted file mode 100644 index 6ee8fd07f..000000000 --- a/mobile/android/modules/FxAccountsWebChannel.jsm +++ /dev/null @@ -1,394 +0,0 @@ -// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Firefox Accounts Web Channel. - * - * Use the WebChannel component to receive messages about account - * state changes. - */ -this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */ - -Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */ -Cu.import("resource://gre/modules/Services.jsm"); /*global Services */ -Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */ -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */ - -const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts"); - -const WEBCHANNEL_ID = "account_updates"; - -const COMMAND_LOADED = "fxaccounts:loaded"; -const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"; -const COMMAND_LOGIN = "fxaccounts:login"; -const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password"; -const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account"; -const COMMAND_PROFILE_CHANGE = "profile:change"; -const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences"; - -const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; - -XPCOMUtils.defineLazyGetter(this, "strings", - () => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */ - -XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm"); - -this.FxAccountsWebChannelHelpers = function() { -}; - -this.FxAccountsWebChannelHelpers.prototype = { - /** - * Get the hash of account name of the previously signed in account. - */ - getPreviousAccountNameHashPref() { - try { - return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; - } catch (_) { - return ""; - } - }, - - /** - * Given an account name, set the hash of the previously signed in account. - * - * @param acctName the account name of the user's account. - */ - setPreviousAccountNameHashPref(acctName) { - let string = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - string.data = this.sha256(acctName); - Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); - }, - - /** - * Given a string, returns the SHA265 hash in base64. - */ - sha256(str) { - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - // Data is an array of bytes. - let data = converter.convertToByteArray(str, {}); - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA256); - hasher.update(data, data.length); - - return hasher.finish(true); - }, -}; - -/** - * Create a new FxAccountsWebChannel to listen for account updates. - * - * @param {Object} options Options - * @param {Object} options - * @param {String} options.content_uri - * The FxA Content server uri - * @param {String} options.channel_id - * The ID of the WebChannel - * @param {String} options.helpers - * Helpers functions. Should only be passed in for testing. - * @constructor - */ -this.FxAccountsWebChannel = function(options) { - if (!options) { - throw new Error("Missing configuration options"); - } - if (!options["content_uri"]) { - throw new Error("Missing 'content_uri' option"); - } - this._contentUri = options.content_uri; - - if (!options["channel_id"]) { - throw new Error("Missing 'channel_id' option"); - } - this._webChannelId = options.channel_id; - - // options.helpers is only specified by tests. - this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options); - - this._setupChannel(); -}; - -this.FxAccountsWebChannel.prototype = { - /** - * WebChannel that is used to communicate with content page - */ - _channel: null, - - /** - * WebChannel ID. - */ - _webChannelId: null, - /** - * WebChannel origin, used to validate origin of messages - */ - _webChannelOrigin: null, - - /** - * Release all resources that are in use. - */ - tearDown() { - this._channel.stopListening(); - this._channel = null; - this._channelCallback = null; - }, - - /** - * Configures and registers a new WebChannel - * - * @private - */ - _setupChannel() { - // if this.contentUri is present but not a valid URI, then this will throw an error. - try { - this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null); - this._registerChannel(); - } catch (e) { - log.e(e.toString()); - throw e; - } - }, - - /** - * Create a new channel with the WebChannelBroker, setup a callback listener - * @private - */ - _registerChannel() { - /** - * Processes messages that are called back from the FxAccountsChannel - * - * @param webChannelId {String} - * Command webChannelId - * @param message {Object} - * Command message - * @param sendingContext {Object} - * Message sending context. - * @param sendingContext.browser {browser} - * The <browser> object that captured the - * WebChannelMessageToChrome. - * @param sendingContext.eventTarget {EventTarget} - * The <EventTarget> where the message was sent. - * @param sendingContext.principal {Principal} - * The <Principal> of the EventTarget where the message was sent. - * @private - * - */ - let listener = (webChannelId, message, sendingContext) => { - if (message) { - let command = message.command; - let data = message.data; - log.d("FxAccountsWebChannel message received, command: " + command); - - // Respond to the message with true or false. - let respond = (data) => { - let response = { - command: command, - messageId: message.messageId, - data: data - }; - log.d("Sending response to command: " + command); - this._channel.send(response, sendingContext); - }; - - switch (command) { - case COMMAND_LOADED: - let mm = sendingContext.browser.docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - mm.sendAsyncMessage(COMMAND_LOADED); - break; - - case COMMAND_CAN_LINK_ACCOUNT: - Accounts.getFirefoxAccount().then(account => { - if (account) { - // If we /have/ an Android Account, we never allow the user to - // login to a different account. They need to manually delete - // the first Android Account and then create a new one. - if (account.email == data.email) { - // In future, we should use a UID for this comparison. - log.d("Relinking existing Android Account: email addresses agree."); - respond({ok: true}); - } else { - log.w("Not relinking existing Android Account: email addresses disagree!"); - let message = strings.GetStringFromName("relinkDenied.message"); - let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs"); - Snackbars.show(message, Snackbars.LENGTH_LONG, { - action: { - label: buttonLabel, - callback: () => { - // We have an account, so this opens Sync native preferences. - Accounts.launchSetup(); - }, - } - }); - respond({ok: false}); - } - } else { - // If we /don't have/ an Android Account, we warn if we're - // connecting to a new Account. This is to minimize surprise; - // we never did this when changing accounts via the native UI. - let prevAcctHash = this._helpers.getPreviousAccountNameHashPref(); - let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email)); - - if (shouldShowWarning) { - log.w("Warning about creating a new Android Account: previously linked to different email address!"); - let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1); - new Prompt({ - title: strings.GetStringFromName("relinkVerify.title"), - message: message, - buttons: [ - // This puts Cancel on the right. - strings.GetStringFromName("relinkVerify.cancel"), - strings.GetStringFromName("relinkVerify.continue"), - ], - }).show(result => respond({ok: result && result.button == 1})); - } else { - log.d("Not warning about creating a new Android Account: no previously linked email address."); - respond({ok: true}); - } - } - }).catch(e => { - log.e(e.toString()); - respond({ok: false}); - }); - break; - - case COMMAND_LOGIN: - // Either create a new Android Account or re-connect an existing - // Android Account here. There's not much to be done if we don't - // succeed or get an error. - Accounts.getFirefoxAccount().then(account => { - if (!account) { - return Accounts.createFirefoxAccountFromJSON(data).then(success => { - if (!success) { - throw new Error("Could not create Firefox Account!"); - } - UITelemetry.addEvent("action.1", "content", null, "fxaccount-create"); - return success; - }); - } else { - return Accounts.updateFirefoxAccountFromJSON(data).then(success => { - if (!success) { - throw new Error("Could not update Firefox Account!"); - } - UITelemetry.addEvent("action.1", "content", null, "fxaccount-login"); - return success; - }); - } - }) - .then(success => { - if (!success) { - throw new Error("Could not create or update Firefox Account!"); - } - - // Remember who it is so we can show a relink warning when appropriate. - this._helpers.setPreviousAccountNameHashPref(data.email); - - log.i("Created or updated Firefox Account."); - }) - .catch(e => { - log.e(e.toString()); - }); - break; - - case COMMAND_CHANGE_PASSWORD: - // Only update an existing Android Account. - Accounts.getFirefoxAccount().then(account => { - if (!account) { - throw new Error("Can't change password of non-existent Firefox Account!"); - } - return Accounts.updateFirefoxAccountFromJSON(data); - }) - .then(success => { - if (!success) { - throw new Error("Could not change Firefox Account password!"); - } - UITelemetry.addEvent("action.1", "content", null, "fxaccount-changepassword"); - log.i("Changed Firefox Account password."); - }) - .catch(e => { - log.e(e.toString()); - }); - break; - - case COMMAND_DELETE_ACCOUNT: - // The fxa-content-server has already confirmed the user's intent. - // Bombs away. There's no recovery from failure, and not even a - // real need to check an account exists (although we do, for error - // messaging only). - Accounts.getFirefoxAccount().then(account => { - if (!account) { - throw new Error("Can't delete non-existent Firefox Account!"); - } - return Accounts.deleteFirefoxAccount().then(success => { - if (!success) { - throw new Error("Could not delete Firefox Account!"); - } - UITelemetry.addEvent("action.1", "content", null, "fxaccount-delete"); - log.i("Firefox Account deleted."); - }); - }).catch(e => { - log.e(e.toString()); - }); - break; - - case COMMAND_PROFILE_CHANGE: - // Only update an existing Android Account. - Accounts.getFirefoxAccount().then(account => { - if (!account) { - throw new Error("Can't change profile of non-existent Firefox Account!"); - } - UITelemetry.addEvent("action.1", "content", null, "fxaccount-changeprofile"); - return Accounts.notifyFirefoxAccountProfileChanged(); - }) - .catch(e => { - log.e(e.toString()); - }); - break; - - case COMMAND_SYNC_PREFERENCES: - UITelemetry.addEvent("action.1", "content", null, "fxaccount-syncprefs"); - Accounts.showSyncPreferences() - .catch(e => { - log.e(e.toString()); - }); - break; - - default: - log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command)); - break; - } - } - }; - - this._channelCallback = listener; - this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); - this._channel.listen(listener); - - log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); - } -}; - -var singleton; -// The entry-point for this module, which ensures only one of our channels is -// ever created - we require this because the WebChannel is global in scope and -// allowing multiple channels would cause such notifications to be sent multiple -// times. -this.EnsureFxAccountsWebChannel = function() { - if (!singleton) { - let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri"); - // The FxAccountsWebChannel listens for events and updates the Java layer. - singleton = new this.FxAccountsWebChannel({ - content_uri: contentUri, - channel_id: WEBCHANNEL_ID, - }); - } -}; diff --git a/mobile/android/modules/moz.build b/mobile/android/modules/moz.build index 479ff1f3f..dd62e484a 100644 --- a/mobile/android/modules/moz.build +++ b/mobile/android/modules/moz.build @@ -10,7 +10,6 @@ EXTRA_JS_MODULES += [ 'dbg-browser-actors.js', 'DelayedInit.jsm', 'DownloadNotifications.jsm', - 'FxAccountsWebChannel.jsm', 'HelperApps.jsm', 'Home.jsm', 'HomeProvider.jsm', diff --git a/mobile/android/services/README.txt b/mobile/android/services/README.txt deleted file mode 100644 index cf4624ca4..000000000 --- a/mobile/android/services/README.txt +++ /dev/null @@ -1 +0,0 @@ -These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost. diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in deleted file mode 100644 index ad9542ad3..000000000 --- a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in +++ /dev/null @@ -1,63 +0,0 @@ - <activity - android:theme="@style/FxAccountTheme.FxAccountStatusActivity" - android:label="@string/fxaccount_status_activity_label" - android:clearTaskOnLaunch="true" - android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA" - android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity" - android:configChanges="locale|layoutDirection" - android:windowSoftInputMode="adjustResize"> - <!-- Adding a launcher will make this activity appear on the - Apps screen, which we only want when testing. --> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <!-- <category android:name="android.intent.category.LAUNCHER" /> --> - </intent-filter> - <intent-filter> - <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_STATUS"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> - - <receiver - android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver"> - <intent-filter> - <action android:name="android.intent.action.PACKAGE_REPLACED" /> - <data android:scheme="package"/> - </intent-filter> - </receiver> - - <activity - android:exported="false" - android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivityWeb"> - <intent-filter> - <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> - - <activity - android:exported="false" - android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivityWeb"> - <intent-filter> - <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> - - <activity - android:exported="false" - android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivityWeb"> - <intent-filter> - <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> - - <activity - android:exported="false" - android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivityWeb"> - <intent-filter> - <action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in deleted file mode 100644 index d5c7e3e5c..000000000 --- a/mobile/android/services/manifests/FxAccountAndroidManifest_permissions.xml.in +++ /dev/null @@ -1,18 +0,0 @@ - <uses-permission android:name="android.permission.GET_ACCOUNTS" /> - <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> - <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> - <uses-permission android:name="android.permission.USE_CREDENTIALS" /> - <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> - <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> - <uses-permission android:name="android.permission.WRITE_SETTINGS" /> - <uses-permission android:name="android.permission.READ_SYNC_STATS" /> - <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> - - <!-- A signature level permission granted only to the Firefox - channels sharing an Android Account type. --> - <permission - android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE" - android:protectionLevel="signature"> - </permission> - - <uses-permission android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE" /> diff --git a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in deleted file mode 100644 index a109d1ba3..000000000 --- a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in +++ /dev/null @@ -1,34 +0,0 @@ - <service - android:exported="true" - android:name="org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticatorService" > - <intent-filter > - <action android:name="android.accounts.AccountAuthenticator" /> - </intent-filter> - - <meta-data - android:name="android.accounts.AccountAuthenticator" - android:resource="@xml/fxaccount_authenticator" /> - </service> - - <service - android:exported="false" - android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" > - </service> - - <service - android:exported="false" - android:name="org.mozilla.gecko.fxa.sync.FxAccountProfileService" > - </service> - - <!-- Firefox Sync. --> - <service - android:exported="false" - android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" > - <intent-filter > - <action android:name="android.content.SyncAdapter" /> - </intent-filter> - - <meta-data - android:name="android.content.SyncAdapter" - android:resource="@xml/fxaccount_syncadapter" /> - </service>
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java deleted file mode 100644 index df603a58e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/ReadingListConstants.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background; - -import org.mozilla.gecko.AppConstants; - -/** - * This is in 'background' not 'reading' so that it's still usable even when the - * Reading List feature is build-time disabled. - */ -public class ReadingListConstants { - public static final String GLOBAL_LOG_TAG = "FxReadingList"; - public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")"; - public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/"; - public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/"; - - public static final String OAUTH_SCOPE_READINGLIST = "readinglist"; - public static final String AUTH_TOKEN_TYPE = "oauth::" + OAUTH_SCOPE_READINGLIST; - - public static boolean DEBUG = false; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java deleted file mode 100644 index 1ead09afa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/EditorBranch.java +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common; - -import java.util.Set; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -public class EditorBranch implements Editor { - - private final String prefix; - private Editor editor; - - public EditorBranch(final SharedPreferences prefs, final String prefix) { - if (!prefix.endsWith(".")) { - throw new IllegalArgumentException("No trailing period in prefix."); - } - this.prefix = prefix; - this.editor = prefs.edit(); - } - - @Override - public void apply() { - this.editor.apply(); - } - - @Override - public Editor clear() { - this.editor = this.editor.clear(); - return this; - } - - @Override - public boolean commit() { - return this.editor.commit(); - } - - @Override - public Editor putBoolean(String key, boolean value) { - this.editor = this.editor.putBoolean(prefix + key, value); - return this; - } - - @Override - public Editor putFloat(String key, float value) { - this.editor = this.editor.putFloat(prefix + key, value); - return this; - } - - @Override - public Editor putInt(String key, int value) { - this.editor = this.editor.putInt(prefix + key, value); - return this; - } - - @Override - public Editor putLong(String key, long value) { - this.editor = this.editor.putLong(prefix + key, value); - return this; - } - - @Override - public Editor putString(String key, String value) { - this.editor = this.editor.putString(prefix + key, value); - return this; - } - - // Not marking as Override, because Android <= 10 doesn't have - // putStringSet. Neither can we implement it. - public Editor putStringSet(String key, Set<String> value) { - throw new RuntimeException("putStringSet not available."); - } - - @Override - public Editor remove(String key) { - this.editor = this.editor.remove(prefix + key); - return this; - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java deleted file mode 100644 index d661e62dc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/GlobalConstants.java +++ /dev/null @@ -1,90 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.AppConstants.Versions; - -/** - * Constant values common to all Android services. - */ -public class GlobalConstants { - public static final String BROWSER_INTENT_PACKAGE = AppConstants.ANDROID_PACKAGE_NAME; - public static final String BROWSER_INTENT_CLASS = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS; - - public static final int SHARED_PREFERENCES_MODE = 0; - - // Common time values. - public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; - public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY; - - // Acceptable cipher suites. - /** - * We support only a very limited range of strong cipher suites and protocols: - * no SSLv3 or TLSv1.0 (if we can), no DHE ciphers that might be vulnerable to Logjam - * (https://weakdh.org/), no RC4. - * - * Backstory: Bug 717691 (we no longer support Android 2.2, so the name - * workaround is unnecessary), Bug 1081953, Bug 1061273, Bug 1166839. - * - * See <http://developer.android.com/reference/javax/net/ssl/SSLSocket.html> for - * supported Android versions for each set of protocols and cipher suites. - * - * Note that currently we need to support connections to Sync 1.1 on Mozilla-hosted infra, - * as well as connections to FxA and Sync 1.5 on AWS. - * - * ELB cipher suites: - * <http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html> - */ - public static final String[] DEFAULT_CIPHER_SUITES; - public static final String[] DEFAULT_PROTOCOLS; - - static { - // Prioritize 128 over 256 as a tradeoff between device CPU/battery and the minor - // increase in strength. - if (Versions.feature20Plus) { - DEFAULT_CIPHER_SUITES = new String[] - { - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", // 20+ - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", // 20+ - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", // 20+ - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+ - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", // 20+ - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 20+ - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+ - - // For Sync 1.1. - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+ - "TLS_RSA_WITH_AES_128_CBC_SHA", // 9+ - }; - } else { - DEFAULT_CIPHER_SUITES = new String[] - { - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+ - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", // 11+ - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+ - - // For Sync 1.1. - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+ - "TLS_RSA_WITH_AES_128_CBC_SHA", // 9+ - }; - } - - if (Versions.feature16Plus) { - DEFAULT_PROTOCOLS = new String[] - { - "TLSv1.2", - "TLSv1.1", - "TLSv1", // We would like to remove this, and will do so when we can. - }; - } else { - // Fall back to TLSv1 if there's nothing better. - DEFAULT_PROTOCOLS = new String[] - { - "TLSv1", - }; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java deleted file mode 100644 index 78d5f61a1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/PrefsBranch.java +++ /dev/null @@ -1,83 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common; - -import java.util.Map; -import java.util.Set; - -import android.content.SharedPreferences; - -/** - * A wrapper around a portion of the SharedPreferences space. - */ -public class PrefsBranch implements SharedPreferences { - private final SharedPreferences prefs; - private final String prefix; // Including trailing period. - - public PrefsBranch(SharedPreferences prefs, String prefix) { - if (!prefix.endsWith(".")) { - throw new IllegalArgumentException("No trailing period in prefix."); - } - this.prefs = prefs; - this.prefix = prefix; - } - - @Override - public boolean contains(String key) { - return prefs.contains(prefix + key); - } - - @Override - public Editor edit() { - return new EditorBranch(prefs, prefix); - } - - @Override - public Map<String, ?> getAll() { - // Not implemented. TODO - return null; - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - return prefs.getBoolean(prefix + key, defValue); - } - - @Override - public float getFloat(String key, float defValue) { - return prefs.getFloat(prefix + key, defValue); - } - - @Override - public int getInt(String key, int defValue) { - return prefs.getInt(prefix + key, defValue); - } - - @Override - public long getLong(String key, long defValue) { - return prefs.getLong(prefix + key, defValue); - } - - @Override - public String getString(String key, String defValue) { - return prefs.getString(prefix + key, defValue); - } - - // Not marking as Override, because Android <= 10 doesn't have - // getStringSet. Neither can we implement it. - public Set<String> getStringSet(String key, Set<String> defValue) { - throw new RuntimeException("getStringSet not available."); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - prefs.registerOnSharedPreferenceChangeListener(listener); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - prefs.unregisterOnSharedPreferenceChangeListener(listener); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java deleted file mode 100644 index 2575717eb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/Logger.java +++ /dev/null @@ -1,232 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log; - -import java.io.PrintWriter; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter; -import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter; -import org.mozilla.gecko.background.common.log.writers.LogWriter; -import org.mozilla.gecko.background.common.log.writers.PrintLogWriter; -import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter; -import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter; - -import android.util.Log; - -/** - * Logging helper class. Serializes all log operations (by synchronizing). - */ -public class Logger { - public static final String LOGGER_TAG = "Logger"; - public static final String DEFAULT_LOG_TAG = "GeckoLogger"; - - // For extra debugging. - public static boolean LOG_PERSONAL_INFORMATION = false; - - /** - * Allow each thread to use its own global log tag. This allows - * independent services to log as different sources. - * - * When your thread sets up logging, it should do something like the following: - * - * Logger.setThreadLogTag("MyTag"); - * - * The value is inheritable, so worker threads and such do not need to - * set the same log tag as their parent. - */ - private static final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() { - @Override - protected String initialValue() { - return DEFAULT_LOG_TAG; - } - }; - - public static void setThreadLogTag(final String logTag) { - Logger.logTag.set(logTag); - } - public static String getThreadLogTag() { - return Logger.logTag.get(); - } - - /** - * Current set of writers to which we will log. - * <p> - * We want logging to be available while running tests, so we initialize - * this set statically. - */ - protected final static Set<LogWriter> logWriters; - static { - final Set<LogWriter> defaultWriters = Logger.defaultLogWriters(); - logWriters = new LinkedHashSet<LogWriter>(defaultWriters); - } - - /** - * Default set of log writers to log to. - */ - public final static Set<LogWriter> defaultLogWriters() { - final String processedPackage = GlobalConstants.BROWSER_INTENT_PACKAGE.replace("org.mozilla.", ""); - - final Set<LogWriter> defaultLogWriters = new LinkedHashSet<LogWriter>(); - - final LogWriter log = new AndroidLogWriter(); - final LogWriter cache = new AndroidLevelCachingLogWriter(log); - - final LogWriter single = new SimpleTagLogWriter(processedPackage, new ThreadLocalTagLogWriter(Logger.logTag, cache)); - - defaultLogWriters.add(single); - return defaultLogWriters; - } - - public static synchronized void startLoggingTo(LogWriter logWriter) { - logWriters.add(logWriter); - } - - public static synchronized void startLoggingToWriters(Set<LogWriter> writers) { - logWriters.addAll(writers); - } - - public static synchronized void stopLoggingTo(LogWriter logWriter) { - try { - logWriter.close(); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e); - } - logWriters.remove(logWriter); - } - - public static synchronized void stopLoggingToAll() { - for (LogWriter logWriter : logWriters) { - try { - logWriter.close(); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e); - } - } - logWriters.clear(); - } - - /** - * Write to only the default log writers. - */ - public static synchronized void resetLogging() { - stopLoggingToAll(); - logWriters.addAll(Logger.defaultLogWriters()); - } - - /** - * Start writing log output to stdout. - * <p> - * Use <code>resetLogging</code> to stop logging to stdout. - */ - public static synchronized void startLoggingToConsole() { - setThreadLogTag("Test"); - startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true))); - } - - // Synchronized version for other classes to use. - public static synchronized boolean shouldLogVerbose(String logTag) { - for (LogWriter logWriter : logWriters) { - if (logWriter.shouldLogVerbose(logTag)) { - return true; - } - } - return false; - } - - public static void error(String tag, String message) { - Logger.error(tag, message, null); - } - - public static void warn(String tag, String message) { - Logger.warn(tag, message, null); - } - - public static void info(String tag, String message) { - Logger.info(tag, message, null); - } - - public static void debug(String tag, String message) { - Logger.debug(tag, message, null); - } - - public static void trace(String tag, String message) { - Logger.trace(tag, message, null); - } - - public static void pii(String tag, String message) { - if (LOG_PERSONAL_INFORMATION) { - Logger.debug(tag, "$$PII$$: " + message); - } - } - - public static synchronized void error(String tag, String message, Throwable error) { - Iterator<LogWriter> it = logWriters.iterator(); - while (it.hasNext()) { - LogWriter writer = it.next(); - try { - writer.error(tag, message, error); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e); - it.remove(); - } - } - } - - public static synchronized void warn(String tag, String message, Throwable error) { - Iterator<LogWriter> it = logWriters.iterator(); - while (it.hasNext()) { - LogWriter writer = it.next(); - try { - writer.warn(tag, message, error); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e); - it.remove(); - } - } - } - - public static synchronized void info(String tag, String message, Throwable error) { - Iterator<LogWriter> it = logWriters.iterator(); - while (it.hasNext()) { - LogWriter writer = it.next(); - try { - writer.info(tag, message, error); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e); - it.remove(); - } - } - } - - public static synchronized void debug(String tag, String message, Throwable error) { - Iterator<LogWriter> it = logWriters.iterator(); - while (it.hasNext()) { - LogWriter writer = it.next(); - try { - writer.debug(tag, message, error); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e); - it.remove(); - } - } - } - - public static synchronized void trace(String tag, String message, Throwable error) { - Iterator<LogWriter> it = logWriters.iterator(); - while (it.hasNext()) { - LogWriter writer = it.next(); - try { - writer.trace(tag, message, error); - } catch (Exception e) { - Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e); - it.remove(); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java deleted file mode 100644 index ac4250a03..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLevelCachingLogWriter.java +++ /dev/null @@ -1,132 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -import java.util.IdentityHashMap; -import java.util.Map; - -import android.util.Log; - -/** - * Make a <code>LogWriter</code> only log when the Android log system says to. - */ -public class AndroidLevelCachingLogWriter extends LogWriter { - protected final LogWriter inner; - - public AndroidLevelCachingLogWriter(LogWriter inner) { - this.inner = inner; - } - - // I can't believe we have to implement this ourselves. - // These aren't synchronized (and neither are the setters) because - // the logging calls themselves are synchronized. - private Map<String, Boolean> isErrorLoggable = new IdentityHashMap<String, Boolean>(); - private Map<String, Boolean> isWarnLoggable = new IdentityHashMap<String, Boolean>(); - private Map<String, Boolean> isInfoLoggable = new IdentityHashMap<String, Boolean>(); - private Map<String, Boolean> isDebugLoggable = new IdentityHashMap<String, Boolean>(); - private Map<String, Boolean> isVerboseLoggable = new IdentityHashMap<String, Boolean>(); - - /** - * Empty the caches of log levels. - */ - public void refreshLogLevels() { - isErrorLoggable = new IdentityHashMap<String, Boolean>(); - isWarnLoggable = new IdentityHashMap<String, Boolean>(); - isInfoLoggable = new IdentityHashMap<String, Boolean>(); - isDebugLoggable = new IdentityHashMap<String, Boolean>(); - isVerboseLoggable = new IdentityHashMap<String, Boolean>(); - } - - private boolean shouldLogError(String logTag) { - Boolean out = isErrorLoggable.get(logTag); - if (out != null) { - return out; - } - out = Log.isLoggable(logTag, Log.ERROR); - isErrorLoggable.put(logTag, out); - return out; - } - - private boolean shouldLogWarn(String logTag) { - Boolean out = isWarnLoggable.get(logTag); - if (out != null) { - return out; - } - out = Log.isLoggable(logTag, Log.WARN); - isWarnLoggable.put(logTag, out); - return out; - } - - private boolean shouldLogInfo(String logTag) { - Boolean out = isInfoLoggable.get(logTag); - if (out != null) { - return out; - } - out = Log.isLoggable(logTag, Log.INFO); - isInfoLoggable.put(logTag, out); - return out; - } - - private boolean shouldLogDebug(String logTag) { - Boolean out = isDebugLoggable.get(logTag); - if (out != null) { - return out; - } - out = Log.isLoggable(logTag, Log.DEBUG); - isDebugLoggable.put(logTag, out); - return out; - } - - @Override - public boolean shouldLogVerbose(String logTag) { - Boolean out = isVerboseLoggable.get(logTag); - if (out != null) { - return out; - } - out = Log.isLoggable(logTag, Log.VERBOSE); - isVerboseLoggable.put(logTag, out); - return out; - } - - @Override - public void error(String tag, String message, Throwable error) { - if (shouldLogError(tag)) { - inner.error(tag, message, error); - } - } - - @Override - public void warn(String tag, String message, Throwable error) { - if (shouldLogWarn(tag)) { - inner.warn(tag, message, error); - } - } - - @Override - public void info(String tag, String message, Throwable error) { - if (shouldLogInfo(tag)) { - inner.info(tag, message, error); - } - } - - @Override - public void debug(String tag, String message, Throwable error) { - if (shouldLogDebug(tag)) { - inner.debug(tag, message, error); - } - } - - @Override - public void trace(String tag, String message, Throwable error) { - if (shouldLogVerbose(tag)) { - inner.trace(tag, message, error); - } - } - - @Override - public void close() { - inner.close(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java deleted file mode 100644 index 9d309844d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/AndroidLogWriter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -import android.util.Log; - -/** - * Log to the Android log. - */ -public class AndroidLogWriter extends LogWriter { - @Override - public boolean shouldLogVerbose(String logTag) { - return true; - } - - @Override - public void error(String tag, String message, Throwable error) { - Log.e(tag, message, error); - } - - @Override - public void warn(String tag, String message, Throwable error) { - Log.w(tag, message, error); - } - - @Override - public void info(String tag, String message, Throwable error) { - Log.i(tag, message, error); - } - - @Override - public void debug(String tag, String message, Throwable error) { - Log.d(tag, message, error); - } - - @Override - public void trace(String tag, String message, Throwable error) { - Log.v(tag, message, error); - } - - @Override - public void close() { - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java deleted file mode 100644 index 74c3608c4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LevelFilteringLogWriter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -import android.util.Log; - -/** - * A LogWriter that logs only if the message is as important as the specified - * level. For example, if the specified level is <code>Log.WARN</code>, only - * <code>warn</code> and <code>error</code> will log. - */ -public class LevelFilteringLogWriter extends LogWriter { - protected final LogWriter inner; - protected final int logLevel; - - public LevelFilteringLogWriter(int logLevel, LogWriter inner) { - this.inner = inner; - this.logLevel = logLevel; - } - - @Override - public void close() { - inner.close(); - } - - @Override - public void error(String tag, String message, Throwable error) { - if (logLevel <= Log.ERROR) { - inner.error(tag, message, error); - } - } - - @Override - public void warn(String tag, String message, Throwable error) { - if (logLevel <= Log.WARN) { - inner.warn(tag, message, error); - } - } - - @Override - public void info(String tag, String message, Throwable error) { - if (logLevel <= Log.INFO) { - inner.info(tag, message, error); - } - } - - @Override - public void debug(String tag, String message, Throwable error) { - if (logLevel <= Log.DEBUG) { - inner.debug(tag, message, error); - } - } - - @Override - public void trace(String tag, String message, Throwable error) { - if (logLevel <= Log.VERBOSE) { - inner.trace(tag, message, error); - } - } - - @Override - public boolean shouldLogVerbose(String tag) { - return logLevel <= Log.VERBOSE; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java deleted file mode 100644 index acfb09969..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/LogWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -/** - * An abstract object that logs information in some way. - * <p> - * Intended to be composed with other log writers, for example a log - * writer could make all log entries have the same single log tag, or - * could ignore certain log levels, before delegating to an inner log - * writer. - */ -public abstract class LogWriter { - public abstract void error(String tag, String message, Throwable error); - public abstract void warn(String tag, String message, Throwable error); - public abstract void info(String tag, String message, Throwable error); - public abstract void debug(String tag, String message, Throwable error); - public abstract void trace(String tag, String message, Throwable error); - - /** - * We expect <code>close</code> to be called only by static - * synchronized methods in class <code>Logger</code>. - */ - public abstract void close(); - - public abstract boolean shouldLogVerbose(String tag); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java deleted file mode 100644 index 6e1f63de3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/PrintLogWriter.java +++ /dev/null @@ -1,77 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -import java.io.PrintWriter; - -/** - * Log to a <code>PrintWriter</code>. - */ -public class PrintLogWriter extends LogWriter { - protected final PrintWriter pw; - protected boolean closed = false; - - public static final String ERROR = " :: E :: "; - public static final String WARN = " :: W :: "; - public static final String INFO = " :: I :: "; - public static final String DEBUG = " :: D :: "; - public static final String VERBOSE = " :: V :: "; - - public PrintLogWriter(PrintWriter pw) { - this.pw = pw; - } - - protected void log(String tag, String message, Throwable error) { - if (closed) { - return; - } - - pw.println(tag + message); - if (error != null) { - error.printStackTrace(pw); - } - } - - @Override - public void error(String tag, String message, Throwable error) { - log(tag, ERROR + message, error); - } - - @Override - public void warn(String tag, String message, Throwable error) { - log(tag, WARN + message, error); - } - - @Override - public void info(String tag, String message, Throwable error) { - log(tag, INFO + message, error); - } - - @Override - public void debug(String tag, String message, Throwable error) { - log(tag, DEBUG + message, error); - } - - @Override - public void trace(String tag, String message, Throwable error) { - log(tag, VERBOSE + message, error); - } - - @Override - public boolean shouldLogVerbose(String tag) { - return true; - } - - @Override - public void close() { - if (closed) { - return; - } - if (pw != null) { - pw.close(); - } - closed = true; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java deleted file mode 100644 index a17654371..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/SimpleTagLogWriter.java +++ /dev/null @@ -1,21 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -/** - * Make a <code>LogWriter</code> only log with a single string tag. - */ -public class SimpleTagLogWriter extends TagLogWriter { - final String tag; - public SimpleTagLogWriter(String tag, LogWriter inner) { - super(inner); - this.tag = tag; - } - - @Override - protected String getMainTag() { - return tag; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java deleted file mode 100644 index d6a9f5eb8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/StringLogWriter.java +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -import java.io.PrintWriter; -import java.io.StringWriter; - -public class StringLogWriter extends LogWriter { - protected final StringWriter sw; - protected final PrintLogWriter inner; - - public StringLogWriter() { - sw = new StringWriter(); - inner = new PrintLogWriter(new PrintWriter(sw)); - } - - public String toString() { - return sw.toString(); - } - - @Override - public boolean shouldLogVerbose(String tag) { - return true; - } - - @Override - public void error(String tag, String message, Throwable error) { - inner.error(tag, message, error); - } - - @Override - public void warn(String tag, String message, Throwable error) { - inner.warn(tag, message, error); - } - - @Override - public void info(String tag, String message, Throwable error) { - inner.info(tag, message, error); - } - - @Override - public void debug(String tag, String message, Throwable error) { - inner.debug(tag, message, error); - } - - @Override - public void trace(String tag, String message, Throwable error) { - inner.trace(tag, message, error); - } - - @Override - public void close() { - inner.close(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java deleted file mode 100644 index fbcd94a91..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/TagLogWriter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -/** - * A @link{LogWriter} that logs each message under a parent tag. - */ -public abstract class TagLogWriter extends LogWriter { - - protected final LogWriter inner; - - public TagLogWriter(final LogWriter inner) { - super(); - this.inner = inner; - } - - protected abstract String getMainTag(); - - @Override - public void error(String tag, String message, Throwable error) { - inner.error(this.getMainTag(), tag + " :: " + message, error); - } - - @Override - public void warn(String tag, String message, Throwable error) { - inner.warn(this.getMainTag(), tag + " :: " + message, error); - } - - @Override - public void info(String tag, String message, Throwable error) { - inner.info(this.getMainTag(), tag + " :: " + message, error); - } - - @Override - public void debug(String tag, String message, Throwable error) { - inner.debug(this.getMainTag(), tag + " :: " + message, error); - } - - @Override - public void trace(String tag, String message, Throwable error) { - inner.trace(this.getMainTag(), tag + " :: " + message, error); - } - - @Override - public boolean shouldLogVerbose(String tag) { - return inner.shouldLogVerbose(this.getMainTag()); - } - - @Override - public void close() { - inner.close(); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java deleted file mode 100644 index 0c83504a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/log/writers/ThreadLocalTagLogWriter.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.log.writers; - -/** - * Log with a single global tag… but that tag can be different for each thread. - * - * Takes a @link{ThreadLocal} as a constructor parameter. - */ -public class ThreadLocalTagLogWriter extends TagLogWriter { - - private final ThreadLocal<String> tag; - - public ThreadLocalTagLogWriter(ThreadLocal<String> tag, LogWriter inner) { - super(inner); - this.tag = tag; - } - - @Override - protected String getMainTag() { - return this.tag.get(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java deleted file mode 100644 index 6639b817d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/common/telemetry/TelemetryWrapper.java +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.common.telemetry; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import org.mozilla.gecko.background.common.log.Logger; - -/** - * Android Background Services are normally built into Fennec, but can also be - * built as a stand-alone APK for rapid local development. The current Telemetry - * implementation is coupled to Gecko, and Background Services should not - * interact with Gecko directly. To maintain this independence, Background - * Services lazily introspects the relevant Telemetry class from the enclosing - * package, warning but otherwise ignoring failures during introspection or - * invocation. - * <p> - * It is possible that Background Services will introspect and invoke the - * Telemetry implementation while Gecko is not running. In this case, the Fennec - * process itself buffers Telemetry events until such time as they can be - * flushed to disk and uploaded. <b>There is no guarantee that all Telemetry - * events will be uploaded!</b> Depending on the volume of data and the - * application lifecycle, Telemetry events may be dropped. - */ -public class TelemetryWrapper { - private static final String LOG_TAG = TelemetryWrapper.class.getSimpleName(); - - // Marking this volatile maintains thread safety cheaply. - private static volatile Method mAddToHistogram; - - public static void addToHistogram(String key, int value) { - if (mAddToHistogram == null) { - try { - final Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry"); - mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class); - } catch (ClassNotFoundException e) { - Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry class found!"); - return; - } catch (NoSuchMethodException e) { - Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry.addToHistogram(String, int) method not found!"); - return; - } - } - - if (mAddToHistogram != null) { - try { - mAddToHistogram.invoke(null, key, value); - } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { - Logger.warn(LOG_TAG, "Got exception invoking telemetry!"); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java deleted file mode 100644 index bce968b00..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/CursorDumper.java +++ /dev/null @@ -1,99 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.db; - -import android.database.Cursor; - -/** - * A utility for dumping a cursor the debug log. - * <p> - * <b>For debugging only!</p> - */ -public class CursorDumper { - protected static String fixedWidth(int width, String s) { - if (s == null) { - return spaces(width); - } - int length = s.length(); - if (width == length) { - return s; - } - if (width > length) { - return s + spaces(width - length); - } - return s.substring(0, width); - } - - protected static String spaces(int i) { - return " ".substring(0, i); - } - - protected static String dashes(int i) { - return "-------------------------------------".substring(0, i); - } - - /** - * Dump a cursor to the debug log, ignoring any log level settings. - * <p> - * The position in the cursor is maintained. Caller is responsible for opening - * and closing cursor. - * - * @param cursor - * to dump. - */ - public static void dumpCursor(Cursor cursor) { - dumpCursor(cursor, 18, "records"); - } - - /** - * Dump a cursor to the debug log, ignoring any log level settings. - * <p> - * The position in the cursor is maintained. Caller is responsible for opening - * and closing cursor. - * - * @param cursor - * to dump. - * @param columnWidth - * how many characters per cursor column. - * @param tags - * a descriptor, printed like "(10 tags)", in the header row. - */ - protected static void dumpCursor(Cursor cursor, int columnWidth, String tags) { - int originalPosition = cursor.getPosition(); - try { - String[] columnNames = cursor.getColumnNames(); - int columnCount = cursor.getColumnCount(); - - for (int i = 0; i < columnCount; ++i) { - System.out.print(fixedWidth(columnWidth, columnNames[i]) + " | "); - } - System.out.println("(" + cursor.getCount() + " " + tags + ")"); - for (int i = 0; i < columnCount; ++i) { - System.out.print(dashes(columnWidth) + " | "); - } - System.out.println(""); - if (!cursor.moveToFirst()) { - System.out.println("EMPTY"); - return; - } - - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - for (int i = 0; i < columnCount; ++i) { - System.out.print(fixedWidth(columnWidth, cursor.getString(i)) + " | "); - } - System.out.println(""); - cursor.moveToNext(); - } - for (int i = 0; i < columnCount-1; ++i) { - System.out.print(dashes(columnWidth + 3)); - } - System.out.print(dashes(columnWidth + 3 - 1)); - System.out.println(""); - } finally { - cursor.moveToPosition(originalPosition); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java deleted file mode 100644 index f38cfdf0e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/db/Tab.java +++ /dev/null @@ -1,86 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.db; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Tabs; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -import android.content.ContentValues; -import android.database.Cursor; - -// Immutable. -public class Tab { - public final String title; - public final String icon; - public final JSONArray history; - public final long lastUsed; - - public Tab(String title, String icon, JSONArray history, long lastUsed) { - this.title = title; - this.icon = icon; - this.history = history; - this.lastUsed = lastUsed; - } - - public ContentValues toContentValues(String clientGUID, int position) { - ContentValues out = new ContentValues(); - out.put(BrowserContract.Tabs.POSITION, position); - out.put(BrowserContract.Tabs.CLIENT_GUID, clientGUID); - - out.put(BrowserContract.Tabs.FAVICON, this.icon); - out.put(BrowserContract.Tabs.LAST_USED, this.lastUsed); - out.put(BrowserContract.Tabs.TITLE, this.title); - out.put(BrowserContract.Tabs.URL, (String) this.history.get(0)); - out.put(BrowserContract.Tabs.HISTORY, this.history.toJSONString()); - return out; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Tab)) { - return false; - } - final Tab other = (Tab) o; - - if (!RepoUtils.stringsEqual(this.title, other.title)) { - return false; - } - if (!RepoUtils.stringsEqual(this.icon, other.icon)) { - return false; - } - - if (!(this.lastUsed == other.lastUsed)) { - return false; - } - - return Utils.sameArrays(this.history, other.history); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - /** - * Extract a <code>Tab</code> from a cursor row. - * <p> - * Caller is responsible for creating, positioning, and closing the cursor. - * - * @param cursor - * to inspect. - * @return <code>Tab</code> instance. - */ - public static Tab fromCursor(final Cursor cursor) { - final String title = RepoUtils.getStringFromCursor(cursor, Tabs.TITLE); - final String icon = RepoUtils.getStringFromCursor(cursor, Tabs.FAVICON); - final JSONArray history = RepoUtils.getJSONArrayFromCursor(cursor, Tabs.HISTORY); - final long lastUsed = RepoUtils.getLongFromCursor(cursor, Tabs.LAST_USED); - - return new Tab(title, icon, history, lastUsed); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java deleted file mode 100644 index 98809137f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20CreateDelegate.java +++ /dev/null @@ -1,52 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; - -public class FxAccount20CreateDelegate { - protected final byte[] emailUTF8; - protected final byte[] authPW; - protected final boolean preVerified; - - /** - * Make a new "create account" delegate. - * - * @param emailUTF8 - * email as UTF-8 bytes. - * @param quickStretchedPW - * quick stretched password as bytes. - * @param preVerified - * true if account should be marked already verified; only effective - * for non-production auth servers. - * @throws UnsupportedEncodingException - * @throws GeneralSecurityException - */ - public FxAccount20CreateDelegate(byte[] emailUTF8, byte[] quickStretchedPW, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException { - this.emailUTF8 = emailUTF8; - this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW); - this.preVerified = preVerified; - } - - public ExtendedJSONObject getCreateBody() throws FxAccountClientException { - final ExtendedJSONObject body = new ExtendedJSONObject(); - try { - body.put("email", new String(emailUTF8, "UTF-8")); - body.put("authPW", Utils.byte2Hex(authPW)); - if (preVerified) { - // Production endpoints do not allow preVerified; this assumes we only - // set it when it's okay to send it. - body.put("preVerified", preVerified); - } - return body; - } catch (UnsupportedEncodingException e) { - throw new FxAccountClientException(e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java deleted file mode 100644 index 0266a6eab..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccount20LoginDelegate.java +++ /dev/null @@ -1,36 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; - -/** - * An abstraction around providing an email and authorization token to the auth - * server. - */ -public class FxAccount20LoginDelegate { - protected final byte[] emailUTF8; - protected final byte[] authPW; - - public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] quickStretchedPW) throws UnsupportedEncodingException, GeneralSecurityException { - this.emailUTF8 = emailUTF8; - this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW); - } - - public ExtendedJSONObject getCreateBody() throws FxAccountClientException { - final ExtendedJSONObject body = new ExtendedJSONObject(); - try { - body.put("email", new String(emailUTF8, "UTF-8")); - body.put("authPW", Utils.byte2Hex(authPW)); - return body; - } catch (UnsupportedEncodingException e) { - throw new FxAccountClientException(e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java deleted file mode 100644 index ed959ff0e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java +++ /dev/null @@ -1,24 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse; -import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse; -import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate; -import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys; -import org.mozilla.gecko.fxa.FxAccountDevice; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -import java.util.List; - -public interface FxAccountClient { - public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate); - public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate); - public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate); - public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate); - public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate); - public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate); - public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java deleted file mode 100644 index 596f4525e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java +++ /dev/null @@ -1,914 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import android.support.annotation.NonNull; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException; -import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.fxa.FxAccountDevice; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.HKDF; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; -import org.mozilla.gecko.sync.net.Resource; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.Executor; - -import javax.crypto.Mac; - -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -/** - * An HTTP client for talking to an FxAccount server. - * <p> - * <p> - * The delegate structure used is a little different from the rest of the code - * base. We add a <code>RequestDelegate</code> layer that processes a typed - * value extracted from the body of a successful response. - */ -public class FxAccountClient20 implements FxAccountClient { - protected static final String LOG_TAG = FxAccountClient20.class.getSimpleName(); - - protected static final String ACCEPT_HEADER = "application/json;charset=utf-8"; - - public static final String JSON_KEY_EMAIL = "email"; - public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken"; - public static final String JSON_KEY_SESSIONTOKEN = "sessionToken"; - public static final String JSON_KEY_UID = "uid"; - public static final String JSON_KEY_VERIFIED = "verified"; - public static final String JSON_KEY_ERROR = "error"; - public static final String JSON_KEY_MESSAGE = "message"; - public static final String JSON_KEY_INFO = "info"; - public static final String JSON_KEY_CODE = "code"; - public static final String JSON_KEY_ERRNO = "errno"; - public static final String JSON_KEY_EXISTS = "exists"; - - protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO }; - protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO }; - - /** - * The server's URI. - * <p> - * We assume throughout that this ends with a trailing slash (and guarantee as - * much in the constructor). - */ - protected final String serverURI; - - protected final Executor executor; - - public FxAccountClient20(String serverURI, Executor executor) { - if (serverURI == null) { - throw new IllegalArgumentException("Must provide a server URI."); - } - if (executor == null) { - throw new IllegalArgumentException("Must provide a non-null executor."); - } - this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; - if (!this.serverURI.endsWith("/")) { - throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI); - } - this.executor = executor; - } - - protected BaseResource getBaseResource(String path, Map<String, String> queryParameters) throws UnsupportedEncodingException, URISyntaxException { - if (queryParameters == null || queryParameters.isEmpty()) { - return getBaseResource(path); - } - final String[] array = new String[2 * queryParameters.size()]; - int i = 0; - for (Entry<String, String> entry : queryParameters.entrySet()) { - array[i++] = entry.getKey(); - array[i++] = entry.getValue(); - } - return getBaseResource(path, array); - } - - /** - * Create <code>BaseResource</code>, encoding query parameters carefully. - * <p> - * This is equivalent to <code>android.net.Uri.Builder</code>, which is not - * present in our JUnit 4 tests. - * - * @param path fragment. - * @param queryParameters list of key/value query parameter pairs. Must be even length! - * @return <code>BaseResource<instance> - * @throws URISyntaxException - * @throws UnsupportedEncodingException - */ - protected BaseResource getBaseResource(String path, String... queryParameters) throws URISyntaxException, UnsupportedEncodingException { - final StringBuilder sb = new StringBuilder(serverURI); - sb.append(path); - if (queryParameters != null) { - int i = 0; - while (i < queryParameters.length) { - sb.append(i > 0 ? "&" : "?"); - final String key = queryParameters[i++]; - final String val = queryParameters[i++]; - sb.append(URLEncoder.encode(key, "UTF-8")); - sb.append("="); - sb.append(URLEncoder.encode(val, "UTF-8")); - } - } - return new BaseResource(new URI(sb.toString())); - } - - /** - * Process a typed value extracted from a successful response (in an - * endpoint-dependent way). - */ - public interface RequestDelegate<T> { - public void handleError(Exception e); - public void handleFailure(FxAccountClientRemoteException e); - public void handleSuccess(T result); - } - - /** - * Thin container for two cryptographic keys. - */ - public static class TwoKeys { - public final byte[] kA; - public final byte[] wrapkB; - public TwoKeys(byte[] kA, byte[] wrapkB) { - this.kA = kA; - this.wrapkB = wrapkB; - } - } - - protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); - } - - enum ResponseType { - JSON_ARRAY, - JSON_OBJECT - } - - /** - * Translate resource callbacks into request callbacks invoked on the provided - * executor. - * <p> - * Override <code>handleSuccess</code> to parse the body of the resource - * request and call the request callback. <code>handleSuccess</code> is - * invoked via the executor, so you don't need to delegate further. - */ - protected abstract class ResourceDelegate<T> extends BaseResourceDelegate { - - protected void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body) throws Exception { - throw new UnsupportedOperationException(); - } - - protected void handleSuccess(final int status, HttpResponse response, final JSONArray body) throws Exception { - throw new UnsupportedOperationException(); - } - - protected final RequestDelegate<T> delegate; - - protected final byte[] tokenId; - protected final byte[] reqHMACKey; - protected final SkewHandler skewHandler; - protected final ResponseType responseType; - - /** - * Create a delegate for an un-authenticated resource. - */ - public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType) { - this(resource, delegate, responseType, null, null); - } - - /** - * Create a delegate for a Hawk-authenticated resource. - * <p> - * Every Hawk request that encloses an entity (PATCH, POST, and PUT) will - * include the payload verification hash. - */ - public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType, final byte[] tokenId, final byte[] reqHMACKey) { - super(resource); - this.delegate = delegate; - this.reqHMACKey = reqHMACKey; - this.tokenId = tokenId; - this.skewHandler = SkewHandler.getSkewHandlerForResource(resource); - this.responseType = responseType; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - if (tokenId != null && reqHMACKey != null) { - // We always include the payload verification hash for FxA Hawk-authenticated requests. - final boolean includePayloadVerificationHash = true; - return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds()); - } - return super.getAuthHeaderProvider(); - } - - @Override - public String getUserAgent() { - return FxAccountConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(HttpResponse response) { - try { - final int status = validateResponse(response); - skewHandler.updateSkew(response, now()); - invokeHandleSuccess(status, response); - } catch (FxAccountClientRemoteException e) { - if (!skewHandler.updateSkew(response, now())) { - // If we couldn't update skew, but we got a failure, let's try clearing the skew. - skewHandler.resetSkew(); - } - invokeHandleFailure(e); - } - } - - protected void invokeHandleFailure(final FxAccountClientRemoteException e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(e); - } - }); - } - - protected void invokeHandleSuccess(final int status, final HttpResponse response) { - executor.execute(new Runnable() { - @Override - public void run() { - try { - SyncResponse syncResponse = new SyncResponse(response); - if (responseType == ResponseType.JSON_ARRAY) { - JSONArray body = syncResponse.jsonArrayBody(); - ResourceDelegate.this.handleSuccess(status, response, body); - } else { - ExtendedJSONObject body = syncResponse.jsonObjectBody(); - ResourceDelegate.this.handleSuccess(status, response, body); - } - } catch (Exception e) { - delegate.handleError(e); - } - } - }); - } - - @Override - public void handleHttpProtocolException(final ClientProtocolException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleHttpIOException(IOException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - invokeHandleError(delegate, e); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - super.addHeaders(request, client); - - // The basics. - final Locale locale = Locale.getDefault(); - request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale)); - request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER); - } - } - - protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody) { - if (requestBody == null) { - resource.post((HttpEntity) null); - } else { - resource.post(requestBody); - } - } - - @SuppressWarnings("static-method") - public long now() { - return System.currentTimeMillis(); - } - - /** - * Intepret a response from the auth server. - * <p> - * Throw an appropriate exception on errors; otherwise, return the response's - * status code. - * - * @return response's HTTP status code. - * @throws FxAccountClientException - */ - public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException { - final int status = response.getStatusLine().getStatusCode(); - if (status == 200) { - return status; - } - int code; - int errno; - String error; - String message; - String info; - ExtendedJSONObject body; - try { - body = new SyncStorageResponse(response).jsonObjectBody(); - body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class); - body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class); - code = body.getLong(JSON_KEY_CODE).intValue(); - errno = body.getLong(JSON_KEY_ERRNO).intValue(); - error = body.getString(JSON_KEY_ERROR); - message = body.getString(JSON_KEY_MESSAGE); - info = body.getString(JSON_KEY_INFO); - } catch (Exception e) { - throw new FxAccountClientMalformedResponseException(response); - } - throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body); - } - - /** - * Don't call this directly. Use <code>unbundleBody</code> instead. - */ - protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest) - throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException { - if (bundleBytes.length < 32) { - throw new IllegalArgumentException("input bundle must include HMAC"); - } - int len = respXORKey.length; - if (bundleBytes.length != len + 32) { - throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths"); - } - int left = len; - for (byte[] array : rest) { - left -= array.length; - } - if (left != 0) { - throw new IllegalArgumentException("XOR key and total output arrays have different lengths"); - } - - byte[] ciphertext = new byte[len]; - byte[] HMAC = new byte[32]; - System.arraycopy(bundleBytes, 0, ciphertext, 0, len); - System.arraycopy(bundleBytes, len, HMAC, 0, 32); - - Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey); - byte[] computedHMAC = hmacHasher.doFinal(ciphertext); - if (!Arrays.equals(computedHMAC, HMAC)) { - throw new FxAccountClientException("Bad message HMAC"); - } - - int offset = 0; - for (byte[] array : rest) { - for (int i = 0; i < array.length; i++) { - array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]); - } - offset += array.length; - } - } - - protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception { - int length = 0; - for (byte[] array : rest) { - length += array.length; - } - - if (body == null) { - throw new FxAccountClientException("body must be non-null"); - } - String bundle = body.getString("bundle"); - if (bundle == null) { - throw new FxAccountClientException("bundle must be a non-null string"); - } - byte[] bundleBytes = Utils.hex2Byte(bundle); - - final byte[] respHMACKey = new byte[32]; - final byte[] respXORKey = new byte[length]; - HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey); - unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest); - } - - public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = getBaseResource("account/keys"); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES]; - byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES]; - unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB); - delegate.handleSuccess(new TwoKeys(kA, wrapkB)); - } - }; - resource.get(); - } - - /** - * Thin container for account status response. - */ - public static class AccountStatusResponse { - public final boolean exists; - public AccountStatusResponse(boolean exists) { - this.exists = exists; - } - } - - /** - * Query the account status of an account given a uid. - * - * @param uid to query. - * @param delegate to invoke callbacks. - */ - public void accountStatus(String uid, final RequestDelegate<AccountStatusResponse> delegate) { - final BaseResource resource; - try { - final Map<String, String> params = new HashMap<>(1); - params.put("uid", uid); - resource = getBaseResource("account/status", params); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<AccountStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - boolean exists = body.getBoolean(JSON_KEY_EXISTS); - delegate.handleSuccess(new AccountStatusResponse(exists)); - } - }; - resource.get(); - } - - /** - * Thin container for recovery email status response. - */ - public static class RecoveryEmailStatusResponse { - public final String email; - public final boolean verified; - public RecoveryEmailStatusResponse(String email, boolean verified) { - this.email = email; - this.verified = verified; - } - } - - /** - * Query the recovery email status of an account given a valid session token. - * <p> - * This API is a little odd: the auth server returns the email and - * verification state of the account that corresponds to the (opaque) session - * token. It might fail if the session token is unknown (or invalid, or - * revoked). - * - * @param sessionToken - * to query. - * @param delegate - * to invoke callbacks. - */ - public void recoveryEmailStatus(byte[] sessionToken, final RequestDelegate<RecoveryEmailStatusResponse> delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = getBaseResource("recovery_email/status"); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<RecoveryEmailStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - String[] requiredStringFields = new String[] { JSON_KEY_EMAIL }; - body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class); - String email = body.getString(JSON_KEY_EMAIL); - Boolean verified = body.getBoolean(JSON_KEY_VERIFIED); - delegate.handleSuccess(new RecoveryEmailStatusResponse(email, verified)); - } - }; - resource.get(); - } - - @SuppressWarnings("unchecked") - public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInMilliseconds, final RequestDelegate<String> delegate) { - final ExtendedJSONObject body = new ExtendedJSONObject(); - body.put("publicKey", publicKey); - body.put("duration", durationInMilliseconds); - - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - try { - HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = getBaseResource("certificate/sign"); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<String>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - String cert = body.getString("cert"); - if (cert == null) { - delegate.handleError(new FxAccountClientException("cert must be a non-null string")); - return; - } - delegate.handleSuccess(cert); - } - }; - post(resource, body); - } - - protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN }; - protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, }; - protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED }; - - /** - * Thin container for login response. - * <p> - * The <code>remoteEmail</code> field is the email address as normalized by the - * server, and is <b>not necessarily</b> the email address delivered to the - * <code>login</code> or <code>create</code> call. - */ - public static class LoginResponse { - public final String remoteEmail; - public final String uid; - public final byte[] sessionToken; - public final boolean verified; - public final byte[] keyFetchToken; - - public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) { - this.remoteEmail = remoteEmail; - this.uid = uid; - this.verified = verified; - this.sessionToken = sessionToken; - this.keyFetchToken = keyFetchToken; - } - } - - // Public for testing only; prefer login and loginAndGetKeys (without boolean parameter). - public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, - final Map<String, String> queryParameters, - final RequestDelegate<LoginResponse> delegate) { - final BaseResource resource; - final ExtendedJSONObject body; - try { - final String path = "account/login"; - final Map<String, String> modifiedParameters = new HashMap<>(); - if (queryParameters != null) { - modifiedParameters.putAll(queryParameters); - } - if (getKeys) { - modifiedParameters.put("keys", "true"); - } - resource = getBaseResource(path, modifiedParameters); - body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody(); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS; - body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class); - - final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS; - body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class); - - String uid = body.getString(JSON_KEY_UID); - boolean verified = body.getBoolean(JSON_KEY_VERIFIED); - byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN)); - byte[] keyFetchToken = null; - if (getKeys) { - keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN)); - } - LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken); - - delegate.handleSuccess(loginResponse); - } - }; - - post(resource, body); - } - - public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, - final boolean getKeys, - final boolean preVerified, - final Map<String, String> queryParameters, - final RequestDelegate<LoginResponse> delegate) { - final BaseResource resource; - final ExtendedJSONObject body; - try { - final String path = "account/create"; - final Map<String, String> modifiedParameters = new HashMap<>(); - if (queryParameters != null) { - modifiedParameters.putAll(queryParameters); - } - if (getKeys) { - modifiedParameters.put("keys", "true"); - } - resource = getBaseResource(path, modifiedParameters); - body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody(); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - // This is very similar to login, except verified is not required. - resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception { - final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS; - body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class); - - String uid = body.getString(JSON_KEY_UID); - boolean verified = false; // In production, we're definitely not verified immediately upon creation. - Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED); - if (tempVerified != null) { - verified = tempVerified; - } - byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN)); - byte[] keyFetchToken = null; - if (getKeys) { - keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN)); - } - LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken); - - delegate.handleSuccess(loginResponse); - } - }; - - post(resource, body); - } - - /** - * We want users to be able to enter their email address case-insensitively. - * We stretch the password locally using the email address as a salt, to make - * dictionary attacks more expensive. This means that a client with a - * case-differing email address is unable to produce the correct - * authorization, even though it knows the password. In this case, the server - * returns the email that the account was created with, so that the client can - * re-stretch the password locally with the correct email salt. This version - * of <code>login</code> retries at most one time with a server provided email - * address. - * <p> - * Be aware that consumers will not see the initial error response from the - * server providing an alternate email (if there is one). - * - * @param emailUTF8 - * user entered email address. - * @param stretcher - * delegate to stretch and re-stretch password. - * @param getKeys - * true if a <code>keyFetchToken</code> should be returned (in - * addition to the standard <code>sessionToken</code>). - * @param queryParameters - * @param delegate - * to invoke callbacks. - */ - public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys, - final Map<String, String> queryParameters, - final RequestDelegate<LoginResponse> delegate) { - byte[] quickStretchedPW; - try { - FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" ); - quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8); - } catch (Exception e) { - delegate.handleError(e); - return; - } - - this.login(emailUTF8, quickStretchedPW, getKeys, queryParameters, new RequestDelegate<LoginResponse>() { - @Override - public void handleSuccess(LoginResponse result) { - delegate.handleSuccess(result); - } - - @Override - public void handleError(Exception e) { - delegate.handleError(e); - } - - @Override - public void handleFailure(FxAccountClientRemoteException e) { - String alternateEmail = e.body.getString(JSON_KEY_EMAIL); - if (!e.isBadEmailCase() || alternateEmail == null) { - delegate.handleFailure(e); - return; - }; - - Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email."); - FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" ); - - try { - // Nota bene: this is not recursive, since we call the fixed password - // signature here, which invokes a non-retrying version. - byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8"); - byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8); - login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate); - } catch (Exception innerException) { - delegate.handleError(innerException); - return; - } - } - }); - } - - /** - * Registers a device given a valid session token. - * - * @param sessionToken to query. - * @param delegate to invoke callbacks. - */ - @Override - public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - final BaseResource resource; - final ExtendedJSONObject body; - try { - resource = getBaseResource("account/device"); - body = device.toJson(); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<FxAccountDevice>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - delegate.handleSuccess(FxAccountDevice.fromJson(body)); - } catch (Exception e) { - delegate.handleError(e); - } - } - }; - - post(resource, body); - } - - @Override - public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - final BaseResource resource; - try { - resource = getBaseResource("account/devices"); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<FxAccountDevice[]>(resource, delegate, ResponseType.JSON_ARRAY, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, JSONArray devicesJson) { - try { - FxAccountDevice[] devices = new FxAccountDevice[devicesJson.size()]; - for (int i = 0; i < devices.length; i++) { - ExtendedJSONObject deviceJson = new ExtendedJSONObject((JSONObject) devicesJson.get(i)); - devices[i] = FxAccountDevice.fromJson(deviceJson); - } - delegate.handleSuccess(devices); - } catch (Exception e) { - delegate.handleError(e); - } - } - }; - - resource.get(); - } - - @Override - public void notifyDevices(@NonNull byte[] sessionToken, @NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - final BaseResource resource; - final ExtendedJSONObject body = createNotifyDevicesBody(deviceIds, payload, TTL); - try { - resource = getBaseResource("account/devices/notify"); - } catch (URISyntaxException | UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - delegate.handleSuccess(body); - } catch (Exception e) { - delegate.handleError(e); - } - } - }; - - post(resource, body); - } - - @NonNull - @SuppressWarnings("unchecked") - private ExtendedJSONObject createNotifyDevicesBody(@NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL) { - final ExtendedJSONObject body = new ExtendedJSONObject(); - final JSONArray to = new JSONArray(); - to.addAll(deviceIds); - body.put("to", to); - if (payload != null) { - body.put("payload", payload); - } - if (TTL != null) { - body.put("TTL", TTL); - } - return body; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java deleted file mode 100644 index 28ee5630e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClientException.java +++ /dev/null @@ -1,133 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpStatus; - -/** - * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>. - */ -public class FxAccountClientException extends Exception { - private static final long serialVersionUID = 7953459541558266597L; - - public FxAccountClientException(String detailMessage) { - super(detailMessage); - } - - public FxAccountClientException(Exception e) { - super(e); - } - - public static class FxAccountClientRemoteException extends FxAccountClientException { - private static final long serialVersionUID = 2209313149952001097L; - - public final HttpResponse response; - public final long httpStatusCode; - public final long apiErrorNumber; - public final String error; - public final String message; - public final String info; - public final ExtendedJSONObject body; - - public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info, ExtendedJSONObject body) { - super(new HTTPFailureException(new SyncStorageResponse(response))); - if (body == null) { - throw new IllegalArgumentException("body must not be null"); - } - this.response = response; - this.httpStatusCode = httpStatusCode; - this.apiErrorNumber = apiErrorNumber; - this.error = error; - this.message = message; - this.info = info; - this.body = body; - } - - @Override - public String toString() { - return "<FxAccountClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">"; - } - - public boolean isInvalidAuthentication() { - return httpStatusCode == HttpStatus.SC_UNAUTHORIZED; - } - - public boolean isAccountAlreadyExists() { - return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS; - } - - public boolean isAccountDoesNotExist() { - return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST; - } - - public boolean isBadPassword() { - return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD; - } - - public boolean isUnverified() { - return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT; - } - - public boolean isUpgradeRequired() { - return - apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED || - apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT || - apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT || - apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT; - } - - public boolean isTooManyRequests() { - return apiErrorNumber == FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS; - } - - public boolean isServerUnavailable() { - return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD; - } - - public boolean isBadEmailCase() { - return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE; - } - - public boolean isAccountLocked() { - return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED; - } - - public int getErrorMessageStringResource() { - if (isUpgradeRequired()) { - return R.string.fxaccount_remote_error_UPGRADE_REQUIRED; - } else if (isAccountAlreadyExists()) { - return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS; - } else if (isAccountDoesNotExist()) { - return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST; - } else if (isBadPassword()) { - return R.string.fxaccount_remote_error_INCORRECT_PASSWORD; - } else if (isUnverified()) { - return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT; - } else if (isTooManyRequests()) { - return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS; - } else if (isServerUnavailable()) { - return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD; - } else if (isAccountLocked()) { - return R.string.fxaccount_remote_error_ACCOUNT_LOCKED; - } else { - return R.string.fxaccount_remote_error_UNKNOWN_ERROR; - } - } - } - - public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException { - private static final long serialVersionUID = 2209313149952001098L; - - public FxAccountClientMalformedResponseException(HttpResponse response) { - super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed", new ExtendedJSONObject()); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java deleted file mode 100644 index 5a89561cb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountRemoteError.java +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -public interface FxAccountRemoteError { - public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101; - public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102; - public static final int INCORRECT_PASSWORD = 103; - public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104; - public static final int INVALID_VERIFICATION_CODE = 105; - public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106; - public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107; - public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108; - public static final int INVALID_REQUEST_SIGNATURE = 109; - public static final int INVALID_AUTHENTICATION_TOKEN = 110; - public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111; - public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112; - public static final int REQUEST_BODY_TOO_LARGE = 113; - public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114; - public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115; - public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116; - public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117; - public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118; - public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119; - public static final int INCORRECT_EMAIL_CASE = 120; - public static final int ACCOUNT_LOCKED = 121; - public static final int UNKNOWN_DEVICE = 123; - public static final int DEVICE_SESSION_CONFLICT = 124; - public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201; - public static final int UNKNOWN_ERROR = 999; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java deleted file mode 100644 index 2d29725a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountUtils.java +++ /dev/null @@ -1,217 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.nativecode.NativeCrypto; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.HKDF; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.crypto.PBKDF2; - -import android.content.Context; - -public class FxAccountUtils { - private static final String LOG_TAG = FxAccountUtils.class.getSimpleName(); - - public static final int SALT_LENGTH_BYTES = 32; - public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES; - - public static final int HASH_LENGTH_BYTES = 16; - public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES; - - public static final int CRYPTO_KEY_LENGTH_BYTES = 32; - public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES; - - public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/"; - - public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000; - - // For extra debugging. Not final so it can be changed from Fennec, or from - // an add-on. - public static boolean LOG_PERSONAL_INFORMATION = false; - - public static void pii(String tag, String message) { - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - Logger.info(tag, "$$FxA PII$$: " + message); - } - } - - public static String bytes(String string) throws UnsupportedEncodingException { - return Utils.byte2Hex(string.getBytes("UTF-8")); - } - - public static byte[] KW(String name) throws UnsupportedEncodingException { - return Utils.concatAll( - KW_VERSION_STRING.getBytes("UTF-8"), - name.getBytes("UTF-8")); - } - - public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException { - return Utils.concatAll( - KW_VERSION_STRING.getBytes("UTF-8"), - name.getBytes("UTF-8"), - ":".getBytes("UTF-8"), - emailUTF8); - } - - /** - * Calculate the SRP verifier <tt>x</tt> value. - */ - public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes)); - byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner)); - return new BigInteger(1, outer); - } - - /** - * Calculate the SRP verifier <tt>v</tt> value. - */ - public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes); - BigInteger v = g.modPow(x, N); - return v; - } - - /** - * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal). - * @param x to format. - * @param N modulus. - * @return x modulo N in hexadecimal. - */ - public static String hexModN(BigInteger x, BigInteger N) { - int byteLength = (N.bitLength() + 7) / 8; - int hexLength = 2 * byteLength; - return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength); - } - - /** - * The first engineering milestone of PICL (Profile-in-the-Cloud) was - * comprised of Sync 1.1 fronted by a Firefox Account. The sync key was - * generated from the Firefox Account password-derived kB value using this - * method. - */ - public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { - byte[] encryptionKey = new byte[32]; - byte[] hmacKey = new byte[32]; - byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32); - System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32); - System.arraycopy(derived, 1*32, hmacKey, 0, 1*32); - return new KeyBundle(encryptionKey, hmacKey); - } - - /** - * Firefox Accounts are password authenticated, but clients should not store - * the plain-text password for any amount of time. Equivalent, but slightly - * more secure, is the quickly client-side stretched password. - * <p> - * We separate this since multiple login-time operations want it, and the - * PBKDF2 operation is computationally expensive. - */ - public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException { - byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8); - try { - return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); - } catch (final LinkageError e) { - // This will throw UnsatisfiedLinkError (missing mozglue) the first time it is called, and - // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this - // is called; LinkageError is their common ancestor. - Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " + - "implementation; ignoring and using Java implementation.", e); - return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32); - } - } - - /** - * The password-derived credential used to authenticate to the Firefox Account - * auth server. - */ - public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { - return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32); - } - - /** - * The password-derived credential used to unwrap keys managed by the Firefox - * Account auth server. - */ - public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException { - return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32); - } - - public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) { - if (unwrapkB == null) { - throw new IllegalArgumentException("unwrapkB must not be null"); - } - if (wrapkB == null) { - throw new IllegalArgumentException("wrapkB must not be null"); - } - if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) { - throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long"); - } - byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES]; - for (int i = 0; i < wrapkB.length; i++) { - kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]); - } - return kB; - } - - /** - * The token server accepts an X-Client-State header, which is the - * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the - * bytes of kB. - * @param kB a byte array, expected to be 32 bytes long. - * @return a 32-character string. - * @throws NoSuchAlgorithmException - */ - public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException { - if (kB == null || - kB.length != 32) { - throw new IllegalArgumentException("Unexpected kB."); - } - byte[] sha256 = Utils.sha256(kB); - byte[] truncated = new byte[16]; - System.arraycopy(sha256, 0, truncated, 0, 16); - return Utils.byte2Hex(truncated); // This is automatically lowercase. - } - - /** - * Given an endpoint, calculate the corresponding BrowserID audience. - * <p> - * This is the domain, in web parlance. - * - * @param serverURI endpoint. - * @return BrowserID audience. - * @throws URISyntaxException - */ - public static String getAudienceForURL(String serverURI) throws URISyntaxException { - URI uri = new URI(serverURI); - return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null).toString(); - } - - public static String defaultClientName(Context context) { - String name = AppConstants.MOZ_APP_DISPLAYNAME; // The display name is never translated. - // Change "Firefox Aurora" or similar into "Aurora". - if (name.contains("Aurora")) { - name = "Aurora"; - } else if (name.contains("Beta")) { - name = "Beta"; - } else if (name.contains("Nightly")) { - name = "Nightly"; - } - return context.getResources().getString(R.string.sync_default_client_name, name, android.os.Build.MODEL); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java deleted file mode 100644 index 2debf3c77..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/PasswordStretcher.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; - -public interface PasswordStretcher { - public byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java deleted file mode 100644 index bf4b1bc97..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/QuickPasswordStretcher.java +++ /dev/null @@ -1,35 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.util.HashMap; -import java.util.Map; - -import org.mozilla.gecko.sync.Utils; - -public class QuickPasswordStretcher implements PasswordStretcher { - protected final String password; - protected final Map<String, String> cache = new HashMap<String, String>(); - - public QuickPasswordStretcher(String password) { - this.password = password; - } - - @Override - public synchronized byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException { - if (emailUTF8 == null) { - throw new IllegalArgumentException("emailUTF8 must not be null"); - } - String key = Utils.byte2Hex(emailUTF8); - if (!cache.containsKey(key)) { - byte[] value = FxAccountUtils.generateQuickStretchedPW(emailUTF8, password.getBytes("UTF-8")); - cache.put(key, Utils.byte2Hex(value)); - return value; - } - return Utils.hex2Byte(cache.get(key)); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java deleted file mode 100644 index 9d0ad5e03..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/SkewHandler.java +++ /dev/null @@ -1,111 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.net.Resource; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.impl.cookie.DateParseException; -import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; - -public class SkewHandler { - private static final String LOG_TAG = "SkewHandler"; - protected volatile long skewMillis = 0L; - protected final String hostname; - - private static final HashMap<String, SkewHandler> skewHandlers = new HashMap<String, SkewHandler>(); - - public static SkewHandler getSkewHandlerForResource(final Resource resource) { - return getSkewHandlerForHostname(resource.getHostname()); - } - - public static SkewHandler getSkewHandlerFromEndpointString(final String url) throws URISyntaxException { - if (url == null) { - throw new IllegalArgumentException("url must not be null."); - } - URI u = new URI(url); - return getSkewHandlerForHostname(u.getHost()); - } - - public static synchronized SkewHandler getSkewHandlerForHostname(final String hostname) { - SkewHandler handler = skewHandlers.get(hostname); - if (handler == null) { - handler = new SkewHandler(hostname); - skewHandlers.put(hostname, handler); - } - return handler; - } - - public static synchronized void clearSkewHandlers() { - skewHandlers.clear(); - } - - public SkewHandler(final String hostname) { - this.hostname = hostname; - } - - public boolean updateSkewFromServerMillis(long millis, long now) { - skewMillis = millis - now; - Logger.debug(LOG_TAG, "Updated skew: " + skewMillis + "ms for hostname " + this.hostname); - return true; - } - - public boolean updateSkewFromHTTPDateString(String date, long now) { - try { - final long millis = DateUtils.parseDate(date).getTime(); - return updateSkewFromServerMillis(millis, now); - } catch (DateParseException e) { - Logger.warn(LOG_TAG, "Unexpected: invalid Date header from " + this.hostname); - return false; - } - } - - public boolean updateSkewFromDateHeader(Header header, long now) { - String date = header.getValue(); - if (null == date) { - Logger.warn(LOG_TAG, "Unexpected: null Date header from " + this.hostname); - return false; - } - return updateSkewFromHTTPDateString(date, now); - } - - /** - * Update our tracked skew value to account for the local clock differing from - * the server's. - * - * @param response - * the received HTTP response. - * @param now - * the current time in milliseconds. - * @return true if the skew value was updated, false otherwise. - */ - public boolean updateSkew(HttpResponse response, long now) { - Header header = response.getFirstHeader(HttpHeaders.DATE); - if (null == header) { - Logger.warn(LOG_TAG, "Unexpected: missing Date header from " + this.hostname); - return false; - } - return updateSkewFromDateHeader(header, now); - } - - public long getSkewInMillis() { - return skewMillis; - } - - public long getSkewInSeconds() { - return skewMillis / 1000; - } - - public void resetSkew() { - skewMillis = 0L; - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java deleted file mode 100644 index 4bdaa6690..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClient.java +++ /dev/null @@ -1,224 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa.oauth; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.Locale; -import java.util.concurrent.Executor; - -import org.mozilla.gecko.background.fxa.FxAccountClientException; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientMalformedResponseException; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.Resource; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -public abstract class FxAccountAbstractClient { - protected static final String LOG_TAG = FxAccountAbstractClient.class.getSimpleName(); - - protected static final String ACCEPT_HEADER = "application/json;charset=utf-8"; - protected static final String AUTHORIZATION_RESPONSE_TYPE = "token"; - - public static final String JSON_KEY_ERROR = "error"; - public static final String JSON_KEY_MESSAGE = "message"; - public static final String JSON_KEY_CODE = "code"; - public static final String JSON_KEY_ERRNO = "errno"; - - protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE }; - protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO }; - - /** - * The server's URI. - * <p> - * We assume throughout that this ends with a trailing slash (and guarantee as - * much in the constructor). - */ - protected final String serverURI; - - protected final Executor executor; - - public FxAccountAbstractClient(String serverURI, Executor executor) { - if (serverURI == null) { - throw new IllegalArgumentException("Must provide a server URI."); - } - if (executor == null) { - throw new IllegalArgumentException("Must provide a non-null executor."); - } - this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; - if (!this.serverURI.endsWith("/")) { - throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI); - } - this.executor = executor; - } - - /** - * Process a typed value extracted from a successful response (in an - * endpoint-dependent way). - */ - public interface RequestDelegate<T> { - public void handleError(Exception e); - public void handleFailure(FxAccountAbstractClientRemoteException e); - public void handleSuccess(T result); - } - - /** - * Intepret a response from the auth server. - * <p> - * Throw an appropriate exception on errors; otherwise, return the response's - * status code. - * - * @return response's HTTP status code. - * @throws FxAccountClientException - */ - public static int validateResponse(HttpResponse response) throws FxAccountAbstractClientRemoteException { - final int status = response.getStatusLine().getStatusCode(); - if (status == 200) { - return status; - } - int code; - int errno; - String error; - String message; - ExtendedJSONObject body; - try { - body = new SyncStorageResponse(response).jsonObjectBody(); - body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class); - body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class); - code = body.getLong(JSON_KEY_CODE).intValue(); - errno = body.getLong(JSON_KEY_ERRNO).intValue(); - error = body.getString(JSON_KEY_ERROR); - message = body.getString(JSON_KEY_MESSAGE); - } catch (Exception e) { - throw new FxAccountAbstractClientMalformedResponseException(response); - } - throw new FxAccountAbstractClientRemoteException(response, code, errno, error, message, body); - } - - protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); - } - - protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) { - try { - if (requestBody == null) { - resource.post((HttpEntity) null); - } else { - resource.post(requestBody); - } - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - } - - /** - * Translate resource callbacks into request callbacks invoked on the provided - * executor. - * <p> - * Override <code>handleSuccess</code> to parse the body of the resource - * request and call the request callback. <code>handleSuccess</code> is - * invoked via the executor, so you don't need to delegate further. - */ - protected abstract class ResourceDelegate<T> extends BaseResourceDelegate { - protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body); - - protected final RequestDelegate<T> delegate; - - /** - * Create a delegate for an un-authenticated resource. - */ - public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) { - super(resource); - this.delegate = delegate; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return super.getAuthHeaderProvider(); - } - - @Override - public String getUserAgent() { - return FxAccountConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(HttpResponse response) { - try { - final int status = validateResponse(response); - invokeHandleSuccess(status, response); - } catch (FxAccountAbstractClientRemoteException e) { - invokeHandleFailure(e); - } - } - - protected void invokeHandleFailure(final FxAccountAbstractClientRemoteException e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(e); - } - }); - } - - protected void invokeHandleSuccess(final int status, final HttpResponse response) { - executor.execute(new Runnable() { - @Override - public void run() { - try { - ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody(); - ResourceDelegate.this.handleSuccess(status, response, body); - } catch (Exception e) { - delegate.handleError(e); - } - } - }); - } - - @Override - public void handleHttpProtocolException(final ClientProtocolException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleHttpIOException(IOException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - invokeHandleError(delegate, e); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - super.addHeaders(request, client); - - // The basics. - final Locale locale = Locale.getDefault(); - request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale)); - request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java deleted file mode 100644 index 21025af0a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountAbstractClientException.java +++ /dev/null @@ -1,68 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa.oauth; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpStatus; - -/** - * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>. - */ -public class FxAccountAbstractClientException extends Exception { - private static final long serialVersionUID = 1953459541558266597L; - - public FxAccountAbstractClientException(String detailMessage) { - super(detailMessage); - } - - public FxAccountAbstractClientException(Exception e) { - super(e); - } - - public static class FxAccountAbstractClientRemoteException extends FxAccountAbstractClientException { - private static final long serialVersionUID = 1209313149952001097L; - - public final HttpResponse response; - public final long httpStatusCode; - public final long apiErrorNumber; - public final String error; - public final String message; - public final ExtendedJSONObject body; - - public FxAccountAbstractClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) { - super(new HTTPFailureException(new SyncStorageResponse(response))); - if (body == null) { - throw new IllegalArgumentException("body must not be null"); - } - this.response = response; - this.httpStatusCode = httpStatusCode; - this.apiErrorNumber = apiErrorNumber; - this.error = error; - this.message = message; - this.body = body; - } - - @Override - public String toString() { - return "<FxAccountAbstractClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">"; - } - - public boolean isInvalidAuthentication() { - return this.httpStatusCode == HttpStatus.SC_UNAUTHORIZED; - } - } - - public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException { - private static final long serialVersionUID = 1209313149952001098L; - - public FxAccountAbstractClientMalformedResponseException(HttpResponse response) { - super(response, 0, FxAccountOAuthRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", new ExtendedJSONObject()); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java deleted file mode 100644 index 4f233695b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthClient10.java +++ /dev/null @@ -1,129 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa.oauth; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.Executor; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.BaseResource; - -import ch.boye.httpclientandroidlib.HttpResponse; - -/** - * Talk to an fxa-oauth-server to get "implicitly granted" OAuth tokens. - * <p> - * To use this client, you will need a pre-allocated fxa-oauth-server - * "client_id" with special "implicit grant" permissions. - * <p> - * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md">https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md</a>. - */ -public class FxAccountOAuthClient10 extends FxAccountAbstractClient { - protected static final String LOG_TAG = FxAccountOAuthClient10.class.getSimpleName(); - - protected static final String AUTHORIZATION_RESPONSE_TYPE = "token"; - - protected static final String JSON_KEY_ACCESS_TOKEN = "access_token"; - protected static final String JSON_KEY_ASSERTION = "assertion"; - protected static final String JSON_KEY_CLIENT_ID = "client_id"; - protected static final String JSON_KEY_RESPONSE_TYPE = "response_type"; - protected static final String JSON_KEY_SCOPE = "scope"; - protected static final String JSON_KEY_STATE = "state"; - protected static final String JSON_KEY_TOKEN = "token"; - protected static final String JSON_KEY_TOKEN_TYPE = "token_type"; - - // access_token: A string that can be used for authorized requests to service providers. - // scope: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions. - // token_type: A string representing the token type. Currently will always be "bearer". - protected static final String[] AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_ACCESS_TOKEN, JSON_KEY_SCOPE, JSON_KEY_TOKEN_TYPE }; - - public FxAccountOAuthClient10(String serverURI, Executor executor) { - super(serverURI, executor); - } - - /** - * Thin container for an authorization response. - */ - public static class AuthorizationResponse { - public final String access_token; - public final String token_type; - public final String scope; - - public AuthorizationResponse(String access_token, String token_type, String scope) { - this.access_token = access_token; - this.token_type = token_type; - this.scope = scope; - } - } - - public void authorization(String client_id, String assertion, String state, String scope, - RequestDelegate<AuthorizationResponse> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "authorization")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<AuthorizationResponse>(resource, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - body.throwIfFieldsMissingOrMisTyped(AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS, String.class); - String access_token = body.getString(JSON_KEY_ACCESS_TOKEN); - String token_type = body.getString(JSON_KEY_TOKEN_TYPE); - String scope = body.getString(JSON_KEY_SCOPE); - delegate.handleSuccess(new AuthorizationResponse(access_token, token_type, scope)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - final ExtendedJSONObject requestBody = new ExtendedJSONObject(); - requestBody.put(JSON_KEY_RESPONSE_TYPE, AUTHORIZATION_RESPONSE_TYPE); - requestBody.put(JSON_KEY_CLIENT_ID, client_id); - requestBody.put(JSON_KEY_ASSERTION, assertion); - if (scope != null) { - requestBody.put(JSON_KEY_SCOPE, scope); - } - if (state != null) { - requestBody.put(JSON_KEY_STATE, state); - } - - post(resource, requestBody, delegate); - } - - public void deleteToken(final String token, final RequestDelegate<Void> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "destroy")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<Void>(resource, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - delegate.handleSuccess(null); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - final ExtendedJSONObject requestBody = new ExtendedJSONObject(); - requestBody.put(JSON_KEY_TOKEN, token); - post(resource, requestBody, delegate); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java deleted file mode 100644 index d949d316b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/oauth/FxAccountOAuthRemoteError.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa.oauth; - -public interface FxAccountOAuthRemoteError { - public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101; - public static final int UNKNOWN_CLIENT_ID = 101; - public static final int INCORRECT_CLIENT_SECRET = 102; - public static final int REDIRECT_URI_DOES_NOT_MATCH_REGISTERED_VALUE = 103; - public static final int INVALID_FXA_ASSERTION = 104; - public static final int UNKNOWN_CODE = 105; - public static final int INCORRECT_CODE = 106; - public static final int EXPIRED_CODE = 107; - public static final int INVALID_TOKEN = 108; - public static final int INVALID_REQUEST_PARAMETER = 109; - public static final int UNKNOWN_ERROR = 999; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java deleted file mode 100644 index cb851a8db..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/profile/FxAccountProfileClient10.java +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.fxa.profile; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.Executor; - -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider; - -import ch.boye.httpclientandroidlib.HttpResponse; - - -/** - * Talk to an fxa-profile-server to get profile information like name, age, gender, and avatar image. - * <p> - * This client was written against the API documented at <a href="https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md</a>. - */ -public class FxAccountProfileClient10 extends FxAccountAbstractClient { - public FxAccountProfileClient10(String serverURI, Executor executor) { - super(serverURI, executor); - } - - public void profile(final String token, RequestDelegate<ExtendedJSONObject> delegate) { - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "profile")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate) { - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return new BearerAuthHeaderProvider(token); - } - - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - delegate.handleSuccess(body); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - resource.get(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java deleted file mode 100644 index 25f0f84d9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/nativecode/NativeCrypto.java +++ /dev/null @@ -1,60 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.background.nativecode; - -import java.security.GeneralSecurityException; - -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.AppConstants; - -import android.util.Log; - -@RobocopTarget -public class NativeCrypto { - static { - try { - System.loadLibrary("mozglue"); - } catch (UnsatisfiedLinkError e) { - Log.wtf("NativeCrypto", "Couldn't load mozglue. Trying /data/app-lib path."); - try { - System.load("/data/app-lib/" + AppConstants.ANDROID_PACKAGE_NAME + "/libmozglue.so"); - } catch (Throwable ee) { - try { - Log.wtf("NativeCrypto", "Couldn't load mozglue: " + ee + ". Trying /data/data path."); - System.load("/data/data/" + AppConstants.ANDROID_PACKAGE_NAME + "/lib/libmozglue.so"); - } catch (UnsatisfiedLinkError eee) { - Log.wtf("NativeCrypto", "Failed every attempt to load mozglue. Giving up."); - throw new RuntimeException("Unable to load mozglue", eee); - } - } - } - } - - /** - * Wrapper to perform PBKDF2-HMAC-SHA-256 in native code. - */ - public native static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen) - throws GeneralSecurityException; - - /** - * Wrapper to perform SHA-1 in native code. - */ - public native static byte[] sha1(byte[] str); - - /** - * Wrapper to perform SHA-256 init in native code. Returns a SHA-256 context. - */ - public native static byte[] sha256init(); - - /** - * Wrapper to update a SHA-256 context in native code. - */ - public native static void sha256update(byte[] ctx, byte[] str, int len); - - /** - * Wrapper to finalize a SHA-256 context in native code. Returns digest. - */ - public native static byte[] sha256finalize(byte[] ctx); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java deleted file mode 100644 index 5bc5422c8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceFragment.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.background.preferences; - -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.WeakReferenceHandler; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.preference.Preference; -import android.preference.PreferenceGroup; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.support.v4.app.Fragment; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnKeyListener; -import android.view.ViewGroup; -import android.widget.ListView; - -public abstract class PreferenceFragment extends Fragment implements PreferenceManagerCompat.OnPreferenceTreeClickListener { - private static final String PREFERENCES_TAG = "android:preferences"; - - private PreferenceManager mPreferenceManager; - private ListView mList; - private boolean mHavePrefs; - private boolean mInitDone; - - /** - * The starting request code given out to preference framework. - */ - private static final int FIRST_REQUEST_CODE = 100; - - private static final int MSG_BIND_PREFERENCES = 1; - - private static class PreferenceFragmentHandler extends WeakReferenceHandler<PreferenceFragment> { - public PreferenceFragmentHandler(final PreferenceFragment that) { - super(that); - } - - @Override - public void handleMessage(Message msg) { - final PreferenceFragment that = mTarget.get(); - if (that == null) { - return; - } - - switch (msg.what) { - - case MSG_BIND_PREFERENCES: - that.bindPreferences(); - break; - } - } - } - - private final Handler mHandler = new PreferenceFragmentHandler(this); - - final private Runnable mRequestFocus = new Runnable() { - @Override - public void run() { - mList.focusableViewAvailable(mList); - } - }; - - /** - * Interface that PreferenceFragment's containing activity should - * implement to be able to process preference items that wish to - * switch to a new fragment. - */ - public interface OnPreferenceStartFragmentCallback { - /** - * Called when the user has clicked on a Preference that has - * a fragment class name associated with it. The implementation - * to should instantiate and switch to an instance of the given - * fragment. - */ - boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref); - } - - @Override - public void onCreate(Bundle paramBundle) { - super.onCreate(paramBundle); - mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE); - PreferenceManagerCompat.setFragment(mPreferenceManager, this); - } - - @Override - public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) { - return paramLayoutInflater.inflate(R.layout.fxaccount_preference_list_fragment, paramViewGroup, - false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (mHavePrefs) { - bindPreferences(); - } - - mInitDone = true; - - if (savedInstanceState != null) { - Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG); - if (container != null) { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - preferenceScreen.restoreHierarchyState(container); - } - } - } - } - - @Override - public void onStart() { - super.onStart(); - PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this); - } - - @Override - public void onStop() { - super.onStop(); - PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager); - PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null); - } - - @Override - public void onDestroyView() { - mList = null; - mHandler.removeCallbacks(mRequestFocus); - mHandler.removeMessages(MSG_BIND_PREFERENCES); - super.onDestroyView(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - Bundle container = new Bundle(); - preferenceScreen.saveHierarchyState(container); - outState.putBundle(PREFERENCES_TAG, container); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data); - } - - /** - * Returns the {@link PreferenceManager} used by this fragment. - * @return The {@link PreferenceManager}. - */ - public PreferenceManager getPreferenceManager() { - return mPreferenceManager; - } - - /** - * Sets the root of the preference hierarchy that this fragment is showing. - * - * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. - */ - public void setPreferenceScreen(PreferenceScreen preferenceScreen) { - if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) { - mHavePrefs = true; - if (mInitDone) { - postBindPreferences(); - } - } - } - - /** - * Gets the root of the preference hierarchy that this fragment is showing. - * - * @return The {@link PreferenceScreen} that is the root of the preference - * hierarchy. - */ - public PreferenceScreen getPreferenceScreen() { - return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager); - } - - /** - * Adds preferences from activities that match the given {@link Intent}. - * - * @param intent The {@link Intent} to query activities. - */ - public void addPreferencesFromIntent(Intent intent) { - requirePreferenceManager(); - - setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen())); - } - - /** - * Inflates the given XML resource and adds the preference hierarchy to the current - * preference hierarchy. - * - * @param preferencesResId The XML resource ID to inflate. - */ - public void addPreferencesFromResource(int preferencesResId) { - requirePreferenceManager(); - - setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(), - preferencesResId, getPreferenceScreen())); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { - //if (preference.getFragment() != null && - if ( - getActivity() instanceof OnPreferenceStartFragmentCallback) { - return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment( - this, preference); - } - return false; - } - - /** - * Finds a {@link Preference} based on its key. - * - * @param key The key of the preference to retrieve. - * @return The {@link Preference} with the key, or null. - * @see PreferenceGroup#findPreference(CharSequence) - */ - public Preference findPreference(CharSequence key) { - if (mPreferenceManager == null) { - return null; - } - return mPreferenceManager.findPreference(key); - } - - private void requirePreferenceManager() { - if (mPreferenceManager == null) { - throw new RuntimeException("This should be called after super.onCreate."); - } - } - - private void postBindPreferences() { - if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; - mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); - } - - private void bindPreferences() { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - if (preferenceScreen != null) { - preferenceScreen.bind(getListView()); - } - } - - public ListView getListView() { - ensureList(); - return mList; - } - - private void ensureList() { - if (mList != null) { - return; - } - View root = getView(); - if (root == null) { - throw new IllegalStateException("Content view not yet created"); - } - View rawListView = root.findViewById(android.R.id.list); - if (!(rawListView instanceof ListView)) { - throw new RuntimeException( - "Content has view with id attribute 'android.R.id.list' " - + "that is not a ListView class"); - } - mList = (ListView)rawListView; - if (mList == null) { - throw new RuntimeException( - "Your content must have a ListView whose id attribute is " + - "'android.R.id.list'"); - } - mList.setOnKeyListener(mListOnKeyListener); - mHandler.post(mRequestFocus); - } - - private final OnKeyListener mListOnKeyListener = new OnKeyListener() { - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - Object selectedItem = mList.getSelectedItem(); - if (selectedItem instanceof Preference) { - @SuppressWarnings("unused") - View selectedView = mList.getSelectedView(); - //return ((Preference)selectedItem).onKey( - // selectedView, keyCode, event); - return false; - } - return false; - } - - }; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java b/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java deleted file mode 100644 index 22c62e431..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/preferences/PreferenceManagerCompat.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.gecko.background.preferences; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.preference.Preference; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.util.Log; - -public class PreferenceManagerCompat { - - private static final String TAG = PreferenceManagerCompat.class.getSimpleName(); - - /** - * Interface definition for a callback to be invoked when a {@link Preference} in the hierarchy - * rooted at this {@link PreferenceScreen} is clicked. - */ - interface OnPreferenceTreeClickListener { - /** - * Called when a preference in the tree rooted at this {@link PreferenceScreen} has been - * clicked. - * - * @param preferenceScreen The {@link PreferenceScreen} that the preference is located in. - * @param preference The preference that was clicked. - * - * @return Whether the click was handled. - */ - boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference); - } - - static PreferenceManager newInstance(Activity activity, int firstRequestCode) { - try { - Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class); - c.setAccessible(true); - return c.newInstance(activity, firstRequestCode); - } catch (Exception e) { - Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e); - } - return null; - } - - /** - * Sets the owning preference fragment - */ - static void setFragment(PreferenceManager manager, PreferenceFragment fragment) { - // stub - } - - /** - * Sets the callback to be invoked when a {@link Preference} in the hierarchy rooted at this - * {@link PreferenceManager} is clicked. - * - * @param listener The callback to be invoked. - */ - static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) { - try { - Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener"); - onPreferenceTreeClickListener.setAccessible(true); - if (listener != null) { - Object proxy = Proxy.newProxyInstance( - onPreferenceTreeClickListener.getType().getClassLoader(), - new Class<?>[] { onPreferenceTreeClickListener.getType() }, - new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) { - if (method.getName().equals("onPreferenceTreeClick")) { - return listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]); - } else { - return null; - } - } - }); - onPreferenceTreeClickListener.set(manager, proxy); - } else { - onPreferenceTreeClickListener.set(manager, null); - } - } catch (Exception e) { - Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e); - } - } - - /** - * Inflates a preference hierarchy from the preference hierarchies of {@link Activity Activities} - * that match the given {@link Intent}. An {@link Activity} defines its preference hierarchy with - * meta-data using the {@link #METADATA_KEY_PREFERENCES} key. - * <p/> - * If a preference hierarchy is given, the new preference hierarchies will be merged in. - * - * @param queryIntent The intent to match activities. - * @param rootPreferences Optional existing hierarchy to merge the new hierarchies into. - * - * @return The root hierarchy (if one was not provided, the new hierarchy's root). - */ - static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class); - m.setAccessible(true); - PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen); - return prefScreen; - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e); - } - return null; - } - - /** - * Inflates a preference hierarchy from XML. If a preference hierarchy is given, the new - * preference hierarchies will be merged in. - * - * @param context The context of the resource. - * @param resId The resource ID of the XML to inflate. - * @param rootPreferences Optional existing hierarchy to merge the new hierarchies into. - * - * @return The root hierarchy (if one was not provided, the new hierarchy's root). - * - * @hide - */ - static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class); - m.setAccessible(true); - PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen); - return prefScreen; - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e); - } - return null; - } - - /** - * Returns the root of the preference hierarchy managed by this class. - * - * @return The {@link PreferenceScreen} object that is at the root of the hierarchy. - */ - static PreferenceScreen getPreferenceScreen(PreferenceManager manager) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen"); - m.setAccessible(true); - return (PreferenceScreen) m.invoke(manager); - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e); - } - return null; - } - - /** - * Called by the {@link PreferenceManager} to dispatch a subactivity result. - */ - static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class); - m.setAccessible(true); - m.invoke(manager, requestCode, resultCode, data); - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e); - } - } - - /** - * Called by the {@link PreferenceManager} to dispatch the activity stop event. - */ - static void dispatchActivityStop(PreferenceManager manager) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop"); - m.setAccessible(true); - m.invoke(manager); - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e); - } - } - - /** - * Called by the {@link PreferenceManager} to dispatch the activity destroy event. - */ - static void dispatchActivityDestroy(PreferenceManager manager) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy"); - m.setAccessible(true); - m.invoke(manager); - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e); - } - } - - /** - * Sets the root of the preference hierarchy. - * - * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. - * - * @return Whether the {@link PreferenceScreen} given is different than the previous. - */ - static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) { - try { - Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class); - m.setAccessible(true); - return ((Boolean) m.invoke(manager, screen)); - } catch (Exception e) { - Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e); - } - return false; - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java deleted file mode 100644 index b032067c5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/ASNUtils.java +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - - -/** - * Java produces signature in ASN.1 format. Here's some hard-coded encoding and decoding - * code, courtesy of a comment in - * <a href="http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array">http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array</a>. - */ -public class ASNUtils { - /** - * Decode two short arrays from ASN.1 bytes. - * @param input to extract. - * @return length 2 array of byte arrays. - */ - public static byte[][] decodeTwoArraysFromASN1(byte[] input) throws IllegalArgumentException { - if (input == null) { - throw new IllegalArgumentException("input must not be null"); - } - if (input.length <= 3) - throw new IllegalArgumentException("bad length"); - if (input[0] != 0x30) - throw new IllegalArgumentException("bad encoding"); - if ((input[1] & ((byte) 0x80)) != 0) - throw new IllegalArgumentException("bad length encoding"); - if (input[2] != 0x02) - throw new IllegalArgumentException("bad encoding"); - if ((input[3] & ((byte) 0x80)) != 0) - throw new IllegalArgumentException("bad length encoding"); - byte rLength = input[3]; - if (input.length <= 5 + rLength) - throw new IllegalArgumentException("bad length"); - if (input[4 + rLength] != 0x02) - throw new IllegalArgumentException("bad encoding"); - if ((input[5 + rLength] & (byte) 0x80) !=0) - throw new IllegalArgumentException("bad length encoding"); - byte sLength = input[5 + rLength]; - if (input.length != 6 + sLength + rLength) - throw new IllegalArgumentException("bad length"); - byte[] rArr = new byte[rLength]; - byte[] sArr = new byte[sLength]; - System.arraycopy(input, 4, rArr, 0, rLength); - System.arraycopy(input, 6 + rLength, sArr, 0, sLength); - return new byte[][] { rArr, sArr }; - } - - /** - * Encode two short arrays into ASN.1 bytes. - * @param first array to encode. - * @param second array to encode. - * @return array. - */ - public static byte[] encodeTwoArraysToASN1(byte[] first, byte[] second) throws IllegalArgumentException { - if (first == null) { - throw new IllegalArgumentException("first must not be null"); - } - if (second == null) { - throw new IllegalArgumentException("second must not be null"); - } - byte[] output = new byte[6 + first.length + second.length]; - output[0] = 0x30; - if (4 + first.length + second.length > 255) - throw new IllegalArgumentException("bad length"); - output[1] = (byte) (4 + first.length + second.length); - if ((output[1] & ((byte) 0x80)) != 0) - throw new IllegalArgumentException("bad length encoding"); - output[2] = 0x02; - output[3] = (byte) first.length; - if ((output[3] & ((byte) 0x80)) != 0) - throw new IllegalArgumentException("bad length encoding"); - System.arraycopy(first, 0, output, 4, first.length); - output[4 + first.length] = 0x02; - output[5 + first.length] = (byte) second.length; - if ((output[5 + first.length] & ((byte) 0x80)) != 0) - throw new IllegalArgumentException("bad length encoding"); - System.arraycopy(second, 0, output, 6 + first.length, second.length); - return output; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java deleted file mode 100644 index 7283a0299..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/BrowserIDKeyPair.java +++ /dev/null @@ -1,35 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public class BrowserIDKeyPair { - public static final String JSON_KEY_PRIVATEKEY = "privateKey"; - public static final String JSON_KEY_PUBLICKEY = "publicKey"; - - protected final SigningPrivateKey privateKey; - protected final VerifyingPublicKey publicKey; - - public BrowserIDKeyPair(SigningPrivateKey privateKey, VerifyingPublicKey publicKey) { - this.privateKey = privateKey; - this.publicKey = publicKey; - } - - public SigningPrivateKey getPrivate() { - return this.privateKey; - } - - public VerifyingPublicKey getPublic() { - return this.publicKey; - } - - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put(JSON_KEY_PRIVATEKEY, privateKey.toJSONObject()); - o.put(JSON_KEY_PUBLICKEY, publicKey.toJSONObject()); - return o; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java deleted file mode 100644 index a04a89c8e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/DSACryptoImplementation.java +++ /dev/null @@ -1,255 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import android.annotation.SuppressLint; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.util.PRNGFixes; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.interfaces.DSAParams; -import java.security.interfaces.DSAPrivateKey; -import java.security.interfaces.DSAPublicKey; -import java.security.spec.DSAPrivateKeySpec; -import java.security.spec.DSAPublicKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; - -public class DSACryptoImplementation { - private static final String LOG_TAG = DSACryptoImplementation.class.getSimpleName(); - - public static final String SIGNATURE_ALGORITHM = "SHA1withDSA"; - public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long. - - /** - * Parameters are serialized as hex strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. We - * expect to follow the JOSE/JWT spec as it solidifies, and that will probably - * mean unifying this base. - */ - protected static final int SERIALIZATION_BASE = 16; - - protected static class DSAVerifyingPublicKey implements VerifyingPublicKey { - protected final DSAPublicKey publicKey; - - public DSAVerifyingPublicKey(DSAPublicKey publicKey) { - this.publicKey = publicKey; - } - - /** - * Serialize to a JSON object. - * <p> - * Parameters are serialized as hex strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. - */ - @Override - public ExtendedJSONObject toJSONObject() { - DSAParams params = publicKey.getParams(); - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("algorithm", "DS"); - o.put("y", publicKey.getY().toString(SERIALIZATION_BASE)); - o.put("g", params.getG().toString(SERIALIZATION_BASE)); - o.put("p", params.getP().toString(SERIALIZATION_BASE)); - o.put("q", params.getQ().toString(SERIALIZATION_BASE)); - return o; - } - - @Override - public boolean verifyMessage(byte[] bytes, byte[] signature) - throws GeneralSecurityException { - if (bytes == null) { - throw new IllegalArgumentException("bytes must not be null"); - } - if (signature == null) { - throw new IllegalArgumentException("signature must not be null"); - } - if (signature.length != SIGNATURE_LENGTH_BYTES) { - return false; - } - byte[] first = new byte[signature.length / 2]; - byte[] second = new byte[signature.length / 2]; - System.arraycopy(signature, 0, first, 0, first.length); - System.arraycopy(signature, first.length, second, 0, second.length); - BigInteger r = new BigInteger(Utils.byte2Hex(first), 16); - BigInteger s = new BigInteger(Utils.byte2Hex(second), 16); - // This is awful, but encoding an extra 0 byte works better on devices. - byte[] encoded = ASNUtils.encodeTwoArraysToASN1( - Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2), - Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2)); - - final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); - signer.initVerify(publicKey); - signer.update(bytes); - return signer.verify(encoded); - } - } - - protected static class DSASigningPrivateKey implements SigningPrivateKey { - protected final DSAPrivateKey privateKey; - - public DSASigningPrivateKey(DSAPrivateKey privateKey) { - this.privateKey = privateKey; - } - - @Override - public String getAlgorithm() { - return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8; - } - - /** - * Serialize to a JSON object. - * <p> - * Parameters are serialized as decimal strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. - */ - @Override - public ExtendedJSONObject toJSONObject() { - DSAParams params = privateKey.getParams(); - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("algorithm", "DS"); - o.put("x", privateKey.getX().toString(SERIALIZATION_BASE)); - o.put("g", params.getG().toString(SERIALIZATION_BASE)); - o.put("p", params.getP().toString(SERIALIZATION_BASE)); - o.put("q", params.getQ().toString(SERIALIZATION_BASE)); - return o; - } - - @SuppressLint("TrulyRandom") - @Override - public byte[] signMessage(byte[] bytes) - throws GeneralSecurityException { - if (bytes == null) { - throw new IllegalArgumentException("bytes must not be null"); - } - - try { - PRNGFixes.apply(); - } catch (Exception e) { - // Not much to be done here: it was weak before, and we couldn't patch it, so it's weak now. Not worth aborting. - Logger.error(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e); - } - - final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); - signer.initSign(privateKey); - signer.update(bytes); - final byte[] signature = signer.sign(); - - final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature); - BigInteger r = new BigInteger(arrays[0]); - BigInteger s = new BigInteger(arrays[1]); - // This is awful, but signatures are always 40 bytes long. - byte[] decoded = Utils.concatAll( - Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2), - Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2)); - return decoded; - } - } - - public static BrowserIDKeyPair generateKeyPair(int keysize) - throws NoSuchAlgorithmException { - final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); - keyPairGenerator.initialize(keysize); - final KeyPair keyPair = keyPairGenerator.generateKeyPair(); - DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate(); - DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic(); - return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey)); - } - - public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException { - if (x == null) { - throw new IllegalArgumentException("x must not be null"); - } - if (p == null) { - throw new IllegalArgumentException("p must not be null"); - } - if (q == null) { - throw new IllegalArgumentException("q must not be null"); - } - if (g == null) { - throw new IllegalArgumentException("g must not be null"); - } - KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g); - KeyFactory keyFactory = KeyFactory.getInstance("DSA"); - DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec); - return new DSASigningPrivateKey(privateKey); - } - - public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException { - if (y == null) { - throw new IllegalArgumentException("n must not be null"); - } - if (p == null) { - throw new IllegalArgumentException("p must not be null"); - } - if (q == null) { - throw new IllegalArgumentException("q must not be null"); - } - if (g == null) { - throw new IllegalArgumentException("g must not be null"); - } - KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g); - KeyFactory keyFactory = KeyFactory.getInstance("DSA"); - DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec); - return new DSAVerifyingPublicKey(publicKey); - } - - public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - String algorithm = o.getString("algorithm"); - if (!"DS".equals(algorithm)) { - throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm); - } - try { - BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE); - BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE); - BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE); - BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE); - return createPrivateKey(x, p, q, g); - } catch (NullPointerException | NumberFormatException e) { - throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); - } - } - - public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - String algorithm = o.getString("algorithm"); - if (!"DS".equals(algorithm)) { - throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm); - } - try { - BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE); - BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE); - BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE); - BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE); - return createPublicKey(y, p, q, g); - } catch (NullPointerException | NumberFormatException e) { - throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE); - } - } - - public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - try { - ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY); - ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY); - if (privateKey == null) { - throw new InvalidKeySpecException("privateKey must not be null"); - } - if (publicKey == null) { - throw new InvalidKeySpecException("publicKey must not be null"); - } - return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey)); - } catch (NonObjectJSONException e) { - throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects"); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java deleted file mode 100644 index 207accc76..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java +++ /dev/null @@ -1,245 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import org.json.simple.JSONObject; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.apache.commons.codec.binary.StringUtils; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.TreeMap; - -/** - * Encode and decode JSON Web Tokens. - * <p> - * Reverse-engineered from the Node.js jwcrypto library at - * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a> - * and informed by the informal draft standard "JSON Web Token (JWT)" at - * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>. - */ -public class JSONWebTokenUtils { - public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; - public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000; - public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L; - public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1"; - public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1"; - - public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException { - final ExtendedJSONObject header = new ExtendedJSONObject(); - header.put("alg", privateKey.getAlgorithm()); - String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8")); - String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")); - ArrayList<String> segments = new ArrayList<String>(); - segments.add(encodedHeader); - segments.add(encodedPayload); - byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8"); - byte[] signature = privateKey.signMessage(message); - segments.add(Base64.encodeBase64URLSafeString(signature)); - return Utils.toDelimitedString(".", segments); - } - - public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException { - if (token == null) { - throw new IllegalArgumentException("token must not be null"); - } - String[] segments = token.split("\\."); - if (segments == null || segments.length != 3) { - throw new GeneralSecurityException("malformed token"); - } - byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8"); - byte[] signature = Base64.decodeBase64(segments[2]); - boolean verifies = publicKey.verifyMessage(message, signature); - if (!verifies) { - throw new GeneralSecurityException("bad signature"); - } - String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1])); - return payload; - } - - /** - * Public for testing. - */ - @SuppressWarnings("unchecked") - public static String getPayloadString(String payloadString, String audience, String issuer, - Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException { - ExtendedJSONObject payload; - if (payloadString != null) { - payload = new ExtendedJSONObject(payloadString); - } else { - payload = new ExtendedJSONObject(); - } - if (audience != null) { - payload.put("aud", audience); - } - payload.put("iss", issuer); - if (issuedAt != null) { - payload.put("iat", issuedAt); - } - payload.put("exp", expiresAt); - // TreeMap so that keys are sorted. A small attempt to keep output stable over time. - return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object)); - } - - protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException { - ExtendedJSONObject payload = new ExtendedJSONObject(); - ExtendedJSONObject principal = new ExtendedJSONObject(); - principal.put("email", email); - payload.put("principal", principal); - payload.put("public-key", publicKeyToSign.toJSONObject()); - return payload.toJSONString(); - } - - public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, - String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException { - String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email); - String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt); - return JSONWebTokenUtils.encode(payloadString, privateKey); - } - - /** - * Create a Browser ID assertion. - * - * @param privateKeyToSignWith - * private key to sign assertion with. - * @param certificate - * to include in assertion; no attempt is made to ensure the - * certificate is valid, or corresponds to the private key, or any - * other condition. - * @param audience - * to produce assertion for. - * @param issuer - * to produce assertion for. - * @param issuedAt - * timestamp for assertion, in milliseconds since the epoch; if null, - * no timestamp is included. - * @param expiresAt - * expiration timestamp for assertion, in milliseconds since the epoch. - * @return assertion. - * @throws NonObjectJSONException - * @throws IOException - * @throws GeneralSecurityException - */ - public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience, - String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException { - String emptyAssertionPayloadString = "{}"; - String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt); - String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith); - return certificate + "~" + signature; - } - - /** - * For debugging only! - * - * @param input - * certificate to dump. - * @return non-null object with keys header, payload, signature if the - * certificate is well-formed. - */ - public static ExtendedJSONObject parseCertificate(String input) { - try { - String[] parts = input.split("\\."); - if (parts.length != 3) { - return null; - } - String cHeader = new String(Base64.decodeBase64(parts[0])); - String cPayload = new String(Base64.decodeBase64(parts[1])); - String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("header", new ExtendedJSONObject(cHeader)); - o.put("payload", new ExtendedJSONObject(cPayload)); - o.put("signature", cSignature); - return o; - } catch (Exception e) { - return null; - } - } - - /** - * For debugging only! - * - * @param input certificate to dump. - * @return true if the certificate is well-formed. - */ - public static boolean dumpCertificate(String input) { - ExtendedJSONObject c = parseCertificate(input); - try { - if (c == null) { - System.out.println("Malformed certificate -- got exception trying to dump contents."); - return false; - } - System.out.println("certificate header: " + c.getObject("header").toJSONString()); - System.out.println("certificate payload: " + c.getObject("payload").toJSONString()); - System.out.println("certificate signature: " + c.getString("signature")); - return true; - } catch (Exception e) { - System.out.println("Malformed certificate -- got exception trying to dump contents."); - return false; - } - } - - /** - * For debugging only! - * - * @param input assertion to dump. - * @return true if the assertion is well-formed. - */ - public static ExtendedJSONObject parseAssertion(String input) { - try { - String[] parts = input.split("~"); - if (parts.length != 2) { - return null; - } - String certificate = parts[0]; - String assertion = parts[1]; - parts = assertion.split("\\."); - if (parts.length != 3) { - return null; - } - String aHeader = new String(Base64.decodeBase64(parts[0])); - String aPayload = new String(Base64.decodeBase64(parts[1])); - String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2])); - // We do all the assertion parsing *before* dumping the certificate in - // case there's a malformed assertion. - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("header", new ExtendedJSONObject(aHeader)); - o.put("payload", new ExtendedJSONObject(aPayload)); - o.put("signature", aSignature); - o.put("certificate", certificate); - return o; - } catch (Exception e) { - return null; - } - } - - /** - * For debugging only! - * - * @param input assertion to dump. - * @return true if the assertion is well-formed. - */ - public static boolean dumpAssertion(String input) { - ExtendedJSONObject a = parseAssertion(input); - try { - if (a == null) { - System.out.println("Malformed assertion -- got exception trying to dump contents."); - return false; - } - dumpCertificate(a.getString("certificate")); - System.out.println("assertion header: " + a.getObject("header").toJSONString()); - System.out.println("assertion payload: " + a.getObject("payload").toJSONString()); - System.out.println("assertion signature: " + a.getString("signature")); - return true; - } catch (Exception e) { - System.out.println("Malformed assertion -- got exception trying to dump contents."); - return false; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java deleted file mode 100644 index c807d4cbb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/MockMyIDTokenFactory.java +++ /dev/null @@ -1,128 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import java.math.BigInteger; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -/** - * Generate certificates and assertions backed by mockmyid.com's private key. - * <p> - * These artifacts are for testing only. - */ -public class MockMyIDTokenFactory { - public static final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16); - public static final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16); - public static final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16); - public static final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16); - public static final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16); - - // Computed lazily by static <code>getMockMyIDPrivateKey</code>. - protected static SigningPrivateKey cachedMockMyIDPrivateKey; - - public static SigningPrivateKey getMockMyIDPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException { - if (cachedMockMyIDPrivateKey == null) { - cachedMockMyIDPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g); - } - return cachedMockMyIDPrivateKey; - } - - /** - * Sign a public key asserting ownership of username@mockmyid.com with - * mockmyid.com's private key. - * - * @param publicKeyToSign - * public key to sign. - * @param username - * sign username@mockmyid.com - * @param issuedAt - * timestamp for certificate, in milliseconds since the epoch. - * @param expiresAt - * expiration timestamp for certificate, in milliseconds since the epoch. - * @return encoded certificate string. - * @throws Exception - */ - public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username, - final long issuedAt, final long expiresAt) - throws Exception { - if (!username.endsWith("@mockmyid.com")) { - username = username + "@mockmyid.com"; - } - SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey(); - return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey); - } - - /** - * Sign a public key asserting ownership of username@mockmyid.com with - * mockmyid.com's private key. - * - * @param publicKeyToSign - * public key to sign. - * @param username - * sign username@mockmyid.com - * @return encoded certificate string. - * @throws Exception - */ - public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username) - throws Exception { - long ciat = System.currentTimeMillis(); - long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS; - return createMockMyIDCertificate(publicKeyToSign, username, ciat, cexp); - } - - /** - * Generate an assertion asserting ownership of username@mockmyid.com to a - * relying party. The underlying certificate is signed by mockymid.com's - * private key. - * - * @param keyPair - * to sign with. - * @param username - * sign username@mockmyid.com. - * @param certificateIssuedAt - * timestamp for certificate, in milliseconds since the epoch. - * @param certificateExpiresAt - * expiration timestamp for certificate, in milliseconds since the epoch. - * @param assertionIssuedAt - * timestamp for assertion, in milliseconds since the epoch; if null, - * no timestamp is included. - * @param assertionExpiresAt - * expiration timestamp for assertion, in milliseconds since the epoch. - * @return encoded assertion string. - * @throws Exception - */ - public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience, - long certificateIssuedAt, long certificateExpiresAt, - Long assertionIssuedAt, long assertionExpiresAt) - throws Exception { - String certificate = createMockMyIDCertificate(keyPair.getPublic(), username, - certificateIssuedAt, certificateExpiresAt); - return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, - JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionExpiresAt); - } - - /** - * Generate an assertion asserting ownership of username@mockmyid.com to a - * relying party. The underlying certificate is signed by mockymid.com's - * private key. - * - * @param keyPair - * to sign with. - * @param username - * sign username@mockmyid.com. - * @return encoded assertion string. - * @throws Exception - */ - public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience) - throws Exception { - long ciat = System.currentTimeMillis(); - long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS; - long aiat = ciat + 1; - long aexp = aiat + JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS; - return createMockMyIDAssertion(keyPair, username, audience, - ciat, cexp, aiat, aexp); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java deleted file mode 100644 index 902f6fb4d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/RSACryptoImplementation.java +++ /dev/null @@ -1,182 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.security.spec.RSAPrivateKeySpec; -import java.security.spec.RSAPublicKeySpec; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; - -public class RSACryptoImplementation { - public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; - - /** - * Parameters are serialized as decimal strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. We - * expect to follow the JOSE/JWT spec as it solidifies, and that will probably - * mean unifying this base. - */ - protected static final int SERIALIZATION_BASE = 10; - - protected static class RSAVerifyingPublicKey implements VerifyingPublicKey { - protected final RSAPublicKey publicKey; - - public RSAVerifyingPublicKey(RSAPublicKey publicKey) { - this.publicKey = publicKey; - } - - /** - * Serialize to a JSON object. - * <p> - * Parameters are serialized as decimal strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. - */ - @Override - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("algorithm", "RS"); - o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE)); - o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE)); - return o; - } - - @Override - public boolean verifyMessage(byte[] bytes, byte[] signature) - throws GeneralSecurityException { - final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); - signer.initVerify(publicKey); - signer.update(bytes); - return signer.verify(signature); - } - } - - protected static class RSASigningPrivateKey implements SigningPrivateKey { - protected final RSAPrivateKey privateKey; - - public RSASigningPrivateKey(RSAPrivateKey privateKey) { - this.privateKey = privateKey; - } - - @Override - public String getAlgorithm() { - return "RS" + (privateKey.getModulus().bitLength() + 7)/8; - } - - /** - * Serialize to a JSON object. - * <p> - * Parameters are serialized as decimal strings. Hex-versus-decimal was - * reverse-engineered from what the Persona public verifier accepted. - */ - @Override - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("algorithm", "RS"); - o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE)); - o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE)); - return o; - } - - @Override - public byte[] signMessage(byte[] bytes) - throws GeneralSecurityException { - final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM); - signer.initSign(privateKey); - signer.update(bytes); - return signer.sign(); - } - } - - public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException { - final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(keysize); - final KeyPair keyPair = keyPairGenerator.generateKeyPair(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey)); - } - - public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException { - if (n == null) { - throw new IllegalArgumentException("n must not be null"); - } - if (d == null) { - throw new IllegalArgumentException("d must not be null"); - } - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - KeySpec keySpec = new RSAPrivateKeySpec(n, d); - RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); - return new RSASigningPrivateKey(privateKey); - } - - public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException { - if (n == null) { - throw new IllegalArgumentException("n must not be null"); - } - if (e == null) { - throw new IllegalArgumentException("e must not be null"); - } - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - KeySpec keySpec = new RSAPublicKeySpec(n, e); - RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); - return new RSAVerifyingPublicKey(publicKey); - } - - public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - String algorithm = o.getString("algorithm"); - if (!"RS".equals(algorithm)) { - throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm); - } - try { - BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE); - BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE); - return createPrivateKey(n, d); - } catch (NullPointerException | NumberFormatException e) { - throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE); - } - } - - public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - String algorithm = o.getString("algorithm"); - if (!"RS".equals(algorithm)) { - throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm); - } - try { - BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE); - BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE); - return createPublicKey(n, e); - } catch (NullPointerException | NumberFormatException e) { - throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE); - } - } - - public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - try { - ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY); - ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY); - if (privateKey == null) { - throw new InvalidKeySpecException("privateKey must not be null"); - } - if (publicKey == null) { - throw new InvalidKeySpecException("publicKey must not be null"); - } - return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey)); - } catch (NonObjectJSONException e) { - throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects"); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java deleted file mode 100644 index 6c388d167..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/SigningPrivateKey.java +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import java.security.GeneralSecurityException; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public interface SigningPrivateKey { - /** - * Return the JSON Web Token "alg" header corresponding to this private key. - * <p> - * The header is used when formatting web tokens, and generally denotes the - * algorithm and an ad-hoc encoding of the key size. - * - * @return header. - */ - public String getAlgorithm(); - - /** - * Generate a JSON representation of a private key. - * <p> - * <b>This should only be used for debugging. No private keys should go over - * the wire at any time.</b> - * - * @param privateKey - * to represent. - * @return JSON representation. - */ - public ExtendedJSONObject toJSONObject(); - - /** - * Sign a message. - * @param message to sign. - * @return signature. - * @throws GeneralSecurityException - */ - public byte[] signMessage(byte[] message) throws GeneralSecurityException; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java deleted file mode 100644 index 74b534b90..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/VerifyingPublicKey.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid; - -import java.security.GeneralSecurityException; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - - -public interface VerifyingPublicKey { - /** - * Generate a JSON representation of a public key. - * - * @param publicKey - * to represent. - * @return JSON representation. - */ - public ExtendedJSONObject toJSONObject(); - - /** - * Verify a signature. - * - * @param message - * to verify signature of. - * @param signature - * to verify. - * @return true if signature is a signature of message produced by the private - * key corresponding to this public key. - * @throws GeneralSecurityException - */ - public boolean verifyMessage(byte[] message, byte[] signature) throws GeneralSecurityException; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java deleted file mode 100644 index aa8db2d48..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/AbstractBrowserIDRemoteVerifierClient.java +++ /dev/null @@ -1,95 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -import java.io.IOException; -import java.net.URI; -import java.security.GeneralSecurityException; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException; -import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.Resource; -import org.mozilla.gecko.sync.net.SyncResponse; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; - -public abstract class AbstractBrowserIDRemoteVerifierClient implements BrowserIDVerifierClient { - public static final String LOG_TAG = AbstractBrowserIDRemoteVerifierClient.class.getSimpleName(); - - protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate { - private final BrowserIDVerifierDelegate delegate; - - protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) { - super(resource); - this.delegate = delegate; - } - - @Override - public String getUserAgent() { - return null; - } - - @Override - public void handleHttpResponse(HttpResponse response) { - SyncResponse res = new SyncResponse(response); - int statusCode = res.getStatusCode(); - Logger.debug(LOG_TAG, "Got response with status code " + statusCode + "."); - - if (statusCode != 200) { - delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200.")); - return; - } - - ExtendedJSONObject o = null; - try { - o = res.jsonObjectBody(); - } catch (Exception e) { - delegate.handleError(new BrowserIDVerifierMalformedResponseException(e)); - return; - } - - String status = o.getString("status"); - if ("failure".equals(status)) { - delegate.handleFailure(o); - return; - } - - if (!("okay".equals(status))) { - delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'.")); - return; - } - - delegate.handleSuccess(o); - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - Logger.warn(LOG_TAG, "Got transport exception.", e); - delegate.handleError(e); - } - - @Override - public void handleHttpProtocolException(ClientProtocolException e) { - Logger.warn(LOG_TAG, "Got protocol exception.", e); - delegate.handleError(e); - } - - @Override - public void handleHttpIOException(IOException e) { - Logger.warn(LOG_TAG, "Got IO exception.", e); - delegate.handleError(e); - } - } - - protected final URI verifierUri; - - public AbstractBrowserIDRemoteVerifierClient(URI verifierUri) { - this.verifierUri = verifierUri; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java deleted file mode 100644 index f61a82323..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient10.java +++ /dev/null @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.List; - -import org.mozilla.gecko.sync.net.BaseResource; - -import ch.boye.httpclientandroidlib.NameValuePair; -import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity; -import ch.boye.httpclientandroidlib.message.BasicNameValuePair; - -/** - * The verifier protocol changed: version 1 posts form-encoded data; version 2 - * posts JSON data. - */ -public class BrowserIDRemoteVerifierClient10 extends AbstractBrowserIDRemoteVerifierClient { - public static final String LOG_TAG = BrowserIDRemoteVerifierClient10.class.getSimpleName(); - - public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify"; - - public BrowserIDRemoteVerifierClient10() throws URISyntaxException { - super(new URI(DEFAULT_VERIFIER_URL)); - } - - public BrowserIDRemoteVerifierClient10(URI verifierUri) { - super(verifierUri); - } - - @Override - public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) { - if (audience == null) { - throw new IllegalArgumentException("audience cannot be null."); - } - if (assertion == null) { - throw new IllegalArgumentException("assertion cannot be null."); - } - if (delegate == null) { - throw new IllegalArgumentException("delegate cannot be null."); - } - - BaseResource r = new BaseResource(verifierUri); - - r.delegate = new RemoteVerifierResourceDelegate(r, delegate); - - List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] { - new BasicNameValuePair("audience", audience), - new BasicNameValuePair("assertion", assertion) }); - - try { - r.post(new UrlEncodedFormEntity(nvps, "UTF-8")); - } catch (UnsupportedEncodingException e) { - delegate.handleError(e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java deleted file mode 100644 index 013856576..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDRemoteVerifierClient20.java +++ /dev/null @@ -1,58 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -import java.net.URI; -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.BaseResource; - -/** - * The verifier protocol changed: version 1 posts form-encoded data; version 2 - * posts JSON data. - */ -public class BrowserIDRemoteVerifierClient20 extends AbstractBrowserIDRemoteVerifierClient { - public static final String LOG_TAG = BrowserIDRemoteVerifierClient20.class.getSimpleName(); - - public static final String DEFAULT_VERIFIER_URL = "https://verifier.accounts.firefox.com/v2"; - - protected static final String JSON_KEY_ASSERTION = "assertion"; - protected static final String JSON_KEY_AUDIENCE = "audience"; - - public BrowserIDRemoteVerifierClient20() throws URISyntaxException { - super(new URI(DEFAULT_VERIFIER_URL)); - } - - public BrowserIDRemoteVerifierClient20(URI verifierUri) { - super(verifierUri); - } - - @Override - public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) { - if (audience == null) { - throw new IllegalArgumentException("audience cannot be null."); - } - if (assertion == null) { - throw new IllegalArgumentException("assertion cannot be null."); - } - if (delegate == null) { - throw new IllegalArgumentException("delegate cannot be null."); - } - - BaseResource r = new BaseResource(verifierUri); - r.delegate = new RemoteVerifierResourceDelegate(r, delegate); - - final ExtendedJSONObject requestBody = new ExtendedJSONObject(); - requestBody.put(JSON_KEY_AUDIENCE, audience); - requestBody.put(JSON_KEY_ASSERTION, assertion); - - try { - r.post(requestBody); - } catch (Exception e) { - delegate.handleError(e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java deleted file mode 100644 index 67a327f19..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierClient.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -public interface BrowserIDVerifierClient { - public abstract void verify(String audience, String assertion, BrowserIDVerifierDelegate delegate); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java deleted file mode 100644 index b58d03281..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public interface BrowserIDVerifierDelegate { - void handleSuccess(ExtendedJSONObject response); - void handleFailure(ExtendedJSONObject response); - void handleError(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java deleted file mode 100644 index dacaf6112..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/verifier/BrowserIDVerifierException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.browserid.verifier; - -public class BrowserIDVerifierException extends Exception { - private static final long serialVersionUID = 2228946910754889975L; - - public BrowserIDVerifierException(String detailMessage) { - super(detailMessage); - } - - public BrowserIDVerifierException(Throwable throwable) { - super(throwable); - } - - public static class BrowserIDVerifierMalformedResponseException extends BrowserIDVerifierException { - private static final long serialVersionUID = 115377527009652839L; - - public BrowserIDVerifierMalformedResponseException(String detailMessage) { - super(detailMessage); - } - - public BrowserIDVerifierMalformedResponseException(Throwable throwable) { - super(throwable); - } - } - - public static class BrowserIDVerifierErrorResponseException extends BrowserIDVerifierException { - private static final long serialVersionUID = 115377527009652840L; - - public BrowserIDVerifierErrorResponseException(String detailMessage) { - super(detailMessage); - } - - public BrowserIDVerifierErrorResponseException(Throwable throwable) { - super(throwable); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java deleted file mode 100644 index 8a31c1ce0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/AccountLoader.java +++ /dev/null @@ -1,227 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Handler; -import android.os.Looper; -import android.content.AsyncTaskLoader; -import android.support.v4.content.LocalBroadcastManager; - -import java.lang.ref.WeakReference; - -/** - * A Loader that queries and updates based on the existence of Firefox and - * legacy Sync Android Accounts. - * - * The loader returns an Android Account (of either Account type) if an account - * exists, and null to indicate no Account is present. - * - * The loader listens for Accounts added and deleted, and also Accounts being - * updated by Sync or another Activity, via the use of - * {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}. - * Be careful of message loops if you update the account state from an activity - * that uses this loader. - * - * This implementation is based on - * <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>. - */ -public class AccountLoader extends AsyncTaskLoader<Account> { - protected Account account = null; - protected BroadcastReceiver broadcastReceiver = null; - - // Hold a weak reference to AccountLoader instance in this Runnable to avoid potentially leaking it - // after posting to a Handler in the BroadcastReceiver returned from makeNewObserver. - private final BroadcastReceiverRunnable broadcastReceiverRunnable = new BroadcastReceiverRunnable(this); - - public AccountLoader(final Context context) { - super(context); - } - - // Task that performs the asynchronous load. - @Override - public Account loadInBackground() { - return FirefoxAccounts.getFirefoxAccount(getContext()); - } - - // Deliver the results to the registered listener. - @Override - public void deliverResult(Account data) { - if (isReset()) { - // The Loader has been reset; ignore the result and invalidate the data. - releaseResources(data); - return; - } - - // Hold a reference to the old data so it doesn't get garbage collected. - // We must protect it until the new data has been delivered. - Account oldData = account; - account = data; - - if (isStarted()) { - // If the Loader is in a started state, deliver the results to the - // client. The superclass method does this for us. - super.deliverResult(data); - } - - // Invalidate the old data as we don't need it any more. - if (oldData != null && oldData != data) { - releaseResources(oldData); - } - } - - // The Loader’s state-dependent behavior. - @Override - protected void onStartLoading() { - if (account != null) { - // Deliver any previously loaded data immediately. - deliverResult(account); - } - - // Begin monitoring the underlying data source. - if (broadcastReceiver == null) { - broadcastReceiver = makeNewObserver(); - registerLocalObserver(getContext(), broadcastReceiver); - registerSystemObserver(getContext(), broadcastReceiver); - } - - if (takeContentChanged() || account == null) { - // When the observer detects a change, it should call onContentChanged() - // on the Loader, which will cause the next call to takeContentChanged() - // to return true. If this is ever the case (or if the current data is - // null), we force a new load. - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - // The Loader is in a stopped state, so we should attempt to cancel the - // current load (if there is one). - cancelLoad(); - - // Note that we leave the observer as is. Loaders in a stopped state - // should still monitor the data source for changes so that the Loader - // will know to force a new load if it is ever started again. - } - - @Override - protected void onReset() { - // Ensure the loader has been stopped. In CursorLoader and the template - // this code follows (see the class comment), this is onStopLoading, which - // appears to not set the started flag (see Loader itself). - stopLoading(); - - // At this point we can release the resources associated with 'mData'. - if (account != null) { - releaseResources(account); - account = null; - } - - // The Loader is being reset, so we should stop monitoring for changes. - if (broadcastReceiver != null) { - final BroadcastReceiver observer = broadcastReceiver; - broadcastReceiver = null; - unregisterObserver(getContext(), observer); - } - } - - @Override - public void onCanceled(final Account data) { - // Attempt to cancel the current asynchronous load. - super.onCanceled(data); - - // The load has been canceled, so we should release the resources - // associated with 'data'. - releaseResources(data); - } - - // Observer which receives notifications when the data changes. - protected BroadcastReceiver makeNewObserver() { - return new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // onContentChanged must be called on the main thread. - // If we're already on the main thread, call it directly. - if (Looper.myLooper() == Looper.getMainLooper()) { - onContentChanged(); - return; - } - - // Otherwise, post a Runnable to a Handler bound to the main thread's message loop. - final Handler mainHandler = new Handler(Looper.getMainLooper()); - mainHandler.post(broadcastReceiverRunnable); - } - }; - } - - private static class BroadcastReceiverRunnable implements Runnable { - private final WeakReference<AccountLoader> accountLoaderWeakReference; - - public BroadcastReceiverRunnable(final AccountLoader accountLoader) { - accountLoaderWeakReference = new WeakReference<>(accountLoader); - } - - @Override - public void run() { - final AccountLoader accountLoader = accountLoaderWeakReference.get(); - if (accountLoader != null) { - accountLoader.onContentChanged(); - } - } - } - - private void releaseResources(Account data) { - // For a simple List, there is nothing to do. For something like a Cursor, we - // would close it in this method. All resources associated with the Loader - // should be released here. - } - - /** - * Register provided observer with the LocalBroadcastManager to listen for internal events. - * - * @param context <code>Context</code> to use for obtaining LocalBroadcastManager instance. - * @param observer <code>BroadcastReceiver</code> which will handle local events. - */ - protected static void registerLocalObserver(final Context context, final BroadcastReceiver observer) { - final IntentFilter intentFilter = new IntentFilter(); - // Firefox Account internal state changed. - intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION); - // Firefox Account profile state changed. - intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION); - - LocalBroadcastManager.getInstance(context).registerReceiver(observer, intentFilter); - } - - /** - * Register provided observer for handling system-wide broadcasts. - * - * @param context <code>Context</code> to use for registering a receiver. - * @param observer <code>BroadcastReceiver</code> which will handle system events. - */ - protected static void registerSystemObserver(final Context context, final BroadcastReceiver observer) { - context.registerReceiver(observer, - // Android Account added or removed. - new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION), - // No broadcast permissions required. - null, - // Null handler ensures that broadcasts will be handled on the main thread. - null - ); - } - - protected static void unregisterObserver(final Context context, final BroadcastReceiver observer) { - LocalBroadcastManager.getInstance(context).unregisterReceiver(observer); - context.unregisterReceiver(observer); - } -} - diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java deleted file mode 100644 index 4184340ec..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java +++ /dev/null @@ -1,222 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import java.io.File; -import java.util.concurrent.CountDownLatch; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.fxa.authenticator.AccountPickler; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.Utils; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ContentResolver; -import android.content.Context; -import android.os.Bundle; - -/** - * Simple public accessors for Firefox account objects. - */ -public class FirefoxAccounts { - private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName(); - - /** - * Returns true if a FirefoxAccount exists, false otherwise. - * - * @param context Android context. - * @return true if at least one Firefox account exists. - */ - public static boolean firefoxAccountsExist(final Context context) { - return getFirefoxAccounts(context).length > 0; - } - - /** - * Return Firefox accounts. - * <p> - * If no accounts exist in the AccountManager, one may be created - * via a pickled FirefoxAccount, if available, and that account - * will be added to the AccountManager and returned. - * <p> - * Note that this can be called from any thread. - * - * @param context Android context. - * @return Firefox account objects. - */ - public static Account[] getFirefoxAccounts(final Context context) { - final Account[] accounts = - AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE); - if (accounts.length > 0) { - return accounts; - } - - final Account pickledAccount = getPickledAccount(context); - return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0]; - } - - private static Account getPickledAccount(final Context context) { - // To avoid a StrictMode violation for disk access, we call this from a background thread. - // We do this every time, so the caller doesn't have to care. - final CountDownLatch latch = new CountDownLatch(1); - final Account[] accounts = new Account[1]; - ThreadPool.run(new Runnable() { - @Override - public void run() { - try { - final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME); - if (!file.exists()) { - accounts[0] = null; - return; - } - - // There is a small race window here: if the user creates a new Firefox account - // between our checks, this could erroneously report that no Firefox accounts - // exist. - final AndroidFxAccount fxAccount = - AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); - accounts[0] = fxAccount != null ? fxAccount.getAndroidAccount() : null; - } finally { - latch.countDown(); - } - } - }); - - try { - latch.await(); // Wait for the background thread to return. - } catch (InterruptedException e) { - Logger.warn(LOG_TAG, - "Foreground thread unexpectedly interrupted while getting pickled account", e); - return null; - } - - return accounts[0]; - } - - /** - * @param context Android context. - * @return the configured Firefox account if one exists, or null otherwise. - */ - public static Account getFirefoxAccount(final Context context) { - Account[] accounts = getFirefoxAccounts(context); - if (accounts.length > 0) { - return accounts[0]; - } - return null; - } - - /** - * @return - * the {@link State} instance associated with the current account, or <code>null</code> if - * no accounts exist. - */ - public static State getFirefoxAccountState(final Context context) { - final Account account = getFirefoxAccount(context); - if (account == null) { - return null; - } - - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - try { - return fxAccount.getState(); - } catch (final Exception ex) { - Logger.warn(LOG_TAG, "Could not get FX account state.", ex); - return null; - } - } - - /* - * @param context Android context - * @return the email address associated with the configured Firefox account if one exists; null otherwise. - */ - public static String getFirefoxAccountEmail(final Context context) { - final Account account = getFirefoxAccount(context); - if (account == null) { - return null; - } - return account.name; - } - - public static void logSyncOptions(Bundle syncOptions) { - final boolean scheduleNow = syncOptions.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); - - Logger.info(LOG_TAG, "Sync options -- scheduling now: " + scheduleNow); - } - - public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip) { - final Bundle syncOptions = new Bundle(); - syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); - syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); - requestSync(account, syncOptions, stagesToSync, stagesToSkip); - } - - public static void requestEventualSync(final Account account, String[] stagesToSync, String[] stagesToSkip) { - requestSync(account, Bundle.EMPTY, stagesToSync, stagesToSkip); - } - - /** - * Request a sync for the given Android Account. - * <p> - * Any hints are strictly optional: the actual requested sync is scheduled by - * the Android sync scheduler, and the sync mechanism may ignore hints as it - * sees fit. - * <p> - * It is safe to call this method from any thread. - * - * @param account to sync. - * @param syncOptions to pass to sync. - * @param stagesToSync stage names to sync. - * @param stagesToSkip stage names to skip. - */ - protected static void requestSync(final Account account, final Bundle syncOptions, String[] stagesToSync, String[] stagesToSkip) { - if (account == null) { - throw new IllegalArgumentException("account must not be null"); - } - if (syncOptions == null) { - throw new IllegalArgumentException("syncOptions must not be null"); - } - - Utils.putStageNamesToSync(syncOptions, stagesToSync, stagesToSkip); - - Logger.info(LOG_TAG, "Requesting sync."); - logSyncOptions(syncOptions); - - // We get strict mode warnings on some devices, so make the request on a - // background thread. - ThreadPool.run(new Runnable() { - @Override - public void run() { - for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) { - ContentResolver.requestSync(account, authority, syncOptions); - } - } - }); - } - - /** - * Start notifying <code>syncStatusListener</code> of sync status changes. - * <p> - * Only a weak reference to <code>syncStatusListener</code> is held. - * - * @param syncStatusListener to start notifying. - */ - public static void addSyncStatusListener(SyncStatusListener syncStatusListener) { - // startObserving null-checks its argument. - FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener); - } - - /** - * Stop notifying <code>syncStatusListener</code> of sync status changes. - * - * @param syncStatusListener to stop notifying. - */ - public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) { - // stopObserving null-checks its argument. - FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java deleted file mode 100644 index c6147b323..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java +++ /dev/null @@ -1,75 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import org.mozilla.gecko.AppConstants; - -public class FxAccountConstants { - public static final String GLOBAL_LOG_TAG = "FxAccounts"; - public static final String ACCOUNT_TYPE = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE; - - // Must be a client ID allocated with "canGrant" privileges! - public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb"; - - public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1"; - public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5"; - public static final String DEFAULT_OAUTH_SERVER_ENDPOINT = "https://oauth.accounts.firefox.com/v1"; - public static final String DEFAULT_PROFILE_SERVER_ENDPOINT = "https://profile.accounts.firefox.com/v1"; - - public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://stable.dev.lcip.org/auth/v1"; - public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5"; - public static final String STAGE_OAUTH_SERVER_ENDPOINT = "https://oauth-stable.dev.lcip.org/v1"; - public static final String STAGE_PROFILE_SERVER_ENDPOINT = "https://latest.dev.lcip.org/profile/v1"; - - // Action to update on cached profile information. - public static final String ACCOUNT_PROFILE_JSON_UPDATED_ACTION = "org.mozilla.gecko.fxa.profile.JSON.updated"; - - // You must be at least 13 years old, on the day of creation, to create a Firefox Account. - public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13; - - // Key for avatar URI in profile JSON. - public static final String KEY_PROFILE_JSON_AVATAR = "avatar"; - // Key for username in profile JSON. - public static final String KEY_PROFILE_JSON_USERNAME = "displayName"; - - // You must wait 15 minutes after failing an age check before trying to create a different account. - public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000; - - public static final String USER_AGENT = "Firefox-Android-FxAccounts/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")"; - - public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json"; - - - /** - * Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent. - */ - public static final long ACCOUNT_DELETED_INTENT_VERSION = 1; - - public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version"; - public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account"; - public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile"; - public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint"; - public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens"; - - /** - * This action is broadcast when an Android Firefox Account's internal state - * is changed. - * <p> - * It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and - * can be received only by Firefox versions sharing the same Android Firefox - * Account type. - */ - public static final String ACCOUNT_STATE_CHANGED_ACTION = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE + ".accounts.ACCOUNT_STATE_CHANGED_ACTION"; - - public static final String ACTION_FXA_CONFIRM_ACCOUNT = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_CONFIRM_ACCOUNT"; - public static final String ACTION_FXA_FINISH_MIGRATING = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_FINISH_MIGRATING"; - public static final String ACTION_FXA_GET_STARTED = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED"; - public static final String ACTION_FXA_STATUS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS"; - public static final String ACTION_FXA_UPDATE_CREDENTIALS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS"; - - public static final String ENDPOINT_PREFERENCES = "preferences"; - public static final String ENDPOINT_NOTIFICATION = "notification"; - public static final String ENDPOINT_FIRSTRUN = "firstrun"; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java deleted file mode 100644 index cd46ae2bd..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java +++ /dev/null @@ -1,81 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public class FxAccountDevice { - - public static final String JSON_KEY_NAME = "name"; - public static final String JSON_KEY_ID = "id"; - public static final String JSON_KEY_TYPE = "type"; - public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice"; - public static final String JSON_KEY_PUSH_CALLBACK = "pushCallback"; - public static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey"; - public static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey"; - - public final String id; - public final String name; - public final String type; - public final Boolean isCurrentDevice; - public final String pushCallback; - public final String pushPublicKey; - public final String pushAuthKey; - - public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice, - String pushCallback, String pushPublicKey, String pushAuthKey) { - this.name = name; - this.id = id; - this.type = type; - this.isCurrentDevice = isCurrentDevice; - this.pushCallback = pushCallback; - this.pushPublicKey = pushPublicKey; - this.pushAuthKey = pushAuthKey; - } - - public static FxAccountDevice forRegister(String name, String type, String pushCallback, - String pushPublicKey, String pushAuthKey) { - return new FxAccountDevice(name, null, type, null, pushCallback, pushPublicKey, pushAuthKey); - } - - public static FxAccountDevice forUpdate(String id, String name, String pushCallback, - String pushPublicKey, String pushAuthKey) { - return new FxAccountDevice(name, id, null, null, pushCallback, pushPublicKey, pushAuthKey); - } - - public static FxAccountDevice fromJson(ExtendedJSONObject json) { - String name = json.getString(JSON_KEY_NAME); - String id = json.getString(JSON_KEY_ID); - String type = json.getString(JSON_KEY_TYPE); - Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE); - String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK); - String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY); - String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY); - return new FxAccountDevice(name, id, type, isCurrentDevice, pushCallback, pushPublicKey, pushAuthKey); - } - - public ExtendedJSONObject toJson() { - final ExtendedJSONObject body = new ExtendedJSONObject(); - if (this.name != null) { - body.put(JSON_KEY_NAME, this.name); - } - if (this.id != null) { - body.put(JSON_KEY_ID, this.id); - } - if (this.type != null) { - body.put(JSON_KEY_TYPE, this.type); - } - if (this.pushCallback != null) { - body.put(JSON_KEY_PUSH_CALLBACK, this.pushCallback); - } - if (this.pushPublicKey != null) { - body.put(JSON_KEY_PUSH_PUBLICKEY, this.pushPublicKey); - } - if (this.pushAuthKey != null) { - body.put(JSON_KEY_PUSH_AUTHKEY, this.pushAuthKey); - } - return body; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java deleted file mode 100644 index 66a8ad843..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java +++ /dev/null @@ -1,282 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountClient; -import org.mozilla.gecko.background.fxa.FxAccountClient20; -import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse; -import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate; -import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; -import org.mozilla.gecko.background.fxa.FxAccountRemoteError; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; -import org.mozilla.gecko.util.BundleEventListener; -import org.mozilla.gecko.util.EventCallback; - -import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.security.GeneralSecurityException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/* This class provides a way to register the current device against FxA - * and also stores the registration details in the Android FxAccount. - * This should be used in a state where we possess a sessionToken, most likely the Married state. - */ -public class FxAccountDeviceRegistrator implements BundleEventListener { - private static final String LOG_TAG = "FxADeviceRegistrator"; - - // The current version of the device registration, we use this to re-register - // devices after we update what we send on device registration. - public static final Integer DEVICE_REGISTRATION_VERSION = 2; - - private static FxAccountDeviceRegistrator instance; - private final WeakReference<Context> context; - - private FxAccountDeviceRegistrator(Context appContext) { - this.context = new WeakReference<Context>(appContext); - } - - private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { - if (instance == null) { - FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext); - tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response - instance = tempInstance; - } - return instance; - } - - public static void register(Context context) { - Context appContext = context.getApplicationContext(); - try { - getInstance(appContext).beginRegistration(appContext); - } catch (Exception e) { - Log.e(LOG_TAG, "Could not start FxA device registration", e); - } - } - - private void beginRegistration(Context context) { - // Fire up gecko and send event - // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices - // because we can't import these modules (circular dependency between browser and services) - final Intent geckoIntent = new Intent(); - geckoIntent.setAction("create-services"); - geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService"); - geckoIntent.putExtra("category", "android-push-service"); - geckoIntent.putExtra("data", "android-fxa-subscribe"); - final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context); - geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile()); - context.startService(geckoIntent); - // -> handleMessage() - } - - @Override - public void handleMessage(String event, Bundle message, EventCallback callback) { - if ("FxAccountsPush:Subscribe:Response".equals(event)) { - try { - doFxaRegistration(message.getBundle("subscription")); - } catch (InvalidFxAState e) { - Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e); - } - } else { - Log.e(LOG_TAG, "No action defined for " + event); - } - } - - private void doFxaRegistration(Bundle subscription) throws InvalidFxAState { - final Context context = this.context.get(); - if (this.context == null) { - throw new IllegalStateException("Application context has been gc'ed"); - } - doFxaRegistration(context, subscription, true); - } - - private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState { - String pushCallback = subscription.getString("pushCallback"); - String pushPublicKey = subscription.getString("pushPublicKey"); - String pushAuthKey = subscription.getString("pushAuthKey"); - - final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context); - if (fxAccount == null) { - Log.e(LOG_TAG, "AndroidFxAccount is null"); - return; - } - final byte[] sessionToken = fxAccount.getSessionToken(); - final FxAccountDevice device; - String deviceId = fxAccount.getDeviceId(); - String clientName = getClientName(fxAccount, context); - if (TextUtils.isEmpty(deviceId)) { - Log.i(LOG_TAG, "Attempting registration for a new device"); - device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey); - } else { - Log.i(LOG_TAG, "Attempting registration for an existing device"); - Logger.pii(LOG_TAG, "Device ID: " + deviceId); - device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey); - } - - ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread - final FxAccountClient20 fxAccountClient = - new FxAccountClient20(fxAccount.getAccountServerURI(), executor); - fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() { - @Override - public void handleError(Exception e) { - Log.e(LOG_TAG, "Error while updating a device registration: ", e); - } - - @Override - public void handleFailure(FxAccountClientRemoteException error) { - Log.e(LOG_TAG, "Error while updating a device registration: ", error); - if (error.httpStatusCode == 400) { - if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) { - recoverFromUnknownDevice(fxAccount); - } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) { - recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context, - subscription, allowRecursion); - } - } else - if (error.httpStatusCode == 401 - && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) { - handleTokenError(error, fxAccountClient, fxAccount); - } else { - logErrorAndResetDeviceRegistrationVersion(error, fxAccount); - } - } - - @Override - public void handleSuccess(FxAccountDevice result) { - Log.i(LOG_TAG, "Device registration complete"); - Logger.pii(LOG_TAG, "Registered device ID: " + result.id); - fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION); - } - }); - } - - private static void logErrorAndResetDeviceRegistrationVersion( - final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) { - Log.e(LOG_TAG, "Device registration failed", error); - fxAccount.resetDeviceRegistrationVersion(); - } - - @Nullable - private static String getClientName(final AndroidFxAccount fxAccount, final Context context) { - try { - SharedPreferencesClientsDataDelegate clientsDataDelegate = - new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context); - return clientsDataDelegate.getClientName(); - } catch (UnsupportedEncodingException | GeneralSecurityException e) { - Log.e(LOG_TAG, "Unable to get client name.", e); - return null; - } - } - - private static void handleTokenError(final FxAccountClientRemoteException error, - final FxAccountClient fxAccountClient, - final AndroidFxAccount fxAccount) { - Log.i(LOG_TAG, "Recovering from invalid token error: ", error); - logErrorAndResetDeviceRegistrationVersion(error, fxAccount); - fxAccountClient.accountStatus(fxAccount.getState().uid, - new RequestDelegate<AccountStatusResponse>() { - @Override - public void handleError(Exception e) { - } - - @Override - public void handleFailure(FxAccountClientRemoteException e) { - } - - @Override - public void handleSuccess(AccountStatusResponse result) { - State doghouseState = fxAccount.getState().makeDoghouseState(); - if (!result.exists) { - Log.i(LOG_TAG, "token invalidated because the account no longer exists"); - // TODO: Should be in a "I have an Android account, but the FxA is gone." State. - // This will do for now.. - fxAccount.setState(doghouseState); - return; - } - Log.e(LOG_TAG, "sessionToken invalid"); - fxAccount.setState(doghouseState); - } - }); - } - - private static void recoverFromUnknownDevice(final AndroidFxAccount fxAccount) { - Log.i(LOG_TAG, "unknown device id, clearing the cached device id"); - fxAccount.setDeviceId(null); - } - - /** - * Will call delegate#complete in all cases - */ - private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error, - final FxAccountClient fxAccountClient, - final byte[] sessionToken, - final AndroidFxAccount fxAccount, - final Context context, - final Bundle subscription, - final boolean allowRecursion) { - Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id"); - fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() { - private void onError() { - Log.e(LOG_TAG, "failed to recover from device-session conflict"); - logErrorAndResetDeviceRegistrationVersion(error, fxAccount); - } - - @Override - public void handleError(Exception e) { - onError(); - } - - @Override - public void handleFailure(FxAccountClientRemoteException e) { - onError(); - } - - @Override - public void handleSuccess(FxAccountDevice[] devices) { - for (FxAccountDevice device : devices) { - if (device.isCurrentDevice) { - fxAccount.setFxAUserData(device.id, 0); // Reset device registration version - if (!allowRecursion) { - Log.d(LOG_TAG, "Failure to register a device on the second try"); - break; - } - try { - doFxaRegistration(context, subscription, false); - return; - } catch (InvalidFxAState e) { - Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e); - break; - } - } - } - onError(); - } - }); - } - - private void setupListeners() throws ClassNotFoundException, NoSuchMethodException, - InvocationTargetException, IllegalAccessException { - // We have no choice but to use reflection here, sorry :( - Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher"); - Method getInstance = eventDispatcher.getMethod("getInstance"); - Object instance = getInstance.invoke(null); - Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener", - BundleEventListener.class, String[].class); - registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java deleted file mode 100644 index 0117e6320..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.mozilla.gecko.fxa; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; - -public class FxAccountPushHandler { - private static final String LOG_TAG = "FxAccountPush"; - - private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected"; - private static final String COMMAND_COLLECTION_CHANGED = "sync:collection_changed"; - - private static final String CLIENTS_COLLECTION = "clients"; - - // Forbid instantiation - private FxAccountPushHandler() {} - - public static void handleFxAPushMessage(Context context, Bundle bundle) { - Log.i(LOG_TAG, "Handling FxA Push Message"); - String rawMessage = bundle.getString("message"); - JSONObject message = null; - if (!TextUtils.isEmpty(rawMessage)) { - try { - message = new JSONObject(rawMessage); - } catch (JSONException e) { - Log.e(LOG_TAG, "Could not parse JSON", e); - return; - } - } - if (message == null) { - // An empty body means we should check the verification state of the account (FxA sends this - // when the account email is verified for example). - // TODO: We're only registering the push endpoint when we are in the Married state, that's why we're skipping the message :( - Log.d(LOG_TAG, "Skipping empty message"); - return; - } - try { - String command = message.getString("command"); - JSONObject data = message.getJSONObject("data"); - switch (command) { - case COMMAND_DEVICE_DISCONNECTED: - handleDeviceDisconnection(context, data); - break; - case COMMAND_COLLECTION_CHANGED: - handleCollectionChanged(context, data); - break; - default: - Log.d(LOG_TAG, "No handler defined for FxA Push command " + command); - break; - } - } catch (JSONException e) { - Log.e(LOG_TAG, "Error while handling FxA push notification", e); - } - } - - private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException { - JSONArray collections = data.getJSONArray("collections"); - int len = collections.length(); - for (int i = 0; i < len; i++) { - if (collections.getString(i).equals(CLIENTS_COLLECTION)) { - final Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account == null) { - Log.e(LOG_TAG, "The account does not exist anymore"); - return; - } - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null); - return; - } - } - } - - private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException { - final Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account == null) { - Log.e(LOG_TAG, "The account does not exist anymore"); - return; - } - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - if (!fxAccount.getDeviceId().equals(data.getString("id"))) { - Log.e(LOG_TAG, "The device ID to disconnect doesn't match with the local device ID.\n" - + "Local: " + fxAccount.getDeviceId() + ", ID to disconnect: " + data.getString("id")); - return; - } - AccountManager.get(context).removeAccount(account, null, null); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java deleted file mode 100644 index 2f70a363a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/SyncStatusListener.java +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa; - -import android.accounts.Account; -import android.content.Context; -import android.support.annotation.UiThread; - -/** - * Interface definition for a callback to be invoked when an sync status change. - */ -public interface SyncStatusListener { - public Context getContext(); - public Account getAccount(); - - /** - * Called when sync has started. - * This is always called in UiThread. - */ - @UiThread - public void onSyncStarted(); - - /** - * Called when sync has finished. - * This is always called in UiThread. - */ - @UiThread - public void onSyncFinished(); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java deleted file mode 100644 index 5c4d7f3cc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/CustomColorPreference.java +++ /dev/null @@ -1,52 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import org.mozilla.gecko.R; -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.Preference; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TextView; - - /** - * This preference is used to define custom colors for both title and summary texts. - * Color code #777777 (placeholder_grey) is used as the fallback color for both title and summary. - */ -public class CustomColorPreference extends Preference { - private int mTitleColor; - private int mSummaryColor; - - public CustomColorPreference(Context context) { - super(context); - } - - public CustomColorPreference(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public CustomColorPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context, attrs); - } - - public void init(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomColorPreference); - mTitleColor = a.getColor(R.styleable.CustomColorPreference_titleColor, R.color.placeholder_grey); - mSummaryColor = a.getColor(R.styleable.CustomColorPreference_summaryColor, R.color.placeholder_grey); - a.recycle(); - } - - @Override - protected void onBindView(View view) { - super.onBindView(view); - final TextView title = (TextView) view.findViewById(android.R.id.title); - final TextView summary = (TextView) view.findViewById(android.R.id.summary); - title.setTextColor(mTitleColor); - summary.setTextColor(mSummaryColor); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java deleted file mode 100644 index fc8cbf0da..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountAbstractActivity.java +++ /dev/null @@ -1,80 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import android.accounts.Account; -import android.app.Activity; -import android.content.Intent; - -import org.mozilla.gecko.Locales.LocaleAwareActivity; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; - -public abstract class FxAccountAbstractActivity extends LocaleAwareActivity { - private static final String LOG_TAG = FxAccountAbstractActivity.class.getSimpleName(); - - protected final boolean cannotResumeWhenAccountsExist; - protected final boolean cannotResumeWhenNoAccountsExist; - - public static final int CAN_ALWAYS_RESUME = 0; - public static final int CANNOT_RESUME_WHEN_ACCOUNTS_EXIST = 1 << 0; - public static final int CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST = 1 << 1; - - public FxAccountAbstractActivity(int resume) { - super(); - this.cannotResumeWhenAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_ACCOUNTS_EXIST); - this.cannotResumeWhenNoAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST); - } - - /** - * Many Firefox Accounts activities shouldn't display if an account already - * exists. This function redirects as appropriate. - * - * @return true if redirected. - */ - protected boolean redirectIfAppropriate() { - if (cannotResumeWhenAccountsExist || cannotResumeWhenNoAccountsExist) { - final Account account = FirefoxAccounts.getFirefoxAccount(this); - if (cannotResumeWhenAccountsExist && account != null) { - redirectToAction(FxAccountConstants.ACTION_FXA_STATUS); - return true; - } - if (cannotResumeWhenNoAccountsExist && account == null) { - redirectToAction(FxAccountConstants.ACTION_FXA_GET_STARTED); - return true; - } - } - return false; - } - - @Override - public void onResume() { - super.onResume(); - redirectIfAppropriate(); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - overridePendingTransition(0, 0); - } - - protected void launchActivity(Class<? extends Activity> activityClass) { - Intent intent = new Intent(this, activityClass); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - } - - protected void redirectToAction(final String action) { - final Intent intent = new Intent(action); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - finish(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java deleted file mode 100644 index b2afd9c5a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountConfirmAccountActivityWeb.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity { - public FxAccountConfirmAccountActivityWeb() { - super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "manage"); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java deleted file mode 100644 index 0e66f1d6c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountFinishMigratingActivityWeb.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -public class FxAccountFinishMigratingActivityWeb extends FxAccountWebFlowActivity { - public FxAccountFinishMigratingActivityWeb() { - super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "signin", "migration=sync11"); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java deleted file mode 100644 index 39a907a44..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountGetStartedActivityWeb.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity { - public FxAccountGetStartedActivityWeb() { - super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signup"); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java deleted file mode 100644 index 4bb929f0a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusActivity.java +++ /dev/null @@ -1,228 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.widget.Toolbar; -import android.util.TypedValue; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.Window; -import android.widget.Toast; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.Locales.LocaleAwareAppCompatActivity; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.sync.Utils; - -/** - * Activity which displays account status. - */ -public class FxAccountStatusActivity extends LocaleAwareAppCompatActivity { - private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName(); - - protected FxAccountStatusFragment statusFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Display the fragment as the content. - statusFragment = new FxAccountStatusFragment(); - getSupportFragmentManager() - .beginTransaction() - .replace(android.R.id.content, statusFragment) - .commit(); - - maybeSetHomeButtonEnabled(); - } - - /** - * Sufficiently recent Android versions need additional code to receive taps - * on the status bar to go "up". See <a - * href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for - * more information. - */ - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - protected void maybeSetHomeButtonEnabled() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - Logger.debug(LOG_TAG, "Not enabling home button; version too low."); - return; - } - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - Logger.debug(LOG_TAG, "Enabling home button."); - actionBar.setHomeButtonEnabled(true); - actionBar.setDisplayHomeAsUpEnabled(true); - return; - } - Logger.debug(LOG_TAG, "Not enabling home button."); - } - - @Override - public void onResume() { - super.onResume(); - - final AndroidFxAccount fxAccount = getAndroidFxAccount(); - if (fxAccount == null) { - Logger.warn(LOG_TAG, "Could not get Firefox Account."); - - // Gracefully redirect to get started. - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - - setResult(RESULT_CANCELED); - finish(); - return; - } - statusFragment.refresh(fxAccount); - } - - /** - * Helper to fetch (unique) Android Firefox Account if one exists, or return null. - */ - protected AndroidFxAccount getAndroidFxAccount() { - Account account = FirefoxAccounts.getFirefoxAccount(this); - if (account == null) { - return null; - } - return new AndroidFxAccount(this, account); - } - - - /** - * Helper function to maybe remove the given Android account. - */ - @SuppressLint("InlinedApi") - public static void maybeDeleteAndroidAccount(final Activity activity, final Account account, final Intent intent) { - if (account == null) { - Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request."); - return; - } - - final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() { - @Override - public void run(AccountManagerFuture<Boolean> future) { - Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed."); - final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name); - Toast.makeText(activity, text, Toast.LENGTH_LONG).show(); - if (intent != null) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); - } - activity.finish(); - } - }; - - /* - * Get the best dialog icon from the theme on v11+. - * See http://stackoverflow.com/questions/14910536/android-dialog-theme-makes-icon-too-light/14910945#14910945. - */ - final int icon; - final TypedValue typedValue = new TypedValue(); - activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true); - icon = typedValue.resourceId; - - final AlertDialog dialog = new AlertDialog.Builder(activity) - .setTitle(R.string.fxaccount_remove_account_dialog_title) - .setIcon(icon) - .setMessage(R.string.fxaccount_remove_account_dialog_message) - .setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - AccountManager.get(activity).removeAccount(account, callback, null); - } - }) - .setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }) - .create(); - - dialog.show(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - finish(); - return true; - } - - if (itemId == R.id.enable_debug_mode) { - FxAccountUtils.LOG_PERSONAL_INFORMATION = !FxAccountUtils.LOG_PERSONAL_INFORMATION; - Toast.makeText(this, (FxAccountUtils.LOG_PERSONAL_INFORMATION ? "Enabled" : "Disabled") + - " Firefox Account personal information!", Toast.LENGTH_LONG).show(); - item.setChecked(!item.isChecked()); - // Display or hide debug options. - statusFragment.hardRefresh(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - final MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.fxaccount_status_menu, menu); - // !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) || defined(MOZ_DEBUG) - boolean enabled = !AppConstants.MOZILLA_OFFICIAL || AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG_BUILD; - if (!enabled) { - menu.removeItem(R.id.enable_debug_mode); - } else { - final MenuItem debugModeItem = menu.findItem(R.id.enable_debug_mode); - if (debugModeItem != null) { - // Update checked state based on internal flag. - menu.findItem(R.id.enable_debug_mode).setChecked(FxAccountUtils.LOG_PERSONAL_INFORMATION); - } - } - return super.onCreateOptionsMenu(menu); - }; - - @Override - public void openOptionsMenu() { - // This is a workaround of an Android bug: - // https://code.google.com/p/android/issues/detail?id=185217 - // openOptionsMenu isn't overriden by WindowDecorActionBar, which is used by AppCompatActivity, - // meaning getSupportActionbar().openOptionsMenu doesn't work. - // Based loosely on the code in: - // http://androidxref.com/6.0.1_r10/xref/frameworks/support/v7/appcompat/src/android/support/v7/internal/app/WindowDecorActionBar.java#getDecorToolbar - - final Window window = getWindow(); - final View decor = window.getDecorView(); - final View view = decor.findViewById(R.id.action_bar); - - if (view instanceof Toolbar) { - final Toolbar toolbar = (Toolbar) view; - toolbar.showOverflowMenu(); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java deleted file mode 100644 index a30b92e5f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java +++ /dev/null @@ -1,949 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import android.accounts.Account; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceCategory; -import android.preference.PreferenceScreen; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.text.format.DateUtils; - -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.background.preferences.PreferenceFragment; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.SyncStatusListener; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.Married; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; -import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.setup.activities.ActivityUtils; -import org.mozilla.gecko.util.HardwareUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * A fragment that displays the status of an AndroidFxAccount. - * <p> - * The owning activity is responsible for providing an AndroidFxAccount at - * appropriate times. - */ -public class FxAccountStatusFragment - extends PreferenceFragment - implements OnPreferenceClickListener, OnPreferenceChangeListener { - private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName(); - - /** - * If a device claims to have synced before this date, we will assume it has never synced. - */ - private static final Date EARLIEST_VALID_SYNCED_DATE; - - static { - final Calendar c = GregorianCalendar.getInstance(); - c.set(2000, Calendar.JANUARY, 1, 0, 0, 0); - EARLIEST_VALID_SYNCED_DATE = c.getTime(); - } - - // When a checkbox is toggled, wait 5 seconds (for other checkbox actions) - // before trying to sync. Should we kill off the fragment before the sync - // request happens, that's okay: the runnable will run if the UI thread is - // still around to service it, and since we're not updating any UI, we'll just - // schedule the sync as usual. See also comment below about garbage - // collection. - private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000; - private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000; - private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000; - - private static final String[] STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE = new String[] { "clients" }; - - // By default, the auth/account server preference is only shown when the - // account is configured to use a custom server. In debug mode, this is set. - private static boolean ALWAYS_SHOW_AUTH_SERVER = false; - - // By default, the Sync server preference is only shown when the account is - // configured to use a custom Sync server. In debug mode, this is set. - private static boolean ALWAYS_SHOW_SYNC_SERVER = false; - - protected PreferenceCategory accountCategory; - protected Preference profilePreference; - protected Preference manageAccountPreference; - protected Preference authServerPreference; - protected Preference removeAccountPreference; - - protected Preference needsPasswordPreference; - protected Preference needsUpgradePreference; - protected Preference needsVerificationPreference; - protected Preference needsMasterSyncAutomaticallyEnabledPreference; - protected Preference needsFinishMigratingPreference; - - protected PreferenceCategory syncCategory; - - protected CheckBoxPreference bookmarksPreference; - protected CheckBoxPreference historyPreference; - protected CheckBoxPreference tabsPreference; - protected CheckBoxPreference passwordsPreference; - protected CheckBoxPreference readingListPreference; - - protected EditTextPreference deviceNamePreference; - protected Preference syncServerPreference; - protected Preference morePreference; - protected Preference syncNowPreference; - - protected volatile AndroidFxAccount fxAccount; - // The contract is: when fxAccount is non-null, then clientsDataDelegate is - // non-null. If violated then an IllegalStateException is thrown. - protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate; - - // Used to post delayed sync requests. - protected Handler handler; - - // Member variable so that re-posting pushes back the already posted instance. - // This Runnable references the fxAccount above, but it is not specific to a - // single account. (That is, it does not capture a single account instance.) - protected Runnable requestSyncRunnable; - - // Runnable to update last synced time. - protected Runnable lastSyncedTimeUpdateRunnable; - - // Broadcast Receiver to update profile Information. - protected FxAccountProfileInformationReceiver accountProfileInformationReceiver; - - protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate(); - private Target profileAvatarTarget; - - protected Preference ensureFindPreference(String key) { - Preference preference = findPreference(key); - if (preference == null) { - throw new IllegalStateException("Could not find preference with key: " + key); - } - return preference; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // We need to do this before we can query the hardware menu button state. - // We're guaranteed to have an activity at this point (onAttach is called - // before onCreate). It's okay to call this multiple times (with different - // contexts). - HardwareUtils.init(getActivity()); - - addPreferences(); - } - - protected void addPreferences() { - addPreferencesFromResource(R.xml.fxaccount_status_prefscreen); - - accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category"); - profilePreference = ensureFindPreference("profile"); - manageAccountPreference = ensureFindPreference("manage_account"); - authServerPreference = ensureFindPreference("auth_server"); - removeAccountPreference = ensureFindPreference("remove_account"); - - needsPasswordPreference = ensureFindPreference("needs_credentials"); - needsUpgradePreference = ensureFindPreference("needs_upgrade"); - needsVerificationPreference = ensureFindPreference("needs_verification"); - needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled"); - needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating"); - - syncCategory = (PreferenceCategory) ensureFindPreference("sync_category"); - - bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks"); - historyPreference = (CheckBoxPreference) ensureFindPreference("history"); - tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs"); - passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords"); - - if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) { - removeDebugButtons(); - } else { - connectDebugButtons(); - ALWAYS_SHOW_AUTH_SERVER = true; - ALWAYS_SHOW_SYNC_SERVER = true; - } - - profilePreference.setOnPreferenceClickListener(this); - manageAccountPreference.setOnPreferenceClickListener(this); - removeAccountPreference.setOnPreferenceClickListener(this); - - needsPasswordPreference.setOnPreferenceClickListener(this); - needsVerificationPreference.setOnPreferenceClickListener(this); - needsFinishMigratingPreference.setOnPreferenceClickListener(this); - - bookmarksPreference.setOnPreferenceClickListener(this); - historyPreference.setOnPreferenceClickListener(this); - tabsPreference.setOnPreferenceClickListener(this); - passwordsPreference.setOnPreferenceClickListener(this); - - deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name"); - deviceNamePreference.setOnPreferenceChangeListener(this); - - syncServerPreference = ensureFindPreference("sync_server"); - morePreference = ensureFindPreference("more"); - morePreference.setOnPreferenceClickListener(this); - - syncNowPreference = ensureFindPreference("sync_now"); - syncNowPreference.setEnabled(true); - syncNowPreference.setOnPreferenceClickListener(this); - - ensureFindPreference("linktos").setOnPreferenceClickListener(this); - ensureFindPreference("linkprivacy").setOnPreferenceClickListener(this); - } - - /** - * We intentionally don't refresh here. Our owning activity is responsible for - * providing an AndroidFxAccount to our refresh method in its onResume method. - */ - @Override - public void onResume() { - super.onResume(); - } - - @Override - public boolean onPreferenceClick(Preference preference) { - if (preference == profilePreference) { - ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=avatar"); - return true; - } - - if (preference == manageAccountPreference) { - ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage"); - return true; - } - - if (preference == removeAccountPreference) { - FxAccountStatusActivity.maybeDeleteAndroidAccount(getActivity(), fxAccount.getAndroidAccount(), null); - return true; - } - - if (preference == needsPasswordPreference) { - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS); - intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - - return true; - } - - if (preference == needsFinishMigratingPreference) { - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING); - intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - - return true; - } - - if (preference == needsVerificationPreference) { - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT); - // Per http://stackoverflow.com/a/8992365, this triggers a known bug with - // the soft keyboard not being shown for the started activity. Why, Android, why? - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES); - startActivity(intent); - - return true; - } - - if (preference == bookmarksPreference || - preference == historyPreference || - preference == passwordsPreference || - preference == tabsPreference) { - saveEngineSelections(); - return true; - } - - if (preference == morePreference) { - getActivity().openOptionsMenu(); - return true; - } - - if (preference == syncNowPreference) { - if (fxAccount != null) { - fxAccount.requestImmediateSync(null, null); - } - return true; - } - - if (TextUtils.equals("linktos", preference.getKey())) { - ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos)); - return true; - } - - if (TextUtils.equals("linkprivacy", preference.getKey())) { - ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_pn)); - return true; - } - - return false; - } - - protected void setCheckboxesEnabled(boolean enabled) { - bookmarksPreference.setEnabled(enabled); - historyPreference.setEnabled(enabled); - tabsPreference.setEnabled(enabled); - passwordsPreference.setEnabled(enabled); - // Since we can't sync, we can't update our remote client record. - deviceNamePreference.setEnabled(enabled); - syncNowPreference.setEnabled(enabled); - } - - /** - * Show at most one error preference, hiding all others. - * - * @param errorPreferenceToShow - * single error preference to show; if null, hide all error preferences - */ - protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) { - final Preference[] errorPreferences = new Preference[] { - this.needsPasswordPreference, - this.needsUpgradePreference, - this.needsVerificationPreference, - this.needsMasterSyncAutomaticallyEnabledPreference, - this.needsFinishMigratingPreference, - }; - for (Preference errorPreference : errorPreferences) { - final boolean currentlyShown = null != findPreference(errorPreference.getKey()); - final boolean shouldBeShown = errorPreference == errorPreferenceToShow; - if (currentlyShown == shouldBeShown) { - continue; - } - if (shouldBeShown) { - syncCategory.addPreference(errorPreference); - } else { - syncCategory.removePreference(errorPreference); - } - } - } - - protected void showNeedsPassword() { - syncCategory.setTitle(R.string.fxaccount_status_sync); - showOnlyOneErrorPreference(needsPasswordPreference); - setCheckboxesEnabled(false); - } - - protected void showNeedsUpgrade() { - syncCategory.setTitle(R.string.fxaccount_status_sync); - showOnlyOneErrorPreference(needsUpgradePreference); - setCheckboxesEnabled(false); - } - - protected void showNeedsVerification() { - syncCategory.setTitle(R.string.fxaccount_status_sync); - showOnlyOneErrorPreference(needsVerificationPreference); - setCheckboxesEnabled(false); - } - - protected void showNeedsMasterSyncAutomaticallyEnabled() { - syncCategory.setTitle(R.string.fxaccount_status_sync); - needsMasterSyncAutomaticallyEnabledPreference.setTitle(AppConstants.Versions.preLollipop ? - R.string.fxaccount_status_needs_master_sync_automatically_enabled : - R.string.fxaccount_status_needs_master_sync_automatically_enabled_v21); - showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference); - setCheckboxesEnabled(false); - } - - protected void showNeedsFinishMigrating() { - syncCategory.setTitle(R.string.fxaccount_status_sync); - showOnlyOneErrorPreference(needsFinishMigratingPreference); - setCheckboxesEnabled(false); - } - - protected void showConnected() { - syncCategory.setTitle(R.string.fxaccount_status_sync_enabled); - showOnlyOneErrorPreference(null); - setCheckboxesEnabled(true); - } - - protected class InnerSyncStatusDelegate implements SyncStatusListener { - protected final Runnable refreshRunnable = new Runnable() { - @Override - public void run() { - refresh(); - } - }; - - @Override - public Context getContext() { - return FxAccountStatusFragment.this.getActivity(); - } - - @Override - public Account getAccount() { - return fxAccount.getAndroidAccount(); - } - - @Override - public void onSyncStarted() { - if (fxAccount == null) { - return; - } - Logger.info(LOG_TAG, "Got sync started message; refreshing."); - getActivity().runOnUiThread(refreshRunnable); - } - - @Override - public void onSyncFinished() { - if (fxAccount == null) { - return; - } - Logger.info(LOG_TAG, "Got sync finished message; refreshing."); - getActivity().runOnUiThread(refreshRunnable); - } - } - - /** - * Notify the fragment that a new AndroidFxAccount instance is current. - * <p> - * <b>Important:</b> call this method on the UI thread! - * <p> - * In future, this might be a Loader. - * - * @param fxAccount new instance. - */ - public void refresh(AndroidFxAccount fxAccount) { - if (fxAccount == null) { - throw new IllegalArgumentException("fxAccount must not be null"); - } - this.fxAccount = fxAccount; - try { - this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), getActivity().getApplicationContext()); - } catch (Exception e) { - Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e); - // Something is terribly wrong; best to get a stack trace rather than - // continue with a null clients delegate. - throw new IllegalStateException(e); - } - - handler = new Handler(); // Attached to current (assumed to be UI) thread. - - // Runnable is not specific to one Firefox Account. This runnable will keep - // a reference to this fragment alive, but we expect posted runnables to be - // serviced very quickly, so this is not an issue. - requestSyncRunnable = new RequestSyncRunnable(); - lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable(); - - // We would very much like register these status observers in bookended - // onResume/onPause calls, but because the Fragment gets onResume during the - // Activity's super.onResume, it hasn't yet been told its Firefox Account. - // So we register the observer here (and remove it in onPause), and open - // ourselves to the possibility that we don't have properly paired - // register/unregister calls. - FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate); - - // Register a local broadcast receiver to get profile cached notification. - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION); - accountProfileInformationReceiver = new FxAccountProfileInformationReceiver(); - LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter); - - // profilePreference is set during onCreate, so it's definitely not null here. - final float cornerRadius = getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2; - profileAvatarTarget = new PicassoPreferenceIconTarget(getResources(), profilePreference, cornerRadius); - - refresh(); - } - - @Override - public void onPause() { - super.onPause(); - FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate); - - // Focus lost, remove scheduled update if any. - if (lastSyncedTimeUpdateRunnable != null) { - handler.removeCallbacks(lastSyncedTimeUpdateRunnable); - } - - // Focus lost, unregister broadcast receiver. - if (accountProfileInformationReceiver != null) { - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver); - } - - if (profileAvatarTarget != null) { - Picasso.with(getActivity()).cancelRequest(profileAvatarTarget); - profileAvatarTarget = null; - } - } - - protected void hardRefresh() { - // This is the only way to guarantee that the EditText dialogs created by - // EditTextPreferences are re-created. This works around the issue described - // at http://androiddev.orkitra.com/?p=112079. - final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); - statusScreen.removeAll(); - addPreferences(); - - refresh(); - } - - protected void refresh() { - // refresh is called from our onResume, which can happen before the owning - // Activity tells us about an account (via our public - // refresh(AndroidFxAccount) method). - if (fxAccount == null) { - throw new IllegalArgumentException("fxAccount must not be null"); - } - - updateProfileInformation(); - updateAuthServerPreference(); - updateSyncServerPreference(); - - try { - // There are error states determined by Android, not the login state - // machine, and we have a chance to present these states here. We handle - // them specially, since we can't surface these states as part of syncing, - // because they generally stop syncs from happening regularly. Right now - // there are no such states. - - // Interrogate the Firefox Account's state. - State state = fxAccount.getState(); - switch (state.getNeededAction()) { - case NeedsUpgrade: - showNeedsUpgrade(); - break; - case NeedsPassword: - showNeedsPassword(); - break; - case NeedsVerification: - showNeedsVerification(); - break; - case NeedsFinishMigrating: - showNeedsFinishMigrating(); - break; - case None: - showConnected(); - break; - } - - // We check for the master setting last, since it is not strictly - // necessary for the user to address this error state: it's really a - // warning state. We surface it for the user's convenience, and to prevent - // confused folks wondering why Sync is not working at all. - final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); - if (!masterSyncAutomatically) { - showNeedsMasterSyncAutomaticallyEnabled(); - return; - } - } finally { - // No matter our state, we should update the checkboxes. - updateSelectedEngines(); - } - - final String clientName = clientsDataDelegate.getClientName(); - deviceNamePreference.setSummary(clientName); - deviceNamePreference.setText(clientName); - - updateSyncNowPreference(); - } - - // This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span. - private String getLastSyncedString(final long startTime) { - if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) { - return getActivity().getString(R.string.fxaccount_status_never_synced); - } - final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime); - return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString); - } - - protected void updateSyncNowPreference() { - final boolean currentlySyncing = fxAccount.isCurrentlySyncing(); - syncNowPreference.setEnabled(!currentlySyncing); - if (currentlySyncing) { - syncNowPreference.setTitle(R.string.fxaccount_status_syncing); - } else { - syncNowPreference.setTitle(R.string.fxaccount_status_sync_now); - } - scheduleAndUpdateLastSyncedTime(); - } - - private void updateProfileInformation() { - - final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON(); - if (profileJSON == null) { - // Update the profile title with email as the fallback. - // Profile icon by default use the default avatar as the fallback. - profilePreference.setTitle(fxAccount.getEmail()); - return; - } - - updateProfileInformation(profileJSON); - } - - /** - * Update profile information from json on UI thread. - * - * @param profileJSON json fetched from server. - */ - protected void updateProfileInformation(final ExtendedJSONObject profileJSON) { - // View changes must always be done on UI thread. - ThreadUtils.assertOnUiThread(); - - FxAccountUtils.pii(LOG_TAG, "Profile JSON is: " + profileJSON.toJSONString()); - - final String userName = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME); - // Update the profile username and email if available. - if (!TextUtils.isEmpty(userName)) { - profilePreference.setTitle(userName); - profilePreference.setSummary(fxAccount.getEmail()); - } else { - profilePreference.setTitle(fxAccount.getEmail()); - } - - // Avatar URI empty, skip profile image fetch. - final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR); - if (TextUtils.isEmpty(avatarURI)) { - Logger.info(LOG_TAG, "AvatarURI is empty, skipping profile image fetch."); - return; - } - - // Using noPlaceholder would avoid a pop of the default image, but it's not available in the version of Picasso - // we ship in the tree. - Picasso - .with(getActivity()) - .load(avatarURI) - .centerInside() - .resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height) - .placeholder(R.drawable.sync_avatar_default) - .error(R.drawable.sync_avatar_default) - .into(profileAvatarTarget); - } - - private void scheduleAndUpdateLastSyncedTime() { - final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp()); - syncNowPreference.setSummary(lastSynced); - handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS); - } - - protected void updateAuthServerPreference() { - final String authServer = fxAccount.getAccountServerURI(); - final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer); - final boolean currentlyShown = null != findPreference(authServerPreference.getKey()); - if (currentlyShown != shouldBeShown) { - if (shouldBeShown) { - accountCategory.addPreference(authServerPreference); - } else { - accountCategory.removePreference(authServerPreference); - } - } - // Always set the summary, because on first run, the preference is visible, - // and the above block will be skipped if there is a custom value. - authServerPreference.setSummary(authServer); - } - - protected void updateSyncServerPreference() { - final String syncServer = fxAccount.getTokenServerURI(); - final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer); - final boolean currentlyShown = null != findPreference(syncServerPreference.getKey()); - if (currentlyShown != shouldBeShown) { - if (shouldBeShown) { - syncCategory.addPreference(syncServerPreference); - } else { - syncCategory.removePreference(syncServerPreference); - } - } - // Always set the summary, because on first run, the preference is visible, - // and the above block will be skipped if there is a custom value. - syncServerPreference.setSummary(syncServer); - } - - /** - * Query shared prefs for the current engine state, and update the UI - * accordingly. - * <p> - * In future, we might want this to be on a background thread, or implemented - * as a Loader. - */ - protected void updateSelectedEngines() { - try { - SharedPreferences syncPrefs = fxAccount.getSyncPrefs(); - Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs); - if (engines != null) { - bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks")); - historyPreference.setChecked(engines.containsKey("history") && engines.get("history")); - passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords")); - tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs")); - return; - } - - // We don't have user specified preferences. Perhaps we have seen a meta/global? - Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs); - if (enabledNames != null) { - bookmarksPreference.setChecked(enabledNames.contains("bookmarks")); - historyPreference.setChecked(enabledNames.contains("history")); - passwordsPreference.setChecked(enabledNames.contains("passwords")); - tabsPreference.setChecked(enabledNames.contains("tabs")); - return; - } - - // Okay, we don't have userSelectedEngines or enabledEngines. That means - // the user hasn't specified to begin with, we haven't specified here, and - // we haven't already seen, Sync engines. We don't know our state, so - // let's check everything (the default) and disable everything. - bookmarksPreference.setChecked(true); - historyPreference.setChecked(true); - passwordsPreference.setChecked(true); - tabsPreference.setChecked(true); - setCheckboxesEnabled(false); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e); - return; - } - } - - /** - * Persist engine selections to local shared preferences, and request a sync - * to persist selections to remote storage. - */ - protected void saveEngineSelections() { - final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>(); - engineSelections.put("bookmarks", bookmarksPreference.isChecked()); - engineSelections.put("history", historyPreference.isChecked()); - engineSelections.put("passwords", passwordsPreference.isChecked()); - engineSelections.put("tabs", tabsPreference.isChecked()); - - // No GlobalSession.config, so store directly to shared prefs. We do this on - // a background thread to avoid IO on the main thread and strict mode - // warnings. - new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start(); - } - - protected void requestDelayedSync() { - Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon."); - handler.removeCallbacks(requestSyncRunnable); - handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC); - } - - /** - * Remove all traces of debug buttons. By default, no debug buttons are shown. - */ - protected void removeDebugButtons() { - final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen"); - final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category"); - statusScreen.removePreference(debugCategory); - } - - /** - * A Runnable that persists engine selections to shared prefs, and then - * requests a delayed sync. - * <p> - * References the member <code>fxAccount</code> and is specific to the Android - * account associated to that account. - */ - protected class PersistEngineSelectionsRunnable implements Runnable { - private final Map<String, Boolean> engineSelections; - - protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) { - this.engineSelections = engineSelections; - } - - @Override - public void run() { - try { - // Name shadowing -- do you like it, or do you love it? - AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount; - if (fxAccount == null) { - return; - } - Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString()); - SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections); - requestDelayedSync(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e); - return; - } - } - } - - /** - * A Runnable that requests a sync. - * <p> - * References the member <code>fxAccount</code>, but is not specific to the - * Android account associated to that account. - */ - protected class RequestSyncRunnable implements Runnable { - @Override - public void run() { - // Name shadowing -- do you like it, or do you love it? - AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount; - if (fxAccount == null) { - return; - } - Logger.info(LOG_TAG, "Requesting a sync sometime soon."); - fxAccount.requestEventualSync(null, null); - } - } - - /** - * The Runnable that schedules a future update and updates the last synced time. - */ - protected class LastSyncTimeUpdateRunnable implements Runnable { - @Override - public void run() { - scheduleAndUpdateLastSyncedTime(); - } - } - - /** - * Broadcast receiver to receive updates for the cached profile action. - */ - public class FxAccountProfileInformationReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (!intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION)) { - return; - } - - Logger.info(LOG_TAG, "Profile avatar cache update action broadcast received."); - // Update the UI from cached profile json on the main thread. - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - updateProfileInformation(); - } - }); - } - } - - /** - * A separate listener to separate debug logic from main code paths. - */ - protected class DebugPreferenceClickListener implements OnPreferenceClickListener { - @Override - public boolean onPreferenceClick(Preference preference) { - final String key = preference.getKey(); - if ("debug_refresh".equals(key)) { - Logger.info(LOG_TAG, "Refreshing."); - refresh(); - } else if ("debug_dump".equals(key)) { - fxAccount.dump(); - } else if ("debug_force_sync".equals(key)) { - Logger.info(LOG_TAG, "Force syncing."); - fxAccount.requestImmediateSync(null, null); - // No sense refreshing, since the sync will complete in the future. - } else if ("debug_forget_certificate".equals(key)) { - State state = fxAccount.getState(); - try { - Married married = (Married) state; - Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate."); - fxAccount.setState(married.makeCohabitingState()); - refresh(); - } catch (ClassCastException e) { - Logger.info(LOG_TAG, "Not in Married state; can't forget certificate."); - // Ignore. - } - } else if ("debug_invalidate_certificate".equals(key)) { - State state = fxAccount.getState(); - try { - Married married = (Married) state; - Logger.info(LOG_TAG, "Invalidating certificate."); - fxAccount.setState(married.makeCohabitingState().withCertificate("INVALID CERTIFICATE")); - refresh(); - } catch (ClassCastException e) { - Logger.info(LOG_TAG, "Not in Married state; can't invalidate certificate."); - // Ignore. - } - } else if ("debug_require_password".equals(key)) { - Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password."); - State state = fxAccount.getState(); - fxAccount.setState(state.makeSeparatedState()); - refresh(); - } else if ("debug_require_upgrade".equals(key)) { - Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade."); - State state = fxAccount.getState(); - fxAccount.setState(state.makeDoghouseState()); - refresh(); - } else if ("debug_migrated_from_sync11".equals(key)) { - Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password."); - State state = fxAccount.getState(); - fxAccount.setState(state.makeMigratedFromSync11State(null)); - refresh(); - } else if ("debug_make_account_stage".equals(key)) { - Logger.info(LOG_TAG, "Moving Account endpoints, in place, to stage. Deleting Sync and RL prefs and requiring password."); - fxAccount.unsafeTransitionToStageEndpoints(); - refresh(); - } else if ("debug_make_account_default".equals(key)) { - Logger.info(LOG_TAG, "Moving Account endpoints, in place, to default (production). Deleting Sync and RL prefs and requiring password."); - fxAccount.unsafeTransitionToDefaultEndpoints(); - refresh(); - } else { - return false; - } - return true; - } - } - - /** - * Iterate through debug buttons, adding a special debug preference click - * listener to each of them. - */ - protected void connectDebugButtons() { - // Separate listener to really separate debug logic from main code paths. - final OnPreferenceClickListener listener = new DebugPreferenceClickListener(); - - // We don't want to use Android resource strings for debug UI, so we just - // use the keys throughout. - final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category"); - debugCategory.setTitle(debugCategory.getKey()); - - for (int i = 0; i < debugCategory.getPreferenceCount(); i++) { - final Preference button = debugCategory.getPreference(i); - button.setTitle(button.getKey()); // Not very friendly, but this is for debugging only! - button.setOnPreferenceClickListener(listener); - } - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == deviceNamePreference) { - String newClientName = (String) newValue; - if (TextUtils.isEmpty(newClientName)) { - newClientName = clientsDataDelegate.getDefaultClientName(); - } - final long now = System.currentTimeMillis(); - clientsDataDelegate.setClientName(newClientName, now); - // Force sync the client record, we want the user to see the device name change immediately - // on the FxA Device Manager if possible ( = we are online) to avoid confusion - // ("I changed my Android's device name but I don't see it on my computer"). - fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null); - hardRefresh(); // Updates the value displayed to the user, among other things. - return true; - } - - // For everything else, accept the change. - return true; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java deleted file mode 100644 index 5a2ea79c8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountUpdateCredentialsActivityWeb.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -public class FxAccountUpdateCredentialsActivityWeb extends FxAccountWebFlowActivity { - public FxAccountUpdateCredentialsActivityWeb() { - super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "force_auth"); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java deleted file mode 100644 index e33e9c577..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountWebFlowActivity.java +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import android.content.Intent; -import android.os.Bundle; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.sync.setup.activities.ActivityUtils; - -/** - * Activity which shows the status activity or passes through to web flow. - */ -public abstract class FxAccountWebFlowActivity extends FxAccountAbstractActivity { - protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName(); - - protected static final String ABOUT_ACCOUNTS = "about:accounts"; - - public static final String EXTRA_ENDPOINT = "entrypoint"; - - protected static final String[] EXTRAS_TO_PASSTHROUGH = new String[] { - EXTRA_ENDPOINT, - }; - - private final String action; - private final String extras; - - public FxAccountWebFlowActivity(int resume, String action) { - this(resume, action, null); - } - - public FxAccountWebFlowActivity(int resume, String action, String extras) { - super(resume); - this.action = action; - this.extras = (extras != null) ? ("&" + extras) : ""; - } - - /** - * {@inheritDoc} - */ - @Override - public void onCreate(Bundle icicle) { - Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); - Logger.debug(LOG_TAG, "onCreate(" + icicle + ")"); - - Locales.initializeLocale(getApplicationContext()); - - super.onCreate(icicle); - } - - protected boolean redirectIfAppropriate() { - final boolean redirected = super.redirectIfAppropriate(); - if (redirected) { - return true; - } - - final StringBuilder sb = new StringBuilder(); - sb.append(ABOUT_ACCOUNTS); - sb.append("?action="); - sb.append(action); - sb.append(extras); - - // Pass through a set of known string values from intent extras to about:accounts. - final Intent intent = getIntent(); - if (intent != null) { - for (String key : EXTRAS_TO_PASSTHROUGH) { - final String value = intent.getStringExtra(key); - if (value != null) { - sb.append("&"); - sb.append(key); - sb.append("="); - sb.append(value); - } - } - } - - ActivityUtils.openURLInFennec(getApplicationContext(), sb.toString()); - return true; - } - - @Override - public void onResume() { - super.onResume(); - - // We are always redirected. - this.finish(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java deleted file mode 100644 index f71d3ed1c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/PicassoPreferenceIconTarget.java +++ /dev/null @@ -1,63 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.activities; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.preference.Preference; -import android.support.v4.graphics.drawable.RoundedBitmapDrawable; -import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; -import org.mozilla.gecko.AppConstants; - -/** - * A Picasso Target that updates a preference icon. - * - * Nota bene: Android grew support for updating preference icons programatically - * only in API 11. This class silently ignores requests before API 11. - */ -public class PicassoPreferenceIconTarget implements Target { - private final Preference preference; - private final Resources resources; - private final float cornerRadius; - - public PicassoPreferenceIconTarget(Resources resources, Preference preference) { - this(resources, preference, 0); - } - - public PicassoPreferenceIconTarget(Resources resources, Preference preference, float cornerRadius) { - this.resources = resources; - this.preference = preference; - this.cornerRadius = cornerRadius; - } - - @Override - public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { - final Drawable drawable; - if (cornerRadius > 0) { - final RoundedBitmapDrawable roundedBitmapDrawable; - roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, bitmap); - roundedBitmapDrawable.setCornerRadius(cornerRadius); - roundedBitmapDrawable.setAntiAlias(true); - drawable = roundedBitmapDrawable; - } else { - drawable = new BitmapDrawable(resources, bitmap); - } - preference.setIcon(drawable); - } - - @Override - public void onBitmapFailed(Drawable errorDrawable) { - preference.setIcon(errorDrawable); - } - - @Override - public void onPrepareLoad(Drawable placeHolderDrawable) { - preference.setIcon(placeHolderDrawable); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java deleted file mode 100644 index 3f2c5620d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java +++ /dev/null @@ -1,362 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -import java.io.FileOutputStream; -import java.io.PrintStream; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.login.StateFactory; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; - -import android.content.Context; - -/** - * Android deletes Account objects when the Authenticator that owns the Account - * disappears. This happens when an App is installed to the SD card and the SD - * card is un-mounted or the device is rebooted. - * <p> - * We work around this by pickling the current Firefox account data every sync - * and unpickling when we check if Firefox accounts exist (called from Fennec). - * <p> - * Android just doesn't support installing Apps that define long-lived Services - * and/or own Account types onto the SD card. The documentation says not to do - * it. There are hordes of developers who want to do it, and have tried to - * register for almost every "package installation changed" broadcast intent - * that Android supports. They all explicitly state that the package that has - * changed does *not* receive the broadcast intent, thereby preventing an App - * from re-establishing its state. - * <p> - * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a> - * <p> - * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality - * will not work until external storage is remounted. - * <p> - * <b>Quote</b>: Your running Service will be killed and will not be restarted - * when external storage is remounted. You can, however, register for the - * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify - * your application when applications installed on external storage have become - * available to the system again. At which time, you can restart your Service. - * <p> - * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>! - * <p> - * See bug 768102 for more information in the context of Sync. - */ -public class AccountPickler { - public static final String LOG_TAG = AccountPickler.class.getSimpleName(); - - public static final long PICKLE_VERSION = 3; - - public static final String KEY_PICKLE_VERSION = "pickle_version"; - public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp"; - - public static final String KEY_ACCOUNT_VERSION = "account_version"; - public static final String KEY_ACCOUNT_TYPE = "account_type"; - public static final String KEY_EMAIL = "email"; - public static final String KEY_PROFILE = "profile"; - public static final String KEY_IDP_SERVER_URI = "idpServerURI"; - public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI"; - public static final String KEY_PROFILE_SERVER_URI = "profileServerURI"; - - public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap"; - - // Deprecated, but maintained for migration purposes. - public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled"; - - public static final String KEY_BUNDLE = "bundle"; - - /** - * Remove Firefox account persisted to disk. - * This operation is synchronized to avoid race condition while deleting the account. - * - * @param context Android context. - * @param filename name of persisted pickle file; must not contain path separators. - * @return <code>true</code> if given pickle existed and was successfully deleted. - */ - public synchronized static boolean deletePickle(final Context context, final String filename) { - return context.deleteFile(filename); - } - - public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) { - final ExtendedJSONObject o = new ExtendedJSONObject(); - o.put(KEY_PICKLE_VERSION, PICKLE_VERSION); - o.put(KEY_PICKLE_TIMESTAMP, now); - - o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION); - o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE); - o.put(KEY_EMAIL, account.getEmail()); - o.put(KEY_PROFILE, account.getProfile()); - o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI()); - o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI()); - o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI()); - - final ExtendedJSONObject p = new ExtendedJSONObject(); - for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) { - p.put(pair.getKey(), pair.getValue()); - } - o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p); - - // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs. - - final ExtendedJSONObject bundle = account.unbundle(); - if (bundle == null) { - Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting."); - return null; - } - o.put(KEY_BUNDLE, bundle); - - return o; - } - - /** - * Persist Firefox account to disk as a JSON object. - * This operation is synchronized to avoid race condition while deleting the account. - * - * @param account the AndroidFxAccount to persist to disk - * @param filename name of file to persist to; must not contain path separators. - */ - public synchronized static void pickle(final AndroidFxAccount account, final String filename) { - final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis()); - writeToDisk(account.context, filename, o); - } - - private static void writeToDisk(final Context context, final String filename, - final ExtendedJSONObject pickle) { - try { - final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); - try { - final PrintStream ps = new PrintStream(fos); - try { - ps.print(pickle.toJSONString()); - Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() + - " account settings to " + filename + "."); - } finally { - ps.close(); - } - } finally { - fos.close(); - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + - "; ignoring.", e); - } - } - - /** - * Create Android account from saved JSON object. Assumes that an account does not exist. - * This operation is synchronized to avoid race condition while deleting the account. - * - * @param context - * Android context. - * @param filename - * name of file to read from; must not contain path separators. - * @return created Android account, or null on error. - */ - public synchronized static AndroidFxAccount unpickle(final Context context, final String filename) { - final String jsonString = Utils.readFile(context, filename); - if (jsonString == null) { - Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting."); - return null; - } - - ExtendedJSONObject json = null; - try { - json = new ExtendedJSONObject(jsonString); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e); - return null; - } - - final UnpickleParams params; - try { - params = UnpickleParams.fromJSON(json); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e); - return null; - } - - final AndroidFxAccount account; - try { - account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile, - params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state, - params.authoritiesToSyncAutomaticallyMap, - params.accountVersion, - true, params.bundle); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e); - return null; - } - - if (account == null) { - Logger.warn(LOG_TAG, "Failed to add Android Account; aborting."); - return null; - } - - Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP); - if (timestamp == null) { - Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring."); - timestamp = -1L; - } - - Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " + - params.pickleVersion + ", pickled at " + timestamp + ")."); - - return account; - } - - private static class UnpickleParams { - private Long pickleVersion; - - private int accountVersion; - private String email; - private String profile; - private String authServerURI; - private String tokenServerURI; - private String profileServerURI; - private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>(); - - private ExtendedJSONObject bundle; - private State state; - - private UnpickleParams() { - } - - private static UnpickleParams fromJSON(final ExtendedJSONObject json) - throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { - final UnpickleParams params = new UnpickleParams(); - params.pickleVersion = json.getLong(KEY_PICKLE_VERSION); - if (params.pickleVersion == null) { - throw new IllegalStateException("Pickle version not found."); - } - - /* - * Version 1 and version 2 are identical, except version 2 throws if the - * internal Android Account type has changed. Version 1 used to throw in - * this case, but we intentionally used the pickle file to migrate across - * Account types, bumping the version simultaneously. - * - * Version 3 replaces "isSyncEnabled" with a map (String -> Boolean) - * associating Android authorities to whether or not they are configured - * to sync automatically. - */ - switch (params.pickleVersion.intValue()) { - case 3: { - // Sanity check. - final String accountType = json.getString(KEY_ACCOUNT_TYPE); - if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { - throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "."); - } - - params.unpickleV3(json); - } - break; - - case 2: { - // Sanity check. - final String accountType = json.getString(KEY_ACCOUNT_TYPE); - if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { - throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "."); - } - - params.unpickleV1(json); - } - break; - - case 1: { - // Warn about account type changing, but don't throw over it. - final String accountType = json.getString(KEY_ACCOUNT_TYPE); - if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { - Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring."); - } - - params.unpickleV1(json); - } - break; - - default: - throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + "."); - } - - return params; - } - - private void unpickleV1(final ExtendedJSONObject json) - throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException { - - this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION); - this.email = json.getString(KEY_EMAIL); - this.profile = json.getString(KEY_PROFILE); - this.authServerURI = json.getString(KEY_IDP_SERVER_URI); - this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI); - this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI); - - // Fallback to default value when profile server URI was not pickled. - if (this.profileServerURI == null) { - this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI) - ? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT - : FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT; - } - - // We get the default value for everything except syncing browser data. - this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED)); - - this.bundle = json.getObject(KEY_BUNDLE); - if (bundle == null) { - throw new IllegalStateException("Pickle bundle is null."); - } - this.state = getState(bundle); - } - - private void unpickleV3(final ExtendedJSONObject json) - throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException { - // We'll overwrite the extracted sync automatically map. - unpickleV1(json); - - // Extract the map of authorities to sync automatically. - authoritiesToSyncAutomaticallyMap.clear(); - final ExtendedJSONObject o = json.getObject(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP); - if (o == null) { - return; - } - for (String key : o.keySet()) { - final Boolean enabled = o.getBoolean(key); - if (enabled != null) { - authoritiesToSyncAutomaticallyMap.put(key, enabled); - } - } - } - - private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException, - NonObjectJSONException, NoSuchAlgorithmException { - // TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain - // old versions? - final StateLabel stateLabelString = StateLabel.valueOf( - bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL)); - final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE); - if (stateLabelString == null || stateString == null) { - throw new IllegalStateException("stateLabel and stateString must not be null, but: " + - "(stateLabel == null) = " + (stateLabelString == null) + - " and (stateString == null) = " + (stateString == null)); - } - - try { - return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString)); - } catch (Exception e) { - throw new IllegalStateException("could not get state", e); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java deleted file mode 100644 index d7ce7c47f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java +++ /dev/null @@ -1,929 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.LocalBroadcastManager; -import android.text.TextUtils; -import android.util.Log; - -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.login.StateFactory; -import org.mozilla.gecko.fxa.login.TokensAndKeysState; -import org.mozilla.gecko.fxa.sync.FxAccountProfileService; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.setup.Constants; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.UnsupportedEncodingException; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Semaphore; - -/** - * A Firefox Account that stores its details and state as user data attached to - * an Android Account instance. - * <p> - * Account user data is accessible only to the Android App(s) that own the - * Account type. Account user data is not removed when the App's private data is - * cleared. - */ -public class AndroidFxAccount { - protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName(); - - public static final int CURRENT_SYNC_PREFS_VERSION = 1; - public static final int CURRENT_RL_PREFS_VERSION = 1; - - // When updating the account, do not forget to update AccountPickler. - public static final int CURRENT_ACCOUNT_VERSION = 3; - public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version"; - public static final String ACCOUNT_KEY_PROFILE = "profile"; - public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI"; - private static final String ACCOUNT_KEY_PROFILE_SERVER = "profileServerURI"; - - public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI"; // Sync-specific. - public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor"; - - public static final int CURRENT_BUNDLE_VERSION = 2; - public static final String BUNDLE_KEY_BUNDLE_VERSION = "version"; - public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel"; - public static final String BUNDLE_KEY_STATE = "state"; - public static final String BUNDLE_KEY_PROFILE_JSON = "profile"; - - public static final String ACCOUNT_KEY_DEVICE_ID = "deviceId"; - public static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion"; - - // Account authentication token type for fetching account profile. - public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile"; - - // Services may request OAuth tokens from the Firefox Account dynamically. - // Each such token is prefixed with "oauth::" and a service-dependent scope. - // Such tokens should be destroyed when the account is removed from the device. - // This list collects all the known "oauth::" token types in order to delete them when necessary. - private static final List<String> KNOWN_OAUTH_TOKEN_TYPES; - - static { - final List<String> list = new ArrayList<>(); - list.add(PROFILE_OAUTH_TOKEN_TYPE); - KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list); - } - - public static final Map<String, Boolean> DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP; - static { - final HashMap<String, Boolean> m = new HashMap<String, Boolean>(); - // By default, Firefox Sync is enabled. - m.put(BrowserContract.AUTHORITY, true); - DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = Collections.unmodifiableMap(m); - } - - private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp"; - - protected final Context context; - protected final AccountManager accountManager; - protected final Account account; - - /** - * A cache associating Account name (email address) to a representation of the - * account's internal bundle. - * <p> - * The cache is invalidated entirely when <it>any</it> new Account is added, - * because there is no reliable way to know that an Account has been removed - * and then re-added. - */ - protected static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache = - new ConcurrentHashMap<>(); - - public static void invalidateCaches() { - perAccountBundleCache.clear(); - } - - /** - * Create an Android Firefox Account instance backed by an Android Account - * instance. - * <p> - * We expect a long-lived application context to avoid life-cycle issues that - * might arise if the internally cached AccountManager instance surfaces UI. - * <p> - * We take care to not install any listeners or observers that might outlive - * the AccountManager; and Android ensures the AccountManager doesn't outlive - * the associated context. - * - * @param applicationContext - * to use as long-lived ambient Android context. - * @param account - * Android account to use for storage. - */ - public AndroidFxAccount(Context applicationContext, Account account) { - this.context = applicationContext; - this.account = account; - this.accountManager = AccountManager.get(this.context); - } - - public static AndroidFxAccount fromContext(Context context) { - context = context.getApplicationContext(); - Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account == null) { - return null; - } - return new AndroidFxAccount(context, account); - } - - /** - * Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around - * {@link AccountPickler#pickle}, and is identical to calling it directly. - * <p> - * Note that pickling is different from bundling, which involves operations on a - * {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account. - * See {@link #persistBundle} and {@link #unbundle} for more. - */ - public void pickle(final String filename) { - AccountPickler.pickle(this, filename); - } - - public Account getAndroidAccount() { - return this.account; - } - - protected int getAccountVersion() { - String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION); - if (v == null) { - return 0; // Implicit. - } - - try { - return Integer.parseInt(v, 10); - } catch (NumberFormatException ex) { - return 0; - } - } - - /** - * Saves the given data as the internal bundle associated with this account. - * @param bundle to write to account. - */ - protected synchronized void persistBundle(ExtendedJSONObject bundle) { - perAccountBundleCache.put(account.name, bundle); - accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString()); - } - - protected ExtendedJSONObject unbundle() { - return unbundle(true); - } - - /** - * Retrieve the internal bundle associated with this account. - * @return bundle associated with account. - */ - protected synchronized ExtendedJSONObject unbundle(boolean allowCachedBundle) { - if (allowCachedBundle) { - final ExtendedJSONObject cachedBundle = perAccountBundleCache.get(account.name); - if (cachedBundle != null) { - Logger.debug(LOG_TAG, "Returning cached account bundle."); - return cachedBundle; - } - } - - final int version = getAccountVersion(); - if (version < CURRENT_ACCOUNT_VERSION) { - // Needs upgrade. For now, do nothing. We'd like to just put your account - // into the Separated state here and have you update your credentials. - return null; - } - - if (version > CURRENT_ACCOUNT_VERSION) { - // Oh dear. - return null; - } - - String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR); - if (bundleString == null) { - return null; - } - final ExtendedJSONObject bundle = unbundleAccountV2(bundleString); - perAccountBundleCache.put(account.name, bundle); - Logger.info(LOG_TAG, "Account bundle persisted to cache."); - return bundle; - } - - protected String getBundleData(String key) { - ExtendedJSONObject o = unbundle(); - if (o == null) { - return null; - } - return o.getString(key); - } - - protected boolean getBundleDataBoolean(String key, boolean def) { - ExtendedJSONObject o = unbundle(); - if (o == null) { - return def; - } - Boolean b = o.getBoolean(key); - if (b == null) { - return def; - } - return b; - } - - protected byte[] getBundleDataBytes(String key) { - ExtendedJSONObject o = unbundle(); - if (o == null) { - return null; - } - return o.getByteArrayHex(key); - } - - protected void updateBundleValues(String key, String value, String... more) { - if (more.length % 2 != 0) { - throw new IllegalArgumentException("more must be a list of key, value pairs"); - } - ExtendedJSONObject descriptor = unbundle(); - if (descriptor == null) { - return; - } - descriptor.put(key, value); - for (int i = 0; i + 1 < more.length; i += 2) { - descriptor.put(more[i], more[i+1]); - } - persistBundle(descriptor); - } - - private ExtendedJSONObject unbundleAccountV1(String bundle) { - ExtendedJSONObject o; - try { - o = new ExtendedJSONObject(bundle); - } catch (Exception e) { - return null; - } - if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) { - return o; - } - return null; - } - - private ExtendedJSONObject unbundleAccountV2(String bundle) { - return unbundleAccountV1(bundle); - } - - /** - * Note that if the user clears data, an account will be left pointing to a - * deleted profile. Such is life. - */ - public String getProfile() { - return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE); - } - - public String getAccountServerURI() { - return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER); - } - - public String getTokenServerURI() { - return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER); - } - - public String getProfileServerURI() { - String profileURI = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_SERVER); - if (profileURI == null) { - if (isStaging()) { - return FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT; - } - return FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT; - } - return profileURI; - } - - public String getOAuthServerURI() { - // Allow testing against stage. - if (isStaging()) { - return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT; - } else { - return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT; - } - } - - private boolean isStaging() { - return FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI()); - } - - private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException { - String profile = getProfile(); - String username = account.name; - - if (profile == null) { - throw new IllegalStateException("Missing profile. Cannot fetch prefs."); - } - - if (username == null) { - throw new IllegalStateException("Missing username. Cannot fetch prefs."); - } - - final String fxaServerURI = getAccountServerURI(); - if (fxaServerURI == null) { - throw new IllegalStateException("No account server URI. Cannot fetch prefs."); - } - - // This is unique for each syncing 'view' of the account. - final String serverURLThing = fxaServerURI + "!" + extra; - return Utils.getPrefsPath(product, username, serverURLThing, profile, version); - } - - /** - * This needs to return a string because of the tortured prefs access in GlobalSession. - */ - public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException { - final String tokenServerURI = getTokenServerURI(); - if (tokenServerURI == null) { - throw new IllegalStateException("No token server URI. Cannot fetch prefs."); - } - - final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa"; - final long version = CURRENT_SYNC_PREFS_VERSION; - return constructPrefsPath(product, version, tokenServerURI); - } - - public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException { - final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading"; - final long version = CURRENT_RL_PREFS_VERSION; - return constructPrefsPath(product, version, ""); - } - - public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException { - return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE); - } - - public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException { - return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE); - } - - /** - * Extract a JSON dictionary of the string values associated to this account. - * <p> - * <b>For debugging use only!</b> The contents of this JSON object completely - * determine the user's Firefox Account status and yield access to whatever - * user data the device has access to. - * - * @return JSON-object of Strings. - */ - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = unbundle(); - o.put("email", account.name); - try { - o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8"))); - } catch (UnsupportedEncodingException e) { - // Ignore. - } - o.put("fxaDeviceId", getDeviceId()); - o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion()); - return o; - } - - public static AndroidFxAccount addAndroidAccount( - Context context, - String email, - String profile, - String idpServerURI, - String tokenServerURI, - String profileServerURI, - State state, - final Map<String, Boolean> authoritiesToSyncAutomaticallyMap) - throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException { - return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, profileServerURI, state, - authoritiesToSyncAutomaticallyMap, - CURRENT_ACCOUNT_VERSION, false, null); - } - - public static AndroidFxAccount addAndroidAccount( - Context context, - String email, - String profile, - String idpServerURI, - String tokenServerURI, - String profileServerURI, - State state, - final Map<String, Boolean> authoritiesToSyncAutomaticallyMap, - final int accountVersion, - final boolean fromPickle, - ExtendedJSONObject bundle) - throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException { - if (email == null) { - throw new IllegalArgumentException("email must not be null"); - } - if (profile == null) { - throw new IllegalArgumentException("profile must not be null"); - } - if (idpServerURI == null) { - throw new IllegalArgumentException("idpServerURI must not be null"); - } - if (tokenServerURI == null) { - throw new IllegalArgumentException("tokenServerURI must not be null"); - } - if (profileServerURI == null) { - throw new IllegalArgumentException("profileServerURI must not be null"); - } - if (state == null) { - throw new IllegalArgumentException("state must not be null"); - } - - // TODO: Add migration code. - if (accountVersion != CURRENT_ACCOUNT_VERSION) { - throw new IllegalStateException("Could not create account of version " + accountVersion + - ". Current version is " + CURRENT_ACCOUNT_VERSION + "."); - } - - // Android has internal restrictions that require all values in this - // bundle to be strings. *sigh* - Bundle userdata = new Bundle(); - userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION); - userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI); - userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI); - userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI); - userdata.putString(ACCOUNT_KEY_PROFILE, profile); - - if (bundle == null) { - bundle = new ExtendedJSONObject(); - // TODO: How to upgrade? - bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION); - } - bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name()); - bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString()); - - userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString()); - - Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE); - AccountManager accountManager = AccountManager.get(context); - // We don't set an Android password, because we don't want to persist the - // password (or anything else as powerful as the password). Instead, we - // internally manage a sessionToken with a remotely owned lifecycle. - boolean added = accountManager.addAccountExplicitly(account, null, userdata); - if (!added) { - return null; - } - - // Try to work around an intermittent issue described at - // http://stackoverflow.com/a/11698139. What happens is that tests that - // delete and re-create the same account frequently will find the account - // missing all or some of the userdata bundle, possibly due to an Android - // AccountManager caching bug. - for (String key : userdata.keySet()) { - accountManager.setUserData(account, key, userdata.getString(key)); - } - - AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - - if (!fromPickle) { - fxAccount.clearSyncPrefs(); - } - - fxAccount.setAuthoritiesToSyncAutomaticallyMap(authoritiesToSyncAutomaticallyMap); - - return fxAccount; - } - - public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException { - getSyncPrefs().edit().clear().commit(); - } - - public void setAuthoritiesToSyncAutomaticallyMap(Map<String, Boolean> authoritiesToSyncAutomaticallyMap) { - if (authoritiesToSyncAutomaticallyMap == null) { - throw new IllegalArgumentException("authoritiesToSyncAutomaticallyMap must not be null"); - } - - for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) { - boolean authorityEnabled = DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.get(authority); - final Boolean enabled = authoritiesToSyncAutomaticallyMap.get(authority); - if (enabled != null) { - authorityEnabled = enabled.booleanValue(); - } - // Accounts are always capable of being synced ... - ContentResolver.setIsSyncable(account, authority, 1); - // ... but not always automatically synced. - ContentResolver.setSyncAutomatically(account, authority, authorityEnabled); - } - } - - public Map<String, Boolean> getAuthoritiesToSyncAutomaticallyMap() { - final Map<String, Boolean> authoritiesToSync = new HashMap<>(); - for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) { - final boolean enabled = ContentResolver.getSyncAutomatically(account, authority); - authoritiesToSync.put(authority, enabled); - } - return authoritiesToSync; - } - - /** - * Is a sync currently in progress? - * - * @return true if Android is currently syncing the underlying Android Account. - */ - public boolean isCurrentlySyncing() { - boolean active = false; - for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) { - active |= ContentResolver.isSyncActive(account, authority); - } - return active; - } - - /** - * Request an immediate sync. Use this to sync as soon as possible in response to user action. - * - * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages. - * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages. - */ - public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip) { - FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip); - } - - /** - * Request an eventual sync. Use this to request the system queue a sync for some time in the - * future. - * - * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages. - * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages. - */ - public void requestEventualSync(String[] stagesToSync, String[] stagesToSkip) { - FirefoxAccounts.requestEventualSync(getAndroidAccount(), stagesToSync, stagesToSkip); - } - - public synchronized void setState(State state) { - if (state == null) { - throw new IllegalArgumentException("state must not be null"); - } - Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() + - " to state " + state.getStateLabel().toString()); - updateBundleValues( - BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name(), - BUNDLE_KEY_STATE, state.toJSONObject().toJSONString()); - broadcastAccountStateChangedIntent(); - } - - protected void broadcastAccountStateChangedIntent() { - final Intent intent = new Intent(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION); - intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - public synchronized State getState() { - String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL); - String stateString = getBundleData(BUNDLE_KEY_STATE); - if (stateLabelString == null || stateString == null) { - throw new IllegalStateException("stateLabelString and stateString must not be null, but: " + - "(stateLabelString == null) = " + (stateLabelString == null) + - " and (stateString == null) = " + (stateString == null)); - } - - try { - StateLabel stateLabel = StateLabel.valueOf(stateLabelString); - Logger.debug(LOG_TAG, "Account is in state " + stateLabel); - return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString)); - } catch (Exception e) { - throw new IllegalStateException("could not get state", e); - } - } - - public byte[] getSessionToken() throws InvalidFxAState { - State state = getState(); - StateLabel stateLabel = state.getStateLabel(); - if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) { - TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state; - return tokensAndKeysState.getSessionToken(); - } - throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state"); - } - - public static class InvalidFxAState extends Exception { - private static final long serialVersionUID = -8537626959811195978L; - - public InvalidFxAState(String message) { - super(message); - } - } - - /** - * <b>For debugging only!</b> - */ - public void dump() { - if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) { - return; - } - ExtendedJSONObject o = toJSONObject(); - ArrayList<String> list = new ArrayList<String>(o.keySet()); - Collections.sort(list); - for (String key : list) { - FxAccountUtils.pii(LOG_TAG, key + ": " + o.get(key)); - } - } - - /** - * Return the Firefox Account's local email address. - * <p> - * It is important to note that this is the local email address, and not - * necessarily the normalized remote email address that the server expects. - * - * @return local email address. - */ - public String getEmail() { - return account.name; - } - - /** - * Return the Firefox Account's local email address, obfuscated. - * <p> - * Use this when logging. - * - * @return local email address, obfuscated. - */ - public String getObfuscatedEmail() { - return Utils.obfuscateEmail(account.name); - } - - /** - * Populate an intent used for starting FxAccountDeletedService service. - * - * @param intent Intent to populate with necessary extras - * @return <code>Intent</code> with a deleted action and account/OAuth information extras - */ - public Intent populateDeletedAccountIntent(final Intent intent) { - final List<String> tokens = new ArrayList<>(); - - intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, - Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION)); - intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name); - intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE, getProfile()); - - // Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will - // be extended in future to support OAuth for other services. - for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) { - final String authToken = accountManager.peekAuthToken(account, tokenKey); - if (authToken != null) { - tokens.add(authToken); - } - } - - // Update intent with tokens and service URI. - intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI()); - // Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras - intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()])); - return intent; - } - - /** - * Create an intent announcing that the profile JSON attached to this Firefox Account has been updated. - * <p> - * It is not guaranteed that the profile JSON has changed. - * - * @return <code>Intent</code> to broadcast. - */ - private Intent makeProfileJSONUpdatedIntent() { - final Intent intent = new Intent(); - intent.setAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION); - return intent; - } - - public void setLastSyncedTimestamp(long now) { - try { - getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e); - } - } - - public long getLastSyncedTimestamp() { - final long neverSynced = -1L; - try { - return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e); - return neverSynced; - } - } - - // Debug only! This is dangerous! - public void unsafeTransitionToDefaultEndpoints() { - unsafeTransitionToStageEndpoints( - FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT, - FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT, - FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT); - } - - // Debug only! This is dangerous! - public void unsafeTransitionToStageEndpoints() { - unsafeTransitionToStageEndpoints( - FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT, - FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT, - FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT); - } - - protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint, String profileServerEndpoint) { - try { - getReadingListPrefs().edit().clear().commit(); - } catch (UnsupportedEncodingException | GeneralSecurityException e) { - // Ignore. - } - try { - getSyncPrefs().edit().clear().commit(); - } catch (UnsupportedEncodingException | GeneralSecurityException e) { - // Ignore. - } - State state = getState(); - setState(state.makeSeparatedState()); - accountManager.setUserData(account, ACCOUNT_KEY_IDP_SERVER, authServerEndpoint); - accountManager.setUserData(account, ACCOUNT_KEY_TOKEN_SERVER, tokenServerEndpoint); - accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_SERVER, profileServerEndpoint); - ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 1); - } - - /** - * Returns the current profile JSON if available, or null. - * - * @return profile JSON object. - */ - public ExtendedJSONObject getProfileJSON() { - final String profileString = getBundleData(BUNDLE_KEY_PROFILE_JSON); - if (profileString == null) { - return null; - } - - try { - return new ExtendedJSONObject(profileString); - } catch (Exception e) { - Logger.error(LOG_TAG, "Failed to parse profile JSON; ignoring and returning null.", e); - } - return null; - } - - /** - * Fetch the profile JSON associated to the underlying Firefox Account from the server and update the local store. - * <p> - * The LocalBroadcastManager is used to notify the receivers asynchronously after a successful fetch. - */ - public void fetchProfileJSON() { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - // Fetch profile information from server. - String authToken; - try { - authToken = accountManager.blockingGetAuthToken(account, AndroidFxAccount.PROFILE_OAUTH_TOKEN_TYPE, true); - if (authToken == null) { - throw new RuntimeException("Couldn't get oauth token! Aborting profile fetch."); - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Error fetching profile information; ignoring.", e); - return; - } - - Logger.info(LOG_TAG, "Intent service launched to fetch profile."); - final Intent intent = new Intent(context, FxAccountProfileService.class); - intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken); - intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI()); - intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(new Handler())); - context.startService(intent); - } - }); - } - - @Nullable - public synchronized String getDeviceId() { - return accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_ID); - } - - @NonNull - public synchronized int getDeviceRegistrationVersion() { - String versionStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION); - if (TextUtils.isEmpty(versionStr)) { - return 0; - } else { - try { - return Integer.parseInt(versionStr); - } catch (NumberFormatException ex) { - return 0; - } - } - } - - public synchronized void setDeviceId(String id) { - accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id); - } - - public synchronized void setDeviceRegistrationVersion(int deviceRegistrationVersion) { - accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION, - Integer.toString(deviceRegistrationVersion)); - } - - public synchronized void resetDeviceRegistrationVersion() { - setDeviceRegistrationVersion(0); - } - - public synchronized void setFxAUserData(String id, int deviceRegistrationVersion) { - accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id); - accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION, - Integer.toString(deviceRegistrationVersion)); - } - - @SuppressLint("ParcelCreator") // The CREATOR field is defined in the super class. - private class ProfileResultReceiver extends ResultReceiver { - public ProfileResultReceiver(Handler handler) { - super(handler); - } - - @Override - protected void onReceiveResult(int resultCode, Bundle bundle) { - super.onReceiveResult(resultCode, bundle); - switch (resultCode) { - case Activity.RESULT_OK: - final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING); - updateBundleValues(BUNDLE_KEY_PROFILE_JSON, resultData); - Logger.info(LOG_TAG, "Profile JSON fetch succeeeded!"); - FxAccountUtils.pii(LOG_TAG, "Profile JSON fetch returned: " + resultData); - LocalBroadcastManager.getInstance(context).sendBroadcast(makeProfileJSONUpdatedIntent()); - break; - case Activity.RESULT_CANCELED: - Logger.warn(LOG_TAG, "Failed to fetch profile JSON; ignoring."); - break; - default: - Logger.warn(LOG_TAG, "Invalid result code received; ignoring."); - break; - } - } - } - - /** - * Take the lock to own updating any Firefox Account's internal state. - * - * We use a <code>Semaphore</code> rather than a <code>ReentrantLock</code> - * because the callback that needs to release the lock may not be invoked on - * the thread that initially acquired the lock. Be aware! - */ - protected static final Semaphore sLock = new Semaphore(1, true /* fair */); - - // Which consumer took the lock? - // Synchronized by this. - protected String lockTag = null; - - // Are we locked? (It's not easy to determine who took the lock dynamically, - // so we maintain this flag internally.) - // Synchronized by this. - protected boolean locked = false; - - // Block until we can take the shared state lock. - public synchronized void acquireSharedAccountStateLock(final String tag) throws InterruptedException { - final long id = Thread.currentThread().getId(); - this.lockTag = tag; - Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ..."); - sLock.acquire(); - locked = true; - Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ... ACQUIRED"); - } - - // If we hold the shared state lock, release it. Otherwise, ignore the request. - public synchronized void releaseSharedAccountStateLock() { - final long id = Thread.currentThread().getId(); - Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ..."); - if (locked) { - sLock.release(); - locked = false; - Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED"); - } else { - Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... NOT LOCKED"); - } - } - - @Override - protected synchronized void finalize() { - if (locked) { - // Should never happen, but... - sLock.release(); - locked = false; - final long id = Thread.currentThread().getId(); - Log.e(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED DURING FINALIZE"); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java deleted file mode 100644 index ff3122322..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxADefaultLoginStateMachineDelegate.java +++ /dev/null @@ -1,84 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountClient; -import org.mozilla.gecko.background.fxa.FxAccountClient20; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; -import org.mozilla.gecko.fxa.login.Married; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.login.StateFactory; -import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager; -import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; - -import android.content.Context; - -public abstract class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate { - protected final static String LOG_TAG = LoginStateMachineDelegate.class.getSimpleName(); - - protected final Context context; - protected final AndroidFxAccount fxAccount; - protected final Executor executor; - protected final FxAccountClient client; - - public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) { - this.context = context; - this.fxAccount = fxAccount; - this.executor = Executors.newSingleThreadExecutor(); - this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor); - } - - abstract public void handleNotMarried(State notMarried); - abstract public void handleMarried(Married married); - - @Override - public FxAccountClient getClient() { - return client; - } - - @Override - public long getCertificateDurationInMilliseconds() { - return 12 * 60 * 60 * 1000; - } - - @Override - public long getAssertionDurationInMilliseconds() { - return 15 * 60 * 1000; - } - - @Override - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return StateFactory.generateKeyPair(); - } - - @Override - public void handleTransition(Transition transition, State state) { - Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); - } - - @Override - public void handleFinal(State state) { - Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); - fxAccount.setState(state); - // Update any notifications displayed. - final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID); - notificationManager.update(context, fxAccount); - - if (state.getStateLabel() != StateLabel.Married) { - handleNotMarried(state); - return; - } else { - handleMarried((Married) state); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java deleted file mode 100644 index 259b1cb88..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java +++ /dev/null @@ -1,385 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.accounts.NetworkErrorException; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountClient; -import org.mozilla.gecko.background.fxa.FxAccountClient20; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; -import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; -import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; -import org.mozilla.gecko.fxa.login.Married; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.login.StateFactory; -import org.mozilla.gecko.fxa.receivers.FxAccountDeletedService; -import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager; -import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; -import org.mozilla.gecko.util.ThreadUtils; - -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -public class FxAccountAuthenticator extends AbstractAccountAuthenticator { - public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName(); - public static final int UNKNOWN_ERROR_CODE = 999; - - protected final Context context; - protected final AccountManager accountManager; - - public FxAccountAuthenticator(Context context) { - super(context); - this.context = context; - this.accountManager = AccountManager.get(context); - } - - @Override - public Bundle addAccount(AccountAuthenticatorResponse response, - String accountType, String authTokenType, String[] requiredFeatures, - Bundle options) - throws NetworkErrorException { - Logger.debug(LOG_TAG, "addAccount"); - - // The data associated to each Account should be invalidated when we change - // the set of Firefox Accounts on the system. - AndroidFxAccount.invalidateCaches(); - - final Bundle res = new Bundle(); - - if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { - res.putInt(AccountManager.KEY_ERROR_CODE, -1); - res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type."); - return res; - } - - final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED); - res.putParcelable(AccountManager.KEY_INTENT, intent); - return res; - } - - @Override - public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) - throws NetworkErrorException { - Logger.debug(LOG_TAG, "confirmCredentials"); - - return null; - } - - @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - Logger.debug(LOG_TAG, "editProperties"); - - return null; - } - - protected static class Responder { - final AccountAuthenticatorResponse response; - final AndroidFxAccount fxAccount; - - public Responder(AccountAuthenticatorResponse response, AndroidFxAccount fxAccount) { - this.response = response; - this.fxAccount = fxAccount; - } - - public void fail(Exception e) { - Logger.warn(LOG_TAG, "Responding with error!", e); - fxAccount.releaseSharedAccountStateLock(); - final Bundle result = new Bundle(); - result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE); - result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString()); - response.onResult(result); - } - - public void succeed(String authToken) { - Logger.info(LOG_TAG, "Responding with success!"); - fxAccount.releaseSharedAccountStateLock(); - final Bundle result = new Bundle(); - result.putString(AccountManager.KEY_ACCOUNT_NAME, fxAccount.account.name); - result.putString(AccountManager.KEY_ACCOUNT_TYPE, fxAccount.account.type); - result.putString(AccountManager.KEY_AUTHTOKEN, authToken); - response.onResult(result); - } - } - - public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate { - protected final Context context; - protected final AndroidFxAccount fxAccount; - protected final Executor executor; - protected final FxAccountClient client; - - public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) { - this.context = context; - this.fxAccount = fxAccount; - this.executor = Executors.newSingleThreadExecutor(); - this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor); - } - - @Override - public FxAccountClient getClient() { - return client; - } - - @Override - public long getCertificateDurationInMilliseconds() { - return 12 * 60 * 60 * 1000; - } - - @Override - public long getAssertionDurationInMilliseconds() { - return 15 * 60 * 1000; - } - - @Override - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return StateFactory.generateKeyPair(); - } - - @Override - public void handleTransition(Transition transition, State state) { - Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel()); - } - - abstract public void handleNotMarried(State notMarried); - abstract public void handleMarried(Married married); - - @Override - public void handleFinal(State state) { - Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel()); - fxAccount.setState(state); - // Update any notifications displayed. - final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID); - notificationManager.update(context, fxAccount); - - if (state.getStateLabel() != StateLabel.Married) { - handleNotMarried(state); - return; - } else { - handleMarried((Married) state); - } - } - } - - protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException { - Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope); - - final Responder responder = new Responder(response, fxAccount); - final String oauthServerUri = fxAccount.getOAuthServerURI(); - - final String audience; - try { - audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token. - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e); - responder.fail(e); - return; - } - - final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); - - stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) { - @Override - public void handleNotMarried(State state) { - final String message = "Cannot fetch oauth token from state: " + state.getStateLabel(); - Logger.warn(LOG_TAG, message); - responder.fail(new RuntimeException(message)); - } - - @Override - public void handleMarried(final Married married) { - final String assertion; - try { - assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - JSONWebTokenUtils.dumpAssertion(assertion); - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e); - responder.fail(e); - return; - } - - final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor); - Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope); - oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() { - @Override - public void handleSuccess(AuthorizationResponse result) { - Logger.debug(LOG_TAG, "OAuth success."); - FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token); - responder.succeed(result.access_token); - } - - @Override - public void handleFailure(FxAccountAbstractClientRemoteException e) { - Logger.error(LOG_TAG, "OAuth failure.", e); - if (e.isInvalidAuthentication()) { - // We were married, generated an assertion, and our assertion was rejected by the - // oauth client. If it's a 401, we probably have a stale certificate. If instead of - // a stale certificate we have bad credentials, the state machine will fail to sign - // our public key and drive us back to Separated. - fxAccount.setState(married.makeCohabitingState()); - } - responder.fail(e); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "OAuth error.", e); - responder.fail(e); - } - }); - } - }); - } - - @Override - public Bundle getAuthToken(final AccountAuthenticatorResponse response, - final Account account, final String authTokenType, final Bundle options) - throws NetworkErrorException { - Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType); - - // If we have a cached authToken, hand it over. - final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType); - if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) { - Logger.info(LOG_TAG, "Return cached token."); - final Bundle result = new Bundle(); - result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); - result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); - result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken); - return result; - } - - // If we're asked for an oauth::scope token, try to generate one. - final String oauthPrefix = "oauth::"; - if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) { - final String scope = authTokenType.substring(oauthPrefix.length()); - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - try { - fxAccount.acquireSharedAccountStateLock(LOG_TAG); - } catch (InterruptedException e) { - Logger.warn(LOG_TAG, "Could not acquire account state lock; return error bundle."); - final Bundle bundle = new Bundle(); - bundle.putInt(AccountManager.KEY_ERROR_CODE, 1); - bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Could not acquire account state lock."); - return bundle; - } - getOAuthToken(response, fxAccount, scope); - return null; - } - - // Otherwise, fail. - Logger.warn(LOG_TAG, "Returning error bundle for getAuthToken with unknown token type."); - final Bundle bundle = new Bundle(); - bundle.putInt(AccountManager.KEY_ERROR_CODE, 2); - bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Unknown token type: " + authTokenType); - return bundle; - } - - @Override - public String getAuthTokenLabel(String authTokenType) { - Logger.debug(LOG_TAG, "getAuthTokenLabel"); - - return null; - } - - @Override - public Bundle hasFeatures(AccountAuthenticatorResponse response, - Account account, String[] features) throws NetworkErrorException { - Logger.debug(LOG_TAG, "hasFeatures"); - - return null; - } - - @Override - public Bundle updateCredentials(AccountAuthenticatorResponse response, - Account account, String authTokenType, Bundle options) - throws NetworkErrorException { - Logger.debug(LOG_TAG, "updateCredentials"); - - return null; - } - - /** - * If the account is going to be removed, broadcast an "account deleted" - * intent. This allows us to clean up the account. - * <p> - * It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast - * than to create our own hacky broadcast here, but that doesn't include enough - * information about which Accounts changed to correctly identify whether a Sync - * account has been removed (when some Firefox channels are installed on the SD - * card). We can work around this by storing additional state but it's both messy - * and expensive because the broadcast is noisy. - * <p> - * Note that this is <b>not</b> called when an Android Account is blown away - * due to the SD card being unmounted. - */ - @Override - public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account) - throws NetworkErrorException { - Bundle result = super.getAccountRemovalAllowed(response, account); - - if (result == null || - !result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) || - result.containsKey(AccountManager.KEY_INTENT)) { - return result; - } - - final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); - if (!removalAllowed) { - return result; - } - - // Broadcast a message to all Firefox channels sharing this Android - // Account type telling that this Firefox account has been deleted. - // - // Broadcast intents protected with permissions are secure, so it's okay - // to include private information such as a password. - final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account); - - // Deleting the pickle file in a blocking manner will avoid race conditions that might happen when - // an account is unpickled while an FxAccount is being deleted. - // Also we have an assumption that this method is always called from a background thread, so we delete - // the pickle file directly without being afraid from a StrictMode violation. - ThreadUtils.assertNotOnUiThread(); - - final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent( - new Intent(context, FxAccountDeletedService.class) - ); - Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " + - "starting FxAccountDeletedService with action: " + serviceIntent.getAction() + "."); - context.startService(serviceIntent); - - Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " + - "deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'."); - deletePickle(); - - return result; - } - - private void deletePickle() { - try { - AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); - } catch (Exception e) { - // This should never happen, but we really don't want to die in a background thread. - Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java deleted file mode 100644 index d138e6c45..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticatorService.java +++ /dev/null @@ -1,55 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -import org.mozilla.gecko.background.common.log.Logger; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -public class FxAccountAuthenticatorService extends Service { - public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName(); - - // Lazily initialized by <code>getAuthenticator</code>. - protected FxAccountAuthenticator accountAuthenticator; - - protected synchronized FxAccountAuthenticator getAuthenticator() { - if (accountAuthenticator == null) { - accountAuthenticator = new FxAccountAuthenticator(this); - } - - return accountAuthenticator; - } - - @Override - public void onCreate() { - Logger.debug(LOG_TAG, "onCreate"); - - accountAuthenticator = getAuthenticator(); - } - - @Override - public IBinder onBind(Intent intent) { - Logger.debug(LOG_TAG, "onBind"); - - if (intent == null) { - // Should never happen, but can -- Bug 1025937. - return null; - } - - if (!android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) { - return null; - } - - final FxAccountAuthenticator authenticator = getAuthenticator(); - if (authenticator == null) { - // Should never happen. - return null; - } - - return authenticator.getIBinder(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java deleted file mode 100644 index 71006e79d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginDelegate.java +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -/** - * Abstraction around things that might need to be signalled to the user via UI, - * such as: - * <ul> - * <li>account not yet verified;</li> - * <li>account password needs to be updated;</li> - * <li>account key management required or changed;</li> - * <li>auth protocol has changed and Firefox needs to be upgraded;</li> - * </ul> - * etc. - * <p> - * Consumers of this code should differentiate error classes based on the types - * of the exceptions thrown. Exceptions that do not have special meaning are of - * type <code>FxAccountLoginException</code> with an appropriate - * <code>cause</code> inner exception. - */ -public interface FxAccountLoginDelegate { - public void handleError(FxAccountLoginException e); - public void handleSuccess(String assertion); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java deleted file mode 100644 index 56c0140b2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountLoginException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.authenticator; - -public class FxAccountLoginException extends Exception { - public FxAccountLoginException(String string) { - super(string); - } - - public FxAccountLoginException(Exception e) { - super(e); - } - - private static final long serialVersionUID = 397685959625820798L; - - public static class FxAccountLoginBadPasswordException extends FxAccountLoginException { - public FxAccountLoginBadPasswordException(String string) { - super(string); - } - - private static final long serialVersionUID = 397685959625820799L; - } - - public static class FxAccountLoginAccountNotVerifiedException extends FxAccountLoginException { - public FxAccountLoginAccountNotVerifiedException(String string) { - super(string); - } - - private static final long serialVersionUID = 397685959625820800L; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java deleted file mode 100644 index 5d3e71ece..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/BaseRequestDelegate.java +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.background.fxa.FxAccountClient20; -import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError; - -public abstract class BaseRequestDelegate<T> implements FxAccountClient20.RequestDelegate<T> { - protected final ExecuteDelegate delegate; - protected final State state; - - public BaseRequestDelegate(State state, ExecuteDelegate delegate) { - this.delegate = delegate; - this.state = state; - } - - @Override - public void handleFailure(FxAccountClientRemoteException e) { - // Order matters here: we don't want to ignore upgrade required responses - // even if the server tells us something else as well. We don't go directly - // to the Doghouse on upgrade required; we want the user to try to update - // their credentials, and then display UI telling them they need to upgrade. - // Then they go to the Doghouse. - if (e.isUpgradeRequired()) { - delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified)); - return; - } - if (e.isInvalidAuthentication()) { - delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified)); - return; - } - if (e.isUnverified()) { - delegate.handleTransition(new AccountNeedsVerification(), state); - return; - } - delegate.handleTransition(new RemoteError(e), state); - } - - @Override - public void handleError(Exception e) { - delegate.handleTransition(new LocalError(e), state); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java deleted file mode 100644 index dd3477a79..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Cohabiting.java +++ /dev/null @@ -1,50 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public class Cohabiting extends TokensAndKeysState { - private static final String LOG_TAG = Cohabiting.class.getSimpleName(); - - public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) { - super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair); - } - - public Married withCertificate(String certificate) { - return new Married(email, uid, sessionToken, kA, kB, keyPair, certificate); - } - - @Override - public void execute(final ExecuteDelegate delegate) { - delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(), - new BaseRequestDelegate<String>(this, delegate) { - @Override - public void handleSuccess(String certificate) { - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - try { - FxAccountUtils.pii(LOG_TAG, "Fetched certificate: " + certificate); - ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate); - if (c != null) { - FxAccountUtils.pii(LOG_TAG, "Header : " + c.getObject("header")); - FxAccountUtils.pii(LOG_TAG, "Payload : " + c.getObject("payload")); - FxAccountUtils.pii(LOG_TAG, "Signature: " + c.getString("signature")); - } else { - FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!"); - } - } catch (Exception e) { - FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!"); - } - } - delegate.handleTransition(new LogMessage("sign succeeded"), withCertificate(certificate)); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java deleted file mode 100644 index 57600577d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Doghouse.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage; - - -public class Doghouse extends State { - public Doghouse(String email, String uid, boolean verified) { - super(StateLabel.Doghouse, email, uid, verified); - } - - @Override - public void execute(final ExecuteDelegate delegate) { - delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this); - } - - @Override - public Action getNeededAction() { - return Action.NeedsUpgrade; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java deleted file mode 100644 index f192cb58b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Engaged.java +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import java.security.NoSuchAlgorithmException; - -import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; - -public class Engaged extends State { - private static final String LOG_TAG = Engaged.class.getSimpleName(); - - protected final byte[] sessionToken; - protected final byte[] keyFetchToken; - protected final byte[] unwrapkB; - - public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) { - super(StateLabel.Engaged, email, uid, verified); - Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken); - this.unwrapkB = unwrapkB; - this.sessionToken = sessionToken; - this.keyFetchToken = keyFetchToken; - } - - @Override - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = super.toJSONObject(); - // Fields are non-null by constructor. - o.put("unwrapkB", Utils.byte2Hex(unwrapkB)); - o.put("sessionToken", Utils.byte2Hex(sessionToken)); - o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken)); - return o; - } - - @Override - public void execute(final ExecuteDelegate delegate) { - BrowserIDKeyPair theKeyPair; - try { - theKeyPair = delegate.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified)); - return; - } - final BrowserIDKeyPair keyPair = theKeyPair; - - delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) { - @Override - public void handleSuccess(TwoKeys result) { - byte[] kB; - try { - kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB); - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - FxAccountUtils.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA)); - FxAccountUtils.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB)); - FxAccountUtils.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB)); - } - } catch (Exception e) { - delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified)); - return; - } - Transition transition = verified - ? new LogMessage("keys succeeded") - : new AccountVerified(); - delegate.handleTransition(transition, new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair)); - } - }); - } - - @Override - public Action getNeededAction() { - if (!verified) { - return Action.NeedsVerification; - } - return Action.None; - } - - public byte[] getSessionToken() { - return sessionToken; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java deleted file mode 100644 index 34e507541..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginStateMachine.java +++ /dev/null @@ -1,84 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import java.security.NoSuchAlgorithmException; -import java.util.EnumSet; -import java.util.Set; - -import org.mozilla.gecko.background.fxa.FxAccountClient; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; -import org.mozilla.gecko.fxa.login.State.StateLabel; - -public class FxAccountLoginStateMachine { - public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName(); - - public interface LoginStateMachineDelegate { - public FxAccountClient getClient(); - public long getCertificateDurationInMilliseconds(); - public long getAssertionDurationInMilliseconds(); - public void handleTransition(Transition transition, State state); - public void handleFinal(State state); - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException; - } - - public static class ExecuteDelegate { - protected final LoginStateMachineDelegate delegate; - protected final StateLabel desiredStateLabel; - // It's as difficult to detect arbitrary cycles as repeated states. - protected final Set<StateLabel> stateLabelsSeen = EnumSet.noneOf(StateLabel.class); - - protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) { - this.delegate = delegate; - this.desiredStateLabel = desiredStateLabel; - this.stateLabelsSeen.add(initialStateLabel); - } - - public FxAccountClient getClient() { - return delegate.getClient(); - } - - public long getCertificateDurationInMilliseconds() { - return delegate.getCertificateDurationInMilliseconds(); - } - - public long getAssertionDurationInMilliseconds() { - return delegate.getAssertionDurationInMilliseconds(); - } - - public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return delegate.generateKeyPair(); - } - - public void handleTransition(Transition transition, State state) { - // Always trigger the transition callback. - delegate.handleTransition(transition, state); - - // Possibly trigger the final callback. We trigger if we're at our desired - // state, or if we've seen this state before. - StateLabel stateLabel = state.getStateLabel(); - if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) { - delegate.handleFinal(state); - return; - } - - // If this wasn't the last state, leave a bread crumb and move on to the - // next state. - stateLabelsSeen.add(stateLabel); - state.execute(this); - } - } - - public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) { - if (initialState.getStateLabel() == desiredStateLabel) { - // We're already where we want to be! - delegate.handleFinal(initialState); - return; - } - ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate); - initialState.execute(executeDelegate); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java deleted file mode 100644 index 683217853..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/FxAccountLoginTransition.java +++ /dev/null @@ -1,68 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - - -public class FxAccountLoginTransition { - public interface Transition { - } - - public static class LogMessage implements Transition { - public final String detailMessage; - - public LogMessage(String detailMessage) { - this.detailMessage = detailMessage; - } - - @Override - public String toString() { - return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')"); - } - } - - public static class AccountNeedsVerification extends LogMessage { - public AccountNeedsVerification() { - super(null); - } - } - - public static class AccountVerified extends LogMessage { - public AccountVerified() { - super(null); - } - } - - public static class PasswordRequired extends LogMessage { - public PasswordRequired() { - super(null); - } - } - - public static class LocalError implements Transition { - public final Exception e; - - public LocalError(Exception e) { - this.e = e; - } - - @Override - public String toString() { - return "Log(" + this.e + ")"; - } - } - - public static class RemoteError implements Transition { - public final Exception e; - - public RemoteError(Exception e) { - this.e = e; - } - - @Override - public String toString() { - return "Log(" + (this.e == null ? "null" : this.e) + ")"; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java deleted file mode 100644 index 1ec7b4051..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java +++ /dev/null @@ -1,117 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; - -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.KeyBundle; - -public class Married extends TokensAndKeysState { - private static final String LOG_TAG = Married.class.getSimpleName(); - - protected final String certificate; - protected final String clientState; - - public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) { - super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair); - Utils.throwIfNull(certificate); - this.certificate = certificate; - try { - this.clientState = FxAccountUtils.computeClientState(kB); - } catch (NoSuchAlgorithmException e) { - // This should never occur. - throw new IllegalStateException("Unable to compute client state from kB."); - } - } - - @Override - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = super.toJSONObject(); - // Fields are non-null by constructor. - o.put("certificate", certificate); - return o; - } - - @Override - public void execute(final ExecuteDelegate delegate) { - delegate.handleTransition(new LogMessage("staying married"), this); - } - - public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, GeneralSecurityException { - // We generate assertions with no iat and an exp after 2050 to avoid - // invalid-timestamp errors from the token server. - final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS; - String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt); - if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) { - return assertion; - } - - try { - FxAccountUtils.pii(LOG_TAG, "Generated assertion: " + assertion); - ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion); - if (a != null) { - FxAccountUtils.pii(LOG_TAG, "aHeader : " + a.getObject("header")); - FxAccountUtils.pii(LOG_TAG, "aPayload : " + a.getObject("payload")); - FxAccountUtils.pii(LOG_TAG, "aSignature: " + a.getString("signature")); - String certificate = a.getString("certificate"); - if (certificate != null) { - ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate); - FxAccountUtils.pii(LOG_TAG, "cHeader : " + c.getObject("header")); - FxAccountUtils.pii(LOG_TAG, "cPayload : " + c.getObject("payload")); - FxAccountUtils.pii(LOG_TAG, "cSignature: " + c.getString("signature")); - // Print the relevant timestamps in sorted order with labels. - HashMap<Long, String> map = new HashMap<Long, String>(); - map.put(a.getObject("payload").getLong("iat"), "aiat"); - map.put(a.getObject("payload").getLong("exp"), "aexp"); - map.put(c.getObject("payload").getLong("iat"), "ciat"); - map.put(c.getObject("payload").getLong("exp"), "cexp"); - ArrayList<Long> values = new ArrayList<Long>(map.keySet()); - Collections.sort(values); - for (Long value : values) { - FxAccountUtils.pii(LOG_TAG, map.get(value) + ": " + value); - } - } else { - FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!"); - } - } else { - FxAccountUtils.pii(LOG_TAG, "Could not parse assertion!"); - } - } catch (Exception e) { - FxAccountUtils.pii(LOG_TAG, "Got exception dumping assertion debug info."); - } - return assertion; - } - - public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { - // TODO Document this choice for deriving from kB. - return FxAccountUtils.generateSyncKeyBundle(kB); - } - - public String getClientState() { - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - FxAccountUtils.pii(LOG_TAG, "Client state: " + this.clientState); - } - return this.clientState; - } - - public Cohabiting makeCohabitingState() { - return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java deleted file mode 100644 index c30ac2ff7..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/MigratedFromSync11.java +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired; - -public class MigratedFromSync11 extends State { - public final String password; - - public MigratedFromSync11(String email, String uid, boolean verified, String password) { - super(StateLabel.MigratedFromSync11, email, uid, verified); - // Null password is allowed. - this.password = password; - } - - @Override - public void execute(final ExecuteDelegate delegate) { - delegate.handleTransition(new PasswordRequired(), this); - } - - @Override - public Action getNeededAction() { - return Action.NeedsFinishMigrating; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java deleted file mode 100644 index bda620df9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Separated.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired; - - -public class Separated extends State { - public Separated(String email, String uid, boolean verified) { - super(StateLabel.Separated, email, uid, verified); - } - - @Override - public void execute(final ExecuteDelegate delegate) { - delegate.handleTransition(new PasswordRequired(), this); - } - - @Override - public Action getNeededAction() { - return Action.NeedsPassword; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java deleted file mode 100644 index 797011ec2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/State.java +++ /dev/null @@ -1,72 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; - -public abstract class State { - public static final long CURRENT_VERSION = 3L; - - public enum StateLabel { - Engaged, - Cohabiting, - Married, - Separated, - Doghouse, - MigratedFromSync11, - } - - public enum Action { - NeedsUpgrade, - NeedsPassword, - NeedsVerification, - NeedsFinishMigrating, - None, - } - - protected final StateLabel stateLabel; - public final String email; - public final String uid; - public final boolean verified; - - public State(StateLabel stateLabel, String email, String uid, boolean verified) { - Utils.throwIfNull(email, uid); - this.stateLabel = stateLabel; - this.email = email; - this.uid = uid; - this.verified = verified; - } - - public StateLabel getStateLabel() { - return this.stateLabel; - } - - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put("version", State.CURRENT_VERSION); - o.put("email", email); - o.put("uid", uid); - o.put("verified", verified); - return o; - } - - public State makeSeparatedState() { - return new Separated(email, uid, verified); - } - - public State makeDoghouseState() { - return new Doghouse(email, uid, verified); - } - - public State makeMigratedFromSync11State(String password) { - return new MigratedFromSync11(email, uid, verified, password); - } - - public abstract void execute(ExecuteDelegate delegate); - - public abstract Action getNeededAction(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java deleted file mode 100644 index a98f2fb27..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java +++ /dev/null @@ -1,206 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.browserid.DSACryptoImplementation; -import org.mozilla.gecko.browserid.RSACryptoImplementation; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; - -/** - * Create {@link State} instances from serialized representations. - * <p> - * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated, - * Doghouse). In the Cohabiting and Married states, the associated key pairs are - * always RSA key pairs. - * <p> - * Version 2 is identical to version 1, except that in the Cohabiting and - * Married states, the associated keypairs are always DSA key pairs. - */ -public class StateFactory { - private static final String LOG_TAG = StateFactory.class.getSimpleName(); - - private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024; - - public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - // New key pairs are always DSA. - return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1); - } - - protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - // V1 key pairs are RSA. - return RSACryptoImplementation.fromJSONObject(o); - } - - protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { - // V2 key pairs are DSA. - return DSACryptoImplementation.fromJSONObject(o); - } - - public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { - Long version = o.getLong("version"); - if (version == null) { - throw new IllegalStateException("version must not be null"); - } - - final int v = version.intValue(); - if (v == 3) { - // The most common case is the most recent version. - return fromJSONObjectV3(stateLabel, o); - } - if (v == 2) { - return fromJSONObjectV2(stateLabel, o); - } - if (v == 1) { - final State state = fromJSONObjectV1(stateLabel, o); - return migrateV1toV2(stateLabel, state); - } - throw new IllegalStateException("version must be in {1, 2}"); - } - - protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { - switch (stateLabel) { - case Engaged: - return new Engaged( - o.getString("email"), - o.getString("uid"), - o.getBoolean("verified"), - Utils.hex2Byte(o.getString("unwrapkB")), - Utils.hex2Byte(o.getString("sessionToken")), - Utils.hex2Byte(o.getString("keyFetchToken"))); - case Cohabiting: - return new Cohabiting( - o.getString("email"), - o.getString("uid"), - Utils.hex2Byte(o.getString("sessionToken")), - Utils.hex2Byte(o.getString("kA")), - Utils.hex2Byte(o.getString("kB")), - keyPairFromJSONObjectV1(o.getObject("keyPair"))); - case Married: - return new Married( - o.getString("email"), - o.getString("uid"), - Utils.hex2Byte(o.getString("sessionToken")), - Utils.hex2Byte(o.getString("kA")), - Utils.hex2Byte(o.getString("kB")), - keyPairFromJSONObjectV1(o.getObject("keyPair")), - o.getString("certificate")); - case Separated: - return new Separated( - o.getString("email"), - o.getString("uid"), - o.getBoolean("verified")); - case Doghouse: - return new Doghouse( - o.getString("email"), - o.getString("uid"), - o.getBoolean("verified")); - default: - throw new IllegalStateException("unrecognized state label: " + stateLabel); - } - } - - /** - * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs. - */ - protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { - switch (stateLabel) { - case Cohabiting: - return new Cohabiting( - o.getString("email"), - o.getString("uid"), - Utils.hex2Byte(o.getString("sessionToken")), - Utils.hex2Byte(o.getString("kA")), - Utils.hex2Byte(o.getString("kB")), - keyPairFromJSONObjectV2(o.getObject("keyPair"))); - case Married: - return new Married( - o.getString("email"), - o.getString("uid"), - Utils.hex2Byte(o.getString("sessionToken")), - Utils.hex2Byte(o.getString("kA")), - Utils.hex2Byte(o.getString("kB")), - keyPairFromJSONObjectV2(o.getObject("keyPair")), - o.getString("certificate")); - default: - return fromJSONObjectV1(stateLabel, o); - } - } - - /** - * Exactly the same as {@link fromJSONObjectV2}, except that there's a new - * MigratedFromSyncV11 state. - */ - protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { - switch (stateLabel) { - case MigratedFromSync11: - return new MigratedFromSync11( - o.getString("email"), - o.getString("uid"), - o.getBoolean("verified"), - o.getString("password")); - default: - return fromJSONObjectV2(stateLabel, o); - } - } - - protected static void logMigration(State from, State to) { - if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) { - return; - } - try { - FxAccountUtils.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString()); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e); - } - FxAccountUtils.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString()); - } - - protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException { - if (state == null) { - // This should never happen, but let's be careful. - Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null."); - return state; - } - - Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel); - - // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only - // Cohabiting and Married states have a persisted keyPair at all; all - // other states need no conversion at all. - switch (stateLabel) { - case Cohabiting: { - // In the Cohabiting state, we can just generate a new key pair and move on. - final Cohabiting cohabiting = (Cohabiting) state; - final BrowserIDKeyPair keyPair = generateKeyPair(); - final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair); - logMigration(cohabiting, migrated); - return migrated; - } - case Married: { - // In the Married state, we cannot only change the key pair: the stored - // certificate signs the public key of the now obsolete key pair. We - // regress to the Cohabiting state; the next time we sync, we should - // advance back to Married. - final Married married = (Married) state; - final BrowserIDKeyPair keyPair = generateKeyPair(); - final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair); - logMigration(married, migrated); - return migrated; - } - default: - // Otherwise, V1 and V2 states are identical. - return state; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java deleted file mode 100644 index b5121a4d4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/TokensAndKeysState.java +++ /dev/null @@ -1,45 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.login; - -import org.mozilla.gecko.browserid.BrowserIDKeyPair; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; - -public abstract class TokensAndKeysState extends State { - protected final byte[] sessionToken; - protected final byte[] kA; - protected final byte[] kB; - protected final BrowserIDKeyPair keyPair; - - public TokensAndKeysState(StateLabel stateLabel, String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) { - super(stateLabel, email, uid, true); - Utils.throwIfNull(sessionToken, kA, kB, keyPair); - this.sessionToken = sessionToken; - this.kA = kA; - this.kB = kB; - this.keyPair = keyPair; - } - - @Override - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject o = super.toJSONObject(); - // Fields are non-null by constructor. - o.put("sessionToken", Utils.byte2Hex(sessionToken)); - o.put("kA", Utils.byte2Hex(kA)); - o.put("kB", Utils.byte2Hex(kB)); - o.put("keyPair", keyPair.toJSONObject()); - return o; - } - - public byte[] getSessionToken() { - return sessionToken; - } - - @Override - public Action getNeededAction() { - return Action.None; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java deleted file mode 100644 index 60a63a5e1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java +++ /dev/null @@ -1,154 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.receivers; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; -import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager; -import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; -import org.mozilla.gecko.sync.repositories.android.ClientsDatabase; -import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; - -import java.util.concurrent.Executor; - -/** - * A background service to clean up after a Firefox Account is deleted. - * <p> - * Note that we specifically handle deleting the pickle file using a Service and a - * BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account - * to delete their respective pickle files (since, if one remains, the account will be restored - * when that channel is used). - */ -public class FxAccountDeletedService extends IntentService { - public static final String LOG_TAG = FxAccountDeletedService.class.getSimpleName(); - - public FxAccountDeletedService() { - super(LOG_TAG); - } - - @Override - protected void onHandleIntent(final Intent intent) { - // We have an in-memory accounts cache which we use for a variety of tasks; it needs to be cleared. - // It should be fine to invalidate it before doing anything else, as the tasks below do not rely - // on this data. - AndroidFxAccount.invalidateCaches(); - - // Intent can, in theory, be null. Bug 1025937. - if (intent == null) { - Logger.debug(LOG_TAG, "Short-circuiting on null intent."); - return; - } - - final Context context = this; - - long intentVersion = intent.getLongExtra( - FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, 0); - long expectedVersion = FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION; - if (intentVersion != expectedVersion) { - Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but " + - "version " + expectedVersion + "expected. Not cleaning up after deleted Account."); - return; - } - - // Android Account name, not Sync encoded account name. - final String accountName = intent.getStringExtra( - FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY); - if (accountName == null) { - Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " + - "deleted Account."); - return; - } - - - // Fire up gecko and unsubscribe push - final Intent geckoIntent = new Intent(); - geckoIntent.setAction("create-services"); - geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService"); - geckoIntent.putExtra("category", "android-push-service"); - geckoIntent.putExtra("data", "android-fxa-unsubscribe"); - final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context); - geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", - intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE)); - context.startService(geckoIntent); - - // Delete client database and non-local tabs. - Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs"); - FennecTabsRepository.deleteNonLocalClientsAndTabs(context); - - - // Clear Firefox Sync client tables. - try { - Logger.info(LOG_TAG, "Deleting the Firefox Sync clients database."); - ClientsDatabase db = null; - try { - db = new ClientsDatabase(context); - db.wipeClientsTable(); - db.wipeCommandsTable(); - } finally { - if (db != null) { - db.close(); - } - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception deleting the Firefox Sync clients database; ignoring.", e); - } - - // Remove any displayed notifications. - new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID).clear(context); - - // Bug 1147275: Delete cached oauth tokens. There's no way to query all - // oauth tokens from Android, so this is tricky to do comprehensively. We - // can query, individually, for specific oauth tokens to delete, however. - final String oauthServerURI = intent.getStringExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY); - final String[] tokens = intent.getStringArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS); - if (oauthServerURI != null && tokens != null) { - final Executor directExecutor = new Executor() { - @Override - public void execute(Runnable runnable) { - runnable.run(); - } - }; - - final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerURI, directExecutor); - - for (String token : tokens) { - if (token == null) { - Logger.error(LOG_TAG, "Cached OAuth token is null; should never happen. Ignoring."); - continue; - } - try { - oauthClient.deleteToken(token, new FxAccountAbstractClient.RequestDelegate<Void>() { - @Override - public void handleSuccess(Void result) { - Logger.info(LOG_TAG, "Successfully deleted cached OAuth token."); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Failed to delete cached OAuth token; ignoring.", e); - } - - @Override - public void handleFailure(FxAccountAbstractClientRemoteException e) { - Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e); - } - }); - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e); - } - } - } else { - Logger.error(LOG_TAG, "Cached OAuth server URI is null or cached OAuth tokens are null; ignoring."); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java deleted file mode 100644 index ad81e0488..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java +++ /dev/null @@ -1,133 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.receivers; - -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.sync.Utils; - -import android.accounts.Account; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * A receiver that takes action when our Android package is upgraded (replaced). - */ -public class FxAccountUpgradeReceiver extends BroadcastReceiver { - private static final String LOG_TAG = FxAccountUpgradeReceiver.class.getSimpleName(); - - /** - * Produce a list of Runnable instances to be executed sequentially on - * upgrade. - * <p> - * Each Runnable will be executed sequentially on a background thread. Any - * unchecked Exception thrown will be caught and ignored. - * - * @param context Android context. - * @return list of Runnable instances. - */ - protected List<Runnable> onUpgradeRunnables(Context context) { - List<Runnable> runnables = new LinkedList<Runnable>(); - runnables.add(new MaybeUnpickleRunnable(context)); - // Recovering accounts that are in the Doghouse should happen *after* we - // unpickle any accounts saved to disk. - runnables.add(new AdvanceFromDoghouseRunnable(context)); - return runnables; - } - - @Override - public void onReceive(final Context context, Intent intent) { - Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); - Logger.info(LOG_TAG, "Upgrade broadcast received."); - - // Iterate Runnable instances one at a time. - final Executor executor = Executors.newSingleThreadExecutor(); - for (final Runnable runnable : onUpgradeRunnables(context)) { - executor.execute(new Runnable() { - @Override - public void run() { - try { - runnable.run(); - } catch (Exception e) { - // We really don't want to throw on a background thread, so we - // catch, log, and move on. - Logger.error(LOG_TAG, "Got exception executing background upgrade Runnable; ignoring.", e); - } - } - }); - } - } - - /** - * A Runnable that tries to unpickle any pickled Firefox Accounts. - */ - protected static class MaybeUnpickleRunnable implements Runnable { - protected final Context context; - - public MaybeUnpickleRunnable(Context context) { - this.context = context; - } - - @Override - public void run() { - // Querying the accounts will unpickle any pickled Firefox Account. - Logger.info(LOG_TAG, "Trying to unpickle any pickled Firefox Account."); - FirefoxAccounts.getFirefoxAccounts(context); - } - } - - /** - * A Runnable that tries to advance existing Firefox Accounts that are in the - * Doghouse state to the Separated state. - * <p> - * This is our main deprecation-and-upgrade mechanism: in some way, the - * Account gets moved to the Doghouse state. If possible, an upgraded version - * of the package advances to Separated, prompting the user to re-connect the - * Account. - */ - protected static class AdvanceFromDoghouseRunnable implements Runnable { - protected final Context context; - - public AdvanceFromDoghouseRunnable(Context context) { - this.context = context; - } - - @Override - public void run() { - final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context); - Logger.info(LOG_TAG, "Trying to advance " + accounts.length + " existing Firefox Accounts from the Doghouse to Separated (if necessary)."); - for (Account account : accounts) { - try { - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - // For great debugging. - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - fxAccount.dump(); - } - State state = fxAccount.getState(); - if (state == null || state.getStateLabel() != StateLabel.Doghouse) { - Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is not in the Doghouse; skipping."); - continue; - } - Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is in the Doghouse; advancing to Separated."); - fxAccount.setState(state.makeSeparatedState()); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception trying to advance account named like " + Utils.obfuscateEmail(account.name) + - " from Doghouse to Separated state; ignoring.", e); - } - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java deleted file mode 100644 index b44da76fc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountNotificationManager.java +++ /dev/null @@ -1,114 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.Builder; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.Action; -import org.mozilla.gecko.sync.telemetry.TelemetryContract; - -/** - * Abstraction that manages notifications shown or hidden for a Firefox Account. - * <p> - * In future, we anticipate this tracking things like: - * <ul> - * <li>new engines to offer to Sync;</li> - * <li>service interruption updates;</li> - * <li>messages from other clients.</li> - * </ul> - */ -public class FxAccountNotificationManager { - private static final String LOG_TAG = FxAccountNotificationManager.class.getSimpleName(); - - protected final int notificationId; - - // We're lazy about updating our locale info, because most syncs don't notify. - private volatile boolean localeUpdated; - - public FxAccountNotificationManager(int notificationId) { - this.notificationId = notificationId; - } - - /** - * Remove all Firefox Account related notifications from the notification manager. - * - * @param context - * Android context. - */ - public void clear(Context context) { - final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(notificationId); - } - - /** - * Reflect new Firefox Account state to the notification manager: show or hide - * notifications reflecting the state of a Firefox Account. - * - * @param context - * Android context. - * @param fxAccount - * Firefox Account to reflect to the notification manager. - */ - public void update(Context context, AndroidFxAccount fxAccount) { - final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - - final State state = fxAccount.getState(); - final Action action = state.getNeededAction(); - if (action == Action.None) { - Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs no action; cancelling any existing notification."); - notificationManager.cancel(notificationId); - return; - } - - if (!localeUpdated) { - localeUpdated = true; - Locales.getLocaleManager().getAndApplyPersistedLocale(context); - } - - final String title; - final String text; - final Intent notificationIntent; - if (action == Action.NeedsFinishMigrating) { - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC11_MIGRATION_NOTIFICATIONS_OFFERED, 1); - - title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title); - text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email); - notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING); - } else { - title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title); - text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email); - notificationIntent = new Intent(FxAccountConstants.ACTION_FXA_STATUS); - } - - notificationIntent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_NOTIFICATION); - - Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title); - FxAccountUtils.pii(LOG_TAG, "And text: " + text); - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); - - final Builder builder = new NotificationCompat.Builder(context); - builder - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.ic_status_logo) - .setAutoCancel(true) - .setContentIntent(pendingIntent); - notificationManager.notify(notificationId, builder.build()); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java deleted file mode 100644 index 7f03eff1c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountProfileService.java +++ /dev/null @@ -1,107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import android.accounts.AccountManager; -import android.app.Activity; -import android.app.IntentService; -import android.content.Intent; -import android.os.Bundle; -import android.os.ResultReceiver; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient; -import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException; -import org.mozilla.gecko.background.fxa.profile.FxAccountProfileClient10; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -public class FxAccountProfileService extends IntentService { - private static final String LOG_TAG = "FxAccountProfileService"; - private static final Executor EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); - public static final String KEY_AUTH_TOKEN = "auth_token"; - public static final String KEY_PROFILE_SERVER_URI = "profileServerURI"; - public static final String KEY_RESULT_RECEIVER = "resultReceiver"; - public static final String KEY_RESULT_STRING = "RESULT_STRING"; - - public FxAccountProfileService() { - super("FxAccountProfileService"); - } - - @Override - protected void onHandleIntent(Intent intent) { - final String authToken = intent.getStringExtra(KEY_AUTH_TOKEN); - final String profileServerURI = intent.getStringExtra(KEY_PROFILE_SERVER_URI); - final ResultReceiver resultReceiver = intent.getParcelableExtra(KEY_RESULT_RECEIVER); - - if (resultReceiver == null) { - Logger.warn(LOG_TAG, "Result receiver must not be null; ignoring intent."); - return; - } - - if (authToken == null || authToken.length() == 0) { - Logger.warn(LOG_TAG, "Invalid Auth Token"); - sendResult("Invalid Auth Token", resultReceiver, Activity.RESULT_CANCELED); - return; - } - - if (profileServerURI == null || profileServerURI.length() == 0) { - Logger.warn(LOG_TAG, "Invalid profile Server Endpoint"); - sendResult("Invalid profile Server Endpoint", resultReceiver, Activity.RESULT_CANCELED); - return; - } - - // This delegate fetches the profile avatar json. - FxAccountProfileClient10.RequestDelegate<ExtendedJSONObject> delegate = new FxAccountAbstractClient.RequestDelegate<ExtendedJSONObject>() { - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Error fetching Account profile.", e); - sendResult("Error fetching Account profile.", resultReceiver, Activity.RESULT_CANCELED); - } - - @Override - public void handleFailure(FxAccountAbstractClientException.FxAccountAbstractClientRemoteException e) { - Logger.warn(LOG_TAG, "Failed to fetch Account profile.", e); - - if (e.isInvalidAuthentication()) { - // The profile server rejected the cached oauth token! Invalidate it. - // A new token will be generated upon next request. - Logger.info(LOG_TAG, "Invalidating oauth token after 401!"); - AccountManager.get(FxAccountProfileService.this).invalidateAuthToken(FxAccountConstants.ACCOUNT_TYPE, authToken); - } - - sendResult("Failed to fetch Account profile.", resultReceiver, Activity.RESULT_CANCELED); - } - - @Override - public void handleSuccess(ExtendedJSONObject result) { - if (result != null){ - FxAccountUtils.pii(LOG_TAG, "Profile server return profile: " + result.toJSONString()); - sendResult(result.toJSONString(), resultReceiver, Activity.RESULT_OK); - } - } - }; - - FxAccountProfileClient10 client = new FxAccountProfileClient10(profileServerURI, EXECUTOR_SERVICE); - try { - client.profile(authToken, delegate); - } catch (Exception e) { - Logger.error(LOG_TAG, "Got exception fetching profile.", e); - delegate.handleError(e); - } - } - - private void sendResult(final String result, final ResultReceiver resultReceiver, final int code) { - if (resultReceiver != null) { - final Bundle bundle = new Bundle(); - bundle.putString(KEY_RESULT_STRING, result); - resultReceiver.send(code, bundle); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java deleted file mode 100644 index 708686e72..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSchedulePolicy.java +++ /dev/null @@ -1,178 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.login.State.Action; -import org.mozilla.gecko.sync.BackoffHandler; - -import android.accounts.Account; -import android.content.ContentResolver; -import android.content.Context; -import android.os.Bundle; - -public class FxAccountSchedulePolicy implements SchedulePolicy { - private static final String LOG_TAG = "FxAccountSchedulePolicy"; - - // Our poll intervals are used to trigger automatic background syncs - // in the absence of user activity. - // - // We also receive sync requests as a result of network tickles, so - // these intervals are long, with the exception of the rapid polling - // while we wait for verification: if we're waiting for the user to - // click on a verification link, we sync very often in order to detect - // a change in state. - // - // In the case of unverified -> unverified (no transition), this should be - // very close to a single HTTP request (with the SyncAdapter overhead, of - // course, but that's not wildly different from alarm manager overhead). - // - // The /account/status endpoint is HAWK authed by sessionToken, so we still - // have to do some crypto no matter what. - - // TODO: only do this for a while... - public static final long POLL_INTERVAL_PENDING_VERIFICATION = 60; // 1 minute. - - // If we're in some kind of error state, there's no point trying often. - // This is not the same as a server-imposed backoff, which will be - // reflected dynamically. - public static final long POLL_INTERVAL_ERROR_STATE_SEC = 24 * 60 * 60; // 24 hours. - - // If we're the only device, just sync once or twice a day in case that - // changes. - public static final long POLL_INTERVAL_SINGLE_DEVICE_SEC = 18 * 60 * 60; // 18 hours. - - // And if we know there are other devices, let's sync often enough that - // we'll be more likely to be caught up (even if not completely) by the - // time you next use this device. This is also achieved via Android's - // network tickles. - public static final long POLL_INTERVAL_MULTI_DEVICE_SEC = 12 * 60 * 60; // 12 hours. - - // This is used solely as an optimization for backoff handling, so it's not - // persisted. - private static volatile long POLL_INTERVAL_CURRENT_SEC = POLL_INTERVAL_SINGLE_DEVICE_SEC; - - // Never sync more frequently than this, unless forced. - // This is to avoid overly-frequent syncs during active browsing. - public static final long RATE_LIMIT_FUNDAMENTAL_SEC = 90; // 90 seconds. - - /** - * We are prompted to sync by several inputs: - * * Periodic syncs that we schedule at long intervals. See the POLL constants. - * * Network-tickle-based syncs that Android starts. - * * Upload-only syncs that are caused by local database writes. - * - * We rate-limit periodic and network-sourced events with this constant. - * We rate limit <b>both</b> with {@link FxAccountSchedulePolicy#RATE_LIMIT_FUNDAMENTAL_SEC}. - */ - public static final long RATE_LIMIT_BACKGROUND_SEC = 60 * 60; // 1 hour. - - private final AndroidFxAccount account; - private final Context context; - - public FxAccountSchedulePolicy(Context context, AndroidFxAccount account) { - this.account = account; - this.context = context; - } - - /** - * Return a millisecond timestamp in the future, offset from the current - * time by the provided amount. - * @param millis the duration by which to delay - * @return a timestamp. - */ - private static long delay(long millis) { - return System.currentTimeMillis() + millis; - } - - /** - * Updates the existing system periodic sync interval to the specified duration. - * - * @param intervalSeconds the requested period, which Android will vary by up to 4%. - */ - protected void requestPeriodicSync(final long intervalSeconds) { - final String authority = BrowserContract.AUTHORITY; - final Account account = this.account.getAndroidAccount(); - this.context.getContentResolver(); - Logger.info(LOG_TAG, "Scheduling periodic sync for " + intervalSeconds + "."); - ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, intervalSeconds); - POLL_INTERVAL_CURRENT_SEC = intervalSeconds; - } - - @Override - public void onSuccessfulSync(int otherClientsCount) { - this.account.setLastSyncedTimestamp(System.currentTimeMillis()); - // This undoes the change made in observeBackoffMillis -- once we hit backoff we'll - // periodically sync at the backoff duration, but as soon as we succeed we'll switch - // into the client-count-dependent interval. - long interval = (otherClientsCount > 0) ? POLL_INTERVAL_MULTI_DEVICE_SEC : POLL_INTERVAL_SINGLE_DEVICE_SEC; - requestPeriodicSync(interval); - } - - @Override - public void onHandleFinal(Action needed) { - switch (needed) { - case NeedsPassword: - case NeedsUpgrade: - case NeedsFinishMigrating: - requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC); - break; - case NeedsVerification: - requestPeriodicSync(POLL_INTERVAL_PENDING_VERIFICATION); - break; - case None: - // No action needed: we'll set the periodic sync interval - // when the sync finishes, via the SessionCallback. - break; - } - } - - @Override - public void onUpgradeRequired() { - // TODO: this shouldn't occur in FxA, but when we upgrade we - // need to reduce the interval again. - requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC); - } - - @Override - public void onUnauthorized() { - // TODO: this shouldn't occur in FxA, but when we fix our credentials - // we need to reduce the interval again. - requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC); - } - - @Override - public void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend) { - if (onlyExtend) { - backoffHandler.extendEarliestNextRequest(delay(backoffMillis)); - } else { - backoffHandler.setEarliestNextRequest(delay(backoffMillis)); - } - - // Yes, we might be part-way through the interval, in which case the backoff - // code will do its job. But we certainly don't want to reduce the interval - // if we're given a small backoff instruction. - // We'll reset the poll interval next time we sync without a backoff instruction. - if (backoffMillis > (POLL_INTERVAL_CURRENT_SEC * 1000)) { - // Slightly inflate the backoff duration to ensure that a fuzzed - // periodic sync doesn't occur before our backoff has passed. Android - // 19+ default to a 4% fuzz factor. - requestPeriodicSync((long) Math.ceil((1.05 * backoffMillis) / 1000)); - } - } - - /** - * Accepts two {@link BackoffHandler} instances as input. These are used - * respectively to track fundamental rate limiting, and to separately - * rate-limit periodic and network-tickled syncs. - */ - @Override - public void configureBackoffMillisBeforeSyncing(BackoffHandler fundamentalRateHandler, BackoffHandler backgroundRateHandler) { - fundamentalRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_FUNDAMENTAL_SEC * 1000)); - backgroundRateHandler.setEarliestNextRequest(delay(RATE_LIMIT_BACKGROUND_SEC * 1000)); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java deleted file mode 100644 index 30990cf7f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java +++ /dev/null @@ -1,568 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import android.accounts.Account; -import android.content.AbstractThreadedSyncAdapter; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SyncResult; -import android.os.Bundle; -import android.os.SystemClock; -import android.text.TextUtils; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper; -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.background.fxa.SkewHandler; -import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator; -import org.mozilla.gecko.fxa.authenticator.AccountPickler; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate; -import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; -import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine; -import org.mozilla.gecko.fxa.login.Married; -import org.mozilla.gecko.fxa.login.State; -import org.mozilla.gecko.fxa.login.State.StateLabel; -import org.mozilla.gecko.fxa.sync.FxAccountSyncDelegate.Result; -import org.mozilla.gecko.sync.BackoffHandler; -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.PrefsBackoffHandler; -import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; -import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; -import org.mozilla.gecko.sync.telemetry.TelemetryContract; -import org.mozilla.gecko.tokenserver.TokenServerClient; -import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; -import org.mozilla.gecko.tokenserver.TokenServerException; -import org.mozilla.gecko.tokenserver.TokenServerToken; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; - -public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { - private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName(); - - public static final int NOTIFICATION_ID = LOG_TAG.hashCode(); - - // Tracks the last seen storage hostname for backoff purposes. - private static final String PREF_BACKOFF_STORAGE_HOST = "backoffStorageHost"; - - // Used to do cheap in-memory rate limiting. Don't sync again if we - // successfully synced within this duration. - private static final int MINIMUM_SYNC_DELAY_MILLIS = 15 * 1000; // 15 seconds. - private volatile long lastSyncRealtimeMillis; - - protected final ExecutorService executor; - protected final FxAccountNotificationManager notificationManager; - - public FxAccountSyncAdapter(Context context, boolean autoInitialize) { - super(context, autoInitialize); - this.executor = Executors.newSingleThreadExecutor(); - this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID); - } - - protected static class SyncDelegate extends FxAccountSyncDelegate { - @Override - public void handleSuccess() { - Logger.info(LOG_TAG, "Sync succeeded."); - super.handleSuccess(); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_COMPLETED, 1); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Got exception syncing.", e); - super.handleError(e); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED, 1); - } - - @Override - public void handleCannotSync(State finalState) { - Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel()); - super.handleCannotSync(finalState); - } - - @Override - public void postponeSync(long millis) { - if (millis <= 0) { - Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay."); - } - super.postponeSync(millis); - } - - @Override - public void rejectSync() { - super.rejectSync(); - } - - protected final Collection<String> stageNamesToSync; - - public SyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) { - super(latch, syncResult); - this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync); - } - - public Collection<String> getStageNamesToSync() { - return this.stageNamesToSync; - } - } - - protected static class SessionCallback implements GlobalSessionCallback { - protected final SyncDelegate syncDelegate; - protected final SchedulePolicy schedulePolicy; - protected volatile BackoffHandler storageBackoffHandler; - - public SessionCallback(SyncDelegate syncDelegate, SchedulePolicy schedulePolicy) { - this.syncDelegate = syncDelegate; - this.schedulePolicy = schedulePolicy; - } - - public void setBackoffHandler(BackoffHandler backoffHandler) { - this.storageBackoffHandler = backoffHandler; - } - - @Override - public boolean shouldBackOffStorage() { - return storageBackoffHandler.delayMilliseconds() > 0; - } - - @Override - public void requestBackoff(long backoffMillis) { - final boolean onlyExtend = true; // Because we trust what the storage server says. - schedulePolicy.configureBackoffMillisOnBackoff(storageBackoffHandler, backoffMillis, onlyExtend); - } - - @Override - public void informUpgradeRequiredResponse(GlobalSession session) { - schedulePolicy.onUpgradeRequired(); - } - - @Override - public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) { - schedulePolicy.onUnauthorized(); - } - - @Override - public void informMigrated(GlobalSession globalSession) { - // It's not possible to migrate a Firefox Account to another Account type - // yet. Yell loudly but otherwise ignore. - Logger.error(LOG_TAG, - "Firefox Account informMigrated called, but it's not yet possible to migrate. " + - "Ignoring even though something is terribly wrong."); - } - - @Override - public void handleStageCompleted(Stage currentState, GlobalSession globalSession) { - } - - @Override - public void handleSuccess(GlobalSession globalSession) { - Logger.info(LOG_TAG, "Global session succeeded."); - - // Get the number of clients, so we can schedule the sync interval accordingly. - try { - int otherClientsCount = globalSession.getClientsDelegate().getClientsCount(); - Logger.debug(LOG_TAG, "" + otherClientsCount + " other client(s)."); - this.schedulePolicy.onSuccessfulSync(otherClientsCount); - } finally { - // Continue with the usual success flow. - syncDelegate.handleSuccess(); - } - } - - @Override - public void handleError(GlobalSession globalSession, Exception e) { - Logger.warn(LOG_TAG, "Global session failed."); // Exception will be dumped by delegate below. - syncDelegate.handleError(e); - // TODO: should we reduce the periodic sync interval? - } - - @Override - public void handleAborted(GlobalSession globalSession, String reason) { - Logger.warn(LOG_TAG, "Global session aborted: " + reason); - syncDelegate.handleError(null); - // TODO: should we reduce the periodic sync interval? - } - }; - - /** - * Return true if the provided {@link BackoffHandler} isn't reporting that we're in - * a backoff state, or the provided {@link Bundle} contains flags that indicate - * we should force a sync. - */ - private boolean shouldPerformSync(final BackoffHandler backoffHandler, final String kind, final Bundle extras) { - final long delay = backoffHandler.delayMilliseconds(); - if (delay <= 0) { - return true; - } - - if (extras == null) { - return false; - } - - final boolean forced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); - if (forced) { - Logger.info(LOG_TAG, "Forced sync (" + kind + "): overruling remaining backoff of " + delay + "ms."); - } else { - Logger.info(LOG_TAG, "Not syncing (" + kind + "): must wait another " + delay + "ms."); - } - return forced; - } - - protected void syncWithAssertion(final String audience, - final String assertion, - final URI tokenServerEndpointURI, - final BackoffHandler tokenBackoffHandler, - final SharedPreferences sharedPrefs, - final KeyBundle syncKeyBundle, - final String clientState, - final SessionCallback callback, - final Bundle extras, - final AndroidFxAccount fxAccount) { - final TokenServerClientDelegate delegate = new TokenServerClientDelegate() { - private boolean didReceiveBackoff = false; - - @Override - public String getUserAgent() { - return FxAccountConstants.USER_AGENT; - } - - @Override - public void handleSuccess(final TokenServerToken token) { - FxAccountUtils.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); - fxAccount.releaseSharedAccountStateLock(); - - if (!didReceiveBackoff) { - // We must be OK to touch this token server. - tokenBackoffHandler.setEarliestNextRequest(0L); - } - - final URI storageServerURI; - try { - storageServerURI = new URI(token.endpoint); - } catch (URISyntaxException e) { - handleError(e); - return; - } - final String storageHostname = storageServerURI.getHost(); - - // We back off on a per-host basis. When we have an endpoint URI from a token, we - // can check on the backoff status for that host. - // If we're supposed to be backing off, we abort the not-yet-started session. - final BackoffHandler storageBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "sync.storage"); - callback.setBackoffHandler(storageBackoffHandler); - - String lastStorageHost = sharedPrefs.getString(PREF_BACKOFF_STORAGE_HOST, null); - final boolean storageHostIsUnchanged = lastStorageHost != null && - lastStorageHost.equalsIgnoreCase(storageHostname); - if (storageHostIsUnchanged) { - Logger.debug(LOG_TAG, "Storage host is unchanged."); - if (!shouldPerformSync(storageBackoffHandler, "storage", extras)) { - Logger.info(LOG_TAG, "Not syncing: storage server requested backoff."); - callback.handleAborted(null, "Storage backoff"); - return; - } - } else { - Logger.debug(LOG_TAG, "Received new storage host."); - } - - // Invalidate the previous backoff, because our storage host has changed, - // or we never had one at all, or we're OK to sync. - storageBackoffHandler.setEarliestNextRequest(0L); - - GlobalSession globalSession = null; - try { - final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs, getContext()); - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - FxAccountUtils.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'."); - FxAccountUtils.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp()); - } - - // We compute skew over time using SkewHandler. This yields an unchanging - // skew adjustment that the HawkAuthHeaderProvider uses to adjust its - // timestamps. Eventually we might want this to adapt within the scope of a - // global session. - final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname); - final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds(); - // We expect Sync to upload large sets of records. Calculating the - // payload verification hash for these record sets could be expensive, - // so we explicitly do not send payload verification hashes to the - // Sync storage endpoint. - final boolean includePayloadVerificationHash = false; - final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew); - - final Context context = getContext(); - final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle); - - Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); - syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); - syncConfig.setClusterURL(storageServerURI); - - globalSession = new GlobalSession(syncConfig, callback, context, clientsDataDelegate); - globalSession.start(); - } catch (Exception e) { - callback.handleError(globalSession, e); - return; - } - } - - @Override - public void handleFailure(TokenServerException e) { - Logger.error(LOG_TAG, "Failed to get token.", e); - try { - // We should only get here *after* we're locked into the married state. - State state = fxAccount.getState(); - if (state.getStateLabel() == StateLabel.Married) { - Married married = (Married) state; - fxAccount.setState(married.makeCohabitingState()); - } - } finally { - fxAccount.releaseSharedAccountStateLock(); - } - callback.handleError(null, e); - } - - @Override - public void handleError(Exception e) { - Logger.error(LOG_TAG, "Failed to get token.", e); - fxAccount.releaseSharedAccountStateLock(); - callback.handleError(null, e); - } - - @Override - public void handleBackoff(int backoffSeconds) { - // This is the token server telling us to back off. - Logger.info(LOG_TAG, "Token server requesting backoff of " + backoffSeconds + "s. Backoff handler: " + tokenBackoffHandler); - didReceiveBackoff = true; - - // If we've already stored a backoff, overrule it: we only use the server - // value for token server scheduling. - tokenBackoffHandler.setEarliestNextRequest(delay(backoffSeconds * 1000)); - } - - private long delay(long delay) { - return System.currentTimeMillis() + delay; - } - }; - - TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor); - tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, delegate); - } - - /** - * A trivial Sync implementation that does not cache client keys, - * certificates, or tokens. - * - * This should be replaced with a full {@link FxAccountAuthenticator}-based - * token implementation. - */ - @Override - public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, final SyncResult syncResult) { - Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG); - Logger.resetLogging(); - - final Context context = getContext(); - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - - Logger.info(LOG_TAG, "Syncing FxAccount" + - " account named like " + Utils.obfuscateEmail(account.name) + - " for authority " + authority + - " with instance " + this + "."); - - Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp()); - - if (FxAccountUtils.LOG_PERSONAL_INFORMATION) { - fxAccount.dump(); - } - - FirefoxAccounts.logSyncOptions(extras); - - if (this.lastSyncRealtimeMillis > 0L && - (this.lastSyncRealtimeMillis + MINIMUM_SYNC_DELAY_MILLIS) > SystemClock.elapsedRealtime() && - !extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) { - Logger.info(LOG_TAG, "Not syncing FxAccount " + Utils.obfuscateEmail(account.name) + - ": minimum interval not met."); - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_FAILED_BACKOFF, 1); - return; - } - - // Pickle in a background thread to avoid strict mode warnings. - ThreadPool.run(new Runnable() { - @Override - public void run() { - try { - AccountPickler.pickle(fxAccount, FxAccountConstants.ACCOUNT_PICKLE_FILENAME); - } catch (Exception e) { - // Should never happen, but we really don't want to die in a background thread. - Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e); - } - } - }); - - final BlockingQueue<Result> latch = new LinkedBlockingQueue<>(1); - - Collection<String> knownStageNames = SyncConfiguration.validEngineNames(); - Collection<String> stageNamesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras); - - final SyncDelegate syncDelegate = new SyncDelegate(latch, syncResult, fxAccount, stageNamesToSync); - - try { - // This will be the same chunk of SharedPreferences that we pass through to GlobalSession/SyncConfiguration. - final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); - - final BackoffHandler backgroundBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "background"); - final BackoffHandler rateLimitBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "rate"); - - // If this sync was triggered by user action, this will be true. - final boolean isImmediate = (extras != null) && - (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false) || - extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)); - - // If it's not an immediate sync, it must be either periodic or tickled. - // Check our background rate limiter. - if (!isImmediate) { - if (!shouldPerformSync(backgroundBackoffHandler, "background", extras)) { - syncDelegate.rejectSync(); - return; - } - } - - // Regardless, let's make sure we're not syncing too often. - if (!shouldPerformSync(rateLimitBackoffHandler, "rate", extras)) { - syncDelegate.postponeSync(rateLimitBackoffHandler.delayMilliseconds()); - return; - } - - final SchedulePolicy schedulePolicy = new FxAccountSchedulePolicy(context, fxAccount); - - // Set a small scheduled 'backoff' to rate-limit the next sync, - // and extend the background delay even further into the future. - schedulePolicy.configureBackoffMillisBeforeSyncing(rateLimitBackoffHandler, backgroundBackoffHandler); - - final String tokenServerEndpoint = fxAccount.getTokenServerURI(); - final URI tokenServerEndpointURI = new URI(tokenServerEndpoint); - final String audience = FxAccountUtils.getAudienceForURL(tokenServerEndpoint); - - try { - // The clock starts... now! - fxAccount.acquireSharedAccountStateLock(FxAccountSyncAdapter.LOG_TAG); - } catch (InterruptedException e) { - // OK, skip this sync. - syncDelegate.handleError(e); - return; - } - - final State state; - try { - state = fxAccount.getState(); - } catch (Exception e) { - fxAccount.releaseSharedAccountStateLock(); - syncDelegate.handleError(e); - return; - } - - TelemetryWrapper.addToHistogram(TelemetryContract.SYNC_STARTED, 1); - - final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine(); - stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) { - @Override - public void handleNotMarried(State notMarried) { - Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel()); - schedulePolicy.onHandleFinal(notMarried.getNeededAction()); - syncDelegate.handleCannotSync(notMarried); - } - - private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) { - return shouldPerformSync(tokenBackoffHandler, "token", extras); - } - - @Override - public void handleMarried(Married married) { - schedulePolicy.onHandleFinal(married.getNeededAction()); - Logger.info(LOG_TAG, "handleMarried: in " + married.getStateLabel()); - - try { - final String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER); - - /* - * At this point we're in the correct state to sync, and we're ready to fetch - * a token and do some work. - * - * But first we need to do two things: - * 1. Check to see whether we're in a backoff situation for the token server. - * If we are, but we're not forcing a sync, then we go no further. - * 2. Clear an existing backoff (if we're syncing it doesn't matter, and if - * we're forcing we'll get a new backoff if things are still bad). - * - * Note that we don't check the storage backoff before the token dance: the token - * server tells us which server we're syncing to! - * - * That logic lives in the TokenServerClientDelegate elsewhere in this file. - */ - - // Strictly speaking this backoff check could be done prior to walking through - // the login state machine, allowing us to short-circuit sooner. - // We don't expect many token server backoffs, and most users will be sitting - // in the Married state, so instead we simply do this here, once. - final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token"); - if (!shouldRequestToken(tokenBackoffHandler, extras)) { - Logger.info(LOG_TAG, "Not syncing (token server)."); - syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds()); - return; - } - - final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); - final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); - final String clientState = married.getClientState(); - syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount); - - // Register the device if necessary (asynchronous, in another thread) - if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION - || TextUtils.isEmpty(fxAccount.getDeviceId())) { - FxAccountDeviceRegistrator.register(context); - } - - // Force fetch the profile avatar information. (asynchronous, in another thread) - Logger.info(LOG_TAG, "Fetching profile avatar information."); - fxAccount.fetchProfileJSON(); - } catch (Exception e) { - syncDelegate.handleError(e); - return; - } - } - }); - - latch.take(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Got error syncing.", e); - syncDelegate.handleError(e); - } finally { - fxAccount.releaseSharedAccountStateLock(); - } - - Logger.info(LOG_TAG, "Syncing done."); - lastSyncRealtimeMillis = SystemClock.elapsedRealtime(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java deleted file mode 100644 index 71148f66c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncDelegate.java +++ /dev/null @@ -1,110 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import java.util.concurrent.BlockingQueue; - -import org.mozilla.gecko.fxa.login.State; - -import android.content.SyncResult; - -public class FxAccountSyncDelegate { - public enum Result { - Success, - Error, - Postponed, - Rejected, - } - - protected final BlockingQueue<Result> latch; - protected final SyncResult syncResult; - - public FxAccountSyncDelegate(BlockingQueue<Result> latch, SyncResult syncResult) { - if (latch == null) { - throw new IllegalArgumentException("latch must not be null"); - } - if (syncResult == null) { - throw new IllegalArgumentException("syncResult must not be null"); - } - this.latch = latch; - this.syncResult = syncResult; - } - - /** - * No error! Say that we made progress. - */ - protected void setSyncResultSuccess() { - syncResult.stats.numUpdates += 1; - } - - /** - * Soft error. Say that we made progress, so that Android will sync us again - * after exponential backoff. - */ - protected void setSyncResultSoftError() { - syncResult.stats.numUpdates += 1; - syncResult.stats.numIoExceptions += 1; - } - - /** - * Hard error. We don't want Android to sync us again, even if we make - * progress, until the user intervenes. - */ - protected void setSyncResultHardError() { - syncResult.stats.numAuthExceptions += 1; - } - - public void handleSuccess() { - setSyncResultSuccess(); - latch.offer(Result.Success); - } - - public void handleError(Exception e) { - setSyncResultSoftError(); - latch.offer(Result.Error); - } - - /** - * When the login machine terminates, we might not be in the - * <code>Married</code> state, and therefore we can't sync. This method - * messages as much to the user. - * <p> - * To avoid stopping us syncing altogether, we set a soft error rather than - * a hard error. In future, we would like to set a hard error if we are in, - * for example, the <code>Separated</code> state, and then have some user - * initiated activity mark the Android account as ready to sync again. This - * is tricky, though, so we play it safe for now. - * - * @param finalState - * that login machine ended in. - */ - public void handleCannotSync(State finalState) { - setSyncResultSoftError(); - latch.offer(Result.Error); - } - - public void postponeSync(long millis) { - if (millis > 0) { - // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669 - // So we don't bother doing this. Instead, we rely on the periodic sync - // we schedule, and the backoff handler for the rest. - /* - Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms."); - syncResult.delayUntil = millis / 1000; - */ - } - setSyncResultSoftError(); - latch.offer(Result.Postponed); - } - - /** - * Simply don't sync, without setting any error flags. - * This is the appropriate behavior when a routine backoff has not yet - * been met. - */ - public void rejectSync() { - latch.offer(Result.Rejected); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java deleted file mode 100644 index 59c06ca97..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncService.java +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; - -public class FxAccountSyncService extends Service { - private static final Object syncAdapterLock = new Object(); - private static FxAccountSyncAdapter syncAdapter; - - @Override - public void onCreate() { - synchronized (syncAdapterLock) { - if (syncAdapter == null) { - syncAdapter = new FxAccountSyncAdapter(getApplicationContext(), true); - } - } - } - - @Override - public IBinder onBind(Intent intent) { - return syncAdapter.getSyncAdapterBinder(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java deleted file mode 100644 index ca64d4f87..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncStatusHelper.java +++ /dev/null @@ -1,113 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import java.util.Map; -import java.util.Map.Entry; -import java.util.WeakHashMap; - -import org.mozilla.gecko.fxa.SyncStatusListener; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.ContentResolver; -import android.content.SyncStatusObserver; - -/** - * Abstract away some details of Android's SyncStatusObserver. - * <p> - * Provides a simplified sync started/sync finished delegate. - */ -public class FxAccountSyncStatusHelper implements SyncStatusObserver { - @SuppressWarnings("unused") - private static final String LOG_TAG = FxAccountSyncStatusHelper.class.getSimpleName(); - - protected static FxAccountSyncStatusHelper sInstance; - - public synchronized static FxAccountSyncStatusHelper getInstance() { - if (sInstance == null) { - sInstance = new FxAccountSyncStatusHelper(); - } - return sInstance; - } - - // Used to unregister this as a listener. - protected Object handle; - - // Maps delegates to whether their underlying Android account was syncing the - // last time we observed a status change. - protected Map<SyncStatusListener, Boolean> delegates = new WeakHashMap<SyncStatusListener, Boolean>(); - - @Override - public synchronized void onStatusChanged(int which) { - for (Entry<SyncStatusListener, Boolean> entry : delegates.entrySet()) { - final SyncStatusListener delegate = entry.getKey(); - final AndroidFxAccount fxAccount = new AndroidFxAccount(delegate.getContext(), delegate.getAccount()); - final boolean active = fxAccount.isCurrentlySyncing(); - // Remember for later. - boolean wasActiveLastTime = entry.getValue(); - // It's okay to update the value of an entry while iterating the entrySet. - entry.setValue(active); - - if (active && !wasActiveLastTime) { - // We've started a sync. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - delegate.onSyncStarted(); - } - }); - } - - if (!active && wasActiveLastTime) { - // We've finished a sync. - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - delegate.onSyncFinished(); - } - }); - } - } - } - - protected void addListener() { - final int mask = ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; - if (this.handle != null) { - throw new IllegalStateException("Already registered this as an observer?"); - } - this.handle = ContentResolver.addStatusChangeListener(mask, this); - } - - protected void removeListener() { - Object handle = this.handle; - this.handle = null; - if (handle != null) { - ContentResolver.removeStatusChangeListener(handle); - } - } - - public synchronized void startObserving(SyncStatusListener delegate) { - if (delegate == null) { - throw new IllegalArgumentException("delegate must not be null"); - } - if (delegates.containsKey(delegate)) { - return; - } - // If we are the first delegate to the party, start listening. - if (delegates.isEmpty()) { - addListener(); - } - delegates.put(delegate, Boolean.FALSE); - } - - public synchronized void stopObserving(SyncStatusListener delegate) { - delegates.remove(delegate); - // If we are the last delegate leaving the party, stop listening. - if (delegates.isEmpty()) { - removeListener(); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java deleted file mode 100644 index 809191f5e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/SchedulePolicy.java +++ /dev/null @@ -1,43 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.fxa.sync; - -import org.mozilla.gecko.fxa.login.State.Action; -import org.mozilla.gecko.sync.BackoffHandler; - -public interface SchedulePolicy { - /** - * Call this with the number of other clients syncing to the account. - */ - public abstract void onSuccessfulSync(int otherClientsCount); - public abstract void onHandleFinal(Action needed); - public abstract void onUpgradeRequired(); - public abstract void onUnauthorized(); - - /** - * Before a sync we typically wish to adjust our backoff policy. This cleans - * the slate prior to encountering a new backoff, and also functions as a rate - * limiter. - * - * The {@link SchedulePolicy} acts as a controller for the {@link BackoffHandler}. - * As a result of calling these two methods, the {@link BackoffHandler} will be - * mutated, and additional side-effects (such as scheduling periodic syncs) can - * occur. - * - * @param rateHandler the backoff handler to configure for basic rate limiting. - * @param backgroundHandler the backoff handler to configure for background operations. - */ - public abstract void configureBackoffMillisBeforeSyncing(BackoffHandler rateHandler, BackoffHandler backgroundHandler); - - /** - * We received an explicit backoff instruction, typically from a server. - * - * @param onlyExtend - * if <code>true</code>, the backoff handler will be asked to update - * its backoff only if the provided value is greater than the current - * backoff. - */ - public abstract void configureBackoffMillisOnBackoff(BackoffHandler backoffHandler, long backoffMillis, boolean onlyExtend); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java deleted file mode 100644 index 3bbb7e8b4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.push; - -/** - * Thin container for a register User-Agent response. - */ -public class RegisterUserAgentResponse { - public final String uaid; - public final String secret; - - public RegisterUserAgentResponse(String uaid, String secret) { - this.uaid = uaid; - this.secret = secret; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java deleted file mode 100644 index 009a7f838..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.push; - -/** - * Thin container for a subscribe channel response. - */ -public class SubscribeChannelResponse { - public final String channelID; - public final String endpoint; - - public SubscribeChannelResponse(String channelID, String endpoint) { - this.channelID = channelID; - this.endpoint = endpoint; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java deleted file mode 100644 index 8edd92f9e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java +++ /dev/null @@ -1,410 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.push.autopush; - -import android.text.TextUtils; - -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.fxa.FxAccountConstants; -import org.mozilla.gecko.push.RegisterUserAgentResponse; -import org.mozilla.gecko.push.SubscribeChannelResponse; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider; -import org.mozilla.gecko.sync.net.Resource; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.Locale; -import java.util.concurrent.Executor; - -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - - -/** - * Interact with the autopush endpoint HTTP API. - * <p/> - * The API is a Mozilla-proprietary interface, and not even specified to Mozilla's usual ad-hoc standards. - * This client is written against a work-in-progress, un-deployed upstream commit. - */ -public class AutopushClient { - protected static final String LOG_TAG = AutopushClient.class.getSimpleName(); - - protected static final String ACCEPT_HEADER = "application/json;charset=utf-8"; - protected static final String TYPE = "gcm"; - - protected static final String JSON_KEY_UAID = "uaid"; - protected static final String JSON_KEY_SECRET = "secret"; - protected static final String JSON_KEY_CHANNEL_ID = "channelID"; - protected static final String JSON_KEY_ENDPOINT = "endpoint"; - - protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT }; - protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT }; - - public static final String JSON_KEY_CODE = "code"; - public static final String JSON_KEY_ERRNO = "errno"; - public static final String JSON_KEY_ERROR = "error"; - public static final String JSON_KEY_MESSAGE = "message"; - - protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE }; - protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO }; - - /** - * The server's URI. - * <p> - * We assume throughout that this ends with a trailing slash (and guarantee as - * much in the constructor). - */ - public final String serverURI; - - protected final Executor executor; - - public AutopushClient(String serverURI, Executor executor) { - if (serverURI == null) { - throw new IllegalArgumentException("Must provide a server URI."); - } - if (executor == null) { - throw new IllegalArgumentException("Must provide a non-null executor."); - } - this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/"; - if (!this.serverURI.endsWith("/")) { - throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI); - } - this.executor = executor; - } - - /** - * A legal autopush server URL includes a sender ID embedded into it. Extract it. - * - * @return a non-null non-empty sender ID. - * @throws AutopushClientException on failure. - */ - public String getSenderIDFromServerURI() throws AutopushClientException { - // Turn "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407/" into "829133274407". - final String[] parts = serverURI.split("/", -1); // The -1 keeps the trailing empty part. - if (parts.length < 3) { - throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); - } - if (!TextUtils.isEmpty(parts[parts.length - 1])) { - // We guarantee a trailing slash, so we should always have an empty part at the tail. - throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); - } - if (!TextUtils.equals("gcm", parts[parts.length - 3])) { - // We should always have /gcm/senderID/. - throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); - } - final String senderID = parts[parts.length - 2]; - if (TextUtils.isEmpty(senderID)) { - // Something is horribly wrong -- we have /gcm//. Abort. - throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI); - } - return senderID; - } - - /** - * Process a typed value extracted from a successful response (in an - * endpoint-dependent way). - */ - public interface RequestDelegate<T> { - void handleError(Exception e); - void handleFailure(AutopushClientException e); - void handleSuccess(T result); - } - - /** - * Intepret a response from the autopush server. - * <p> - * Throw an appropriate exception on errors; otherwise, return the response's - * status code. - * - * @return response's HTTP status code. - * @throws AutopushClientException - */ - public static int validateResponse(HttpResponse response) throws AutopushClientException { - final int status = response.getStatusLine().getStatusCode(); - if (200 <= status && status <= 299) { - return status; - } - long code; - long errno; - String error; - String message; - String info; - ExtendedJSONObject body; - try { - body = new SyncStorageResponse(response).jsonObjectBody(); - // TODO: The service doesn't do the right thing yet :( - // body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class); - body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class); - // Would throw above if missing; the -1 defaults quiet NPE warnings. - code = body.getLong(JSON_KEY_CODE, -1); - errno = body.getLong(JSON_KEY_ERRNO, -1); - error = body.getString(JSON_KEY_ERROR); - message = body.getString(JSON_KEY_MESSAGE); - } catch (Exception e) { - throw new AutopushClientException.AutopushClientMalformedResponseException(response); - } - throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body); - } - - protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); - } - - protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) { - try { - if (requestBody == null) { - resource.post((HttpEntity) null); - } else { - resource.post(requestBody); - } - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - } - - /** - * Translate resource callbacks into request callbacks invoked on the provided - * executor. - * <p> - * Override <code>handleSuccess</code> to parse the body of the resource - * request and call the request callback. <code>handleSuccess</code> is - * invoked via the executor, so you don't need to delegate further. - */ - protected abstract class ResourceDelegate<T> extends BaseResourceDelegate { - protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body); - - protected final String secret; - protected final RequestDelegate<T> delegate; - - /** - * Create a delegate for an un-authenticated resource. - */ - public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate<T> delegate) { - super(resource); - this.delegate = delegate; - this.secret = secret; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - if (secret != null) { - return new BearerAuthHeaderProvider(secret); - } - return null; - } - - @Override - public String getUserAgent() { - return FxAccountConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(HttpResponse response) { - try { - final int status = validateResponse(response); - invokeHandleSuccess(status, response); - } catch (AutopushClientException e) { - invokeHandleFailure(e); - } - } - - protected void invokeHandleFailure(final AutopushClientException e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(e); - } - }); - } - - protected void invokeHandleSuccess(final int status, final HttpResponse response) { - executor.execute(new Runnable() { - @Override - public void run() { - try { - ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody(); - ResourceDelegate.this.handleSuccess(status, response, body); - } catch (Exception e) { - delegate.handleError(e); - } - } - }); - } - - @Override - public void handleHttpProtocolException(final ClientProtocolException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleHttpIOException(IOException e) { - invokeHandleError(delegate, e); - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - invokeHandleError(delegate, e); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - super.addHeaders(request, client); - - // The basics. - final Locale locale = Locale.getDefault(); - request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale)); - request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER); - } - } - - public void registerUserAgent(final String token, RequestDelegate<RegisterUserAgentResponse> delegate) { - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "registration")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<RegisterUserAgentResponse>(resource, null, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - body.throwIfFieldsMissingOrMisTyped(REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS, String.class); - final String uaid = body.getString(JSON_KEY_UAID); - final String secret = body.getString(JSON_KEY_SECRET); - delegate.handleSuccess(new RegisterUserAgentResponse(uaid, secret)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - final ExtendedJSONObject body = new ExtendedJSONObject(); - body.put("type", TYPE); - body.put("token", token); - - resource.post(body); - } - - public void reregisterUserAgent(final String uaid, final String secret, final String token, RequestDelegate<Void> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "registration/" + uaid)); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - delegate.handleSuccess(null); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - final ExtendedJSONObject body = new ExtendedJSONObject(); - body.put("type", TYPE); - body.put("token", token); - - resource.put(body); - } - - - public void subscribeChannel(final String uaid, final String secret, final String appServerKey, RequestDelegate<SubscribeChannelResponse> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription")); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<SubscribeChannelResponse>(resource, secret, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - body.throwIfFieldsMissingOrMisTyped(REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS, String.class); - final String channelID = body.getString(JSON_KEY_CHANNEL_ID); - final String endpoint = body.getString(JSON_KEY_ENDPOINT); - delegate.handleSuccess(new SubscribeChannelResponse(channelID, endpoint)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - - final ExtendedJSONObject body = new ExtendedJSONObject(); - body.put("key", appServerKey); - resource.post(body); - } - - public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate<Void> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID)); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - delegate.handleSuccess(null); - } - }; - - resource.delete(); - } - - public void unregisterUserAgent(final String uaid, final String secret, RequestDelegate<Void> delegate) { - final BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "registration/" + uaid)); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - delegate.handleSuccess(null); - } - }; - - resource.delete(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java deleted file mode 100644 index e3fda7a45..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java +++ /dev/null @@ -1,81 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.push.autopush; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpStatus; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public class AutopushClientException extends Exception { - private static final long serialVersionUID = 7953459541558266500L; - - public AutopushClientException(String detailMessage) { - super(detailMessage); - } - - public AutopushClientException(Exception e) { - super(e); - } - - public boolean isTransientError() { - return false; - } - - public static class AutopushClientRemoteException extends AutopushClientException { - private static final long serialVersionUID = 2209313149952001000L; - - public final HttpResponse response; - public final long httpStatusCode; - public final long apiErrorNumber; - public final String error; - public final String message; - public final ExtendedJSONObject body; - - public AutopushClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) { - super(new HTTPFailureException(new SyncStorageResponse(response))); - if (body == null) { - throw new IllegalArgumentException("body must not be null"); - } - this.response = response; - this.httpStatusCode = httpStatusCode; - this.apiErrorNumber = apiErrorNumber; - this.error = error; - this.message = message; - this.body = body; - } - - @Override - public String toString() { - return "<AutopushClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">"; - } - - public boolean isInvalidAuthentication() { - return httpStatusCode == HttpStatus.SC_UNAUTHORIZED; - } - - public boolean isNotFound() { - return httpStatusCode == HttpStatus.SC_NOT_FOUND; - } - - public boolean isGone() { - return httpStatusCode == HttpStatus.SC_GONE; - } - - @Override - public boolean isTransientError() { - return httpStatusCode >= 500; - } - } - - public static class AutopushClientMalformedResponseException extends AutopushClientRemoteException { - private static final long serialVersionUID = 2209313149952001909L; - - public AutopushClientMalformedResponseException(HttpResponse response) { - super(response, 0, 999, "Response malformed", "Response malformed", new ExtendedJSONObject()); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java deleted file mode 100644 index 75eb5ad37..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/AlreadySyncingException.java +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; - -import android.content.SyncResult; - -public class AlreadySyncingException extends SyncException { - Stage inState; - public AlreadySyncingException(Stage currentState) { - inState = currentState; - } - - private static final long serialVersionUID = -5647548462539009893L; - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java deleted file mode 100644 index abb880621..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BackoffHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - - -public interface BackoffHandler { - public long getEarliestNextRequest(); - - /** - * Provide a timestamp in millis before which we shouldn't sync again. - * Overrides any existing value. - * - * @param next - * a timestamp in milliseconds. - */ - public void setEarliestNextRequest(long next); - - /** - * Provide a timestamp in millis before which we shouldn't sync again. Only - * change our persisted value if it's later than the existing time. - * - * @param next - * a timestamp in milliseconds. - */ - public void extendEarliestNextRequest(long next); - - /** - * Return the number of milliseconds until we're allowed to sync again, - * or 0 if now is fine. - */ - public long delayMilliseconds(); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java deleted file mode 100644 index 3db93652d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/BadRequiredFieldJSONException.java +++ /dev/null @@ -1,5 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java deleted file mode 100644 index 1fd363bcb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java +++ /dev/null @@ -1,199 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map.Entry; -import java.util.Set; - -import org.json.simple.JSONArray; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.sync.crypto.CryptoException; -import org.mozilla.gecko.sync.crypto.KeyBundle; - -public class CollectionKeys { - private KeyBundle defaultKeyBundle = null; - private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>(); - - /** - * Randomly generate a basic CollectionKeys object. - * @throws CryptoException - */ - public static CollectionKeys generateCollectionKeys() throws CryptoException { - CollectionKeys ck = new CollectionKeys(); - ck.clear(); - ck.defaultKeyBundle = KeyBundle.withRandomKeys(); - // TODO: eventually we would like to keep per-collection keys, just generate - // new ones as appropriate. - return ck; - } - - public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException { - if (this.defaultKeyBundle == null) { - throw new NoCollectionKeysSetException(); - } - return this.defaultKeyBundle; - } - - public boolean keyBundleForCollectionIsNotDefault(String collection) { - return collectionKeyBundles.containsKey(collection); - } - - public KeyBundle keyBundleForCollection(String collection) - throws NoCollectionKeysSetException { - if (this.defaultKeyBundle == null) { - throw new NoCollectionKeysSetException(); - } - if (keyBundleForCollectionIsNotDefault(collection)) { - return collectionKeyBundles.get(collection); - } - return this.defaultKeyBundle; - } - - /** - * Take a pair of values in a JSON array, handing them off to KeyBundle to - * produce a usable keypair. - */ - private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException { - String encKeyStr = (String) array.get(0); - String hmacKeyStr = (String) array.get(1); - return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr); - } - - @SuppressWarnings("unchecked") - private static JSONArray keyBundleToArray(KeyBundle bundle) { - // Generate JSON. - JSONArray keysArray = new JSONArray(); - keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey()))); - keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey()))); - return keysArray; - } - - private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException { - ExtendedJSONObject json = new ExtendedJSONObject(); - json.put("id", "keys"); - json.put("collection", "crypto"); - json.put("default", keyBundleToArray(this.defaultKeyBundle())); - ExtendedJSONObject colls = new ExtendedJSONObject(); - for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) { - colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue())); - } - json.put("collections", colls); - return json; - } - - public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException { - ExtendedJSONObject payload = this.asRecordContents(); - CryptoRecord record = new CryptoRecord(payload); - record.collection = "crypto"; - record.guid = "keys"; - record.deleted = false; - return record; - } - - /** - * Set my key bundle and collection keys with the given key bundle and data - * (possibly decrypted) from the given record. - * - * @param keys - * A "crypto/keys" <code>CryptoRecord</code>, encrypted with - * <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null. - * @param syncKeyBundle - * If non-null, the sync key bundle to decrypt <code>keys</code> with. - */ - public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle) - throws CryptoException, IOException, NonObjectJSONException { - if (keys == null) { - throw new IllegalArgumentException("cannot set key pairs from null record"); - } - if (syncKeyBundle != null) { - keys.keyBundle = syncKeyBundle; - keys.decrypt(); - } - ExtendedJSONObject cleartext = keys.payload; - KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default")); - - ExtendedJSONObject collections = cleartext.getObject("collections"); - HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>(); - for (Entry<String, Object> pair : collections.entrySet()) { - KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue()); - collectionKeys.put(pair.getKey(), bundle); - } - - this.collectionKeyBundles.clear(); - this.collectionKeyBundles.putAll(collectionKeys); - this.defaultKeyBundle = defaultKey; - } - - public void setKeyBundleForCollection(String collection, KeyBundle keys) { - this.collectionKeyBundles.put(collection, keys); - } - - public void setDefaultKeyBundle(KeyBundle keys) { - this.defaultKeyBundle = keys; - } - - public void clear() { - this.defaultKeyBundle = null; - this.collectionKeyBundles.clear(); - } - - /** - * Return set of collections where key is either missing from one collection - * or not the same in both collections. - * <p> - * Does not check for different default keys. - */ - public static Set<String> differences(CollectionKeys a, CollectionKeys b) { - Set<String> differences = new HashSet<String>(); - Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet()); - collections.addAll(b.collectionKeyBundles.keySet()); - - // Iterate through one collection, collecting missing and differences. - for (String collection : collections) { - KeyBundle keyA; - KeyBundle keyB; - try { - keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate. - keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate. - } catch (NoCollectionKeysSetException e) { - differences.add(collection); - continue; - } - // keyA and keyB are not null at this point. - if (!keyA.equals(keyB)) { - differences.add(collection); - } - } - - return differences; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof CollectionKeys)) { - return false; - } - CollectionKeys other = (CollectionKeys) o; - try { - // It would be nice to use map equality here, but there can be map entries - // where the key is the default key that should compare equal to a missing - // map entry. Therefore, we always compute the set of differences. - return defaultKeyBundle().equals(other.defaultKeyBundle()) && - CollectionKeys.differences(this, other).isEmpty(); - } catch (NoCollectionKeysSetException e) { - // If either default key bundle is not set, we'll say the bundles are not equal. - return false; - } - } - - @Override - public int hashCode() { - return super.hashCode(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java deleted file mode 100644 index 371603de5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandProcessor.java +++ /dev/null @@ -1,261 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Process commands received from Sync clients. - * <p> - * We need a command processor at two different times: - * <ol> - * <li>We execute commands during the "clients" engine stage of a Sync. Each - * command takes a <code>GlobalSession</code> instance as a parameter.</li> - * <li>We queue commands to be executed or propagated to other Sync clients - * during an activity completely unrelated to a sync</li> - * </ol> - * To provide a processor for both these time frames, we maintain a static - * long-lived singleton. - */ -public class CommandProcessor { - private static final String LOG_TAG = "Command"; - private static final AtomicInteger currentId = new AtomicInteger(); - protected ConcurrentHashMap<String, CommandRunner> commands = new ConcurrentHashMap<String, CommandRunner>(); - - private final static CommandProcessor processor = new CommandProcessor(); - - /** - * Get the global singleton command processor. - * - * @return the singleton processor. - */ - public static CommandProcessor getProcessor() { - return processor; - } - - public static class Command { - public final String commandType; - public final JSONArray args; - private List<String> argsList; - - public Command(String commandType, JSONArray args) { - this.commandType = commandType; - this.args = args; - } - - /** - * Get list of arguments as strings. Individual arguments may be null. - * - * @return list of strings. - */ - public synchronized List<String> getArgsList() { - if (argsList == null) { - ArrayList<String> argsList = new ArrayList<String>(args.size()); - - for (int i = 0; i < args.size(); i++) { - final Object arg = args.get(i); - if (arg == null) { - argsList.add(null); - continue; - } - argsList.add(arg.toString()); - } - this.argsList = argsList; - } - return this.argsList; - } - - @SuppressWarnings("unchecked") - public JSONObject asJSONObject() { - JSONObject out = new JSONObject(); - out.put("command", this.commandType); - out.put("args", this.args); - return out; - } - } - - /** - * Register a command. - * <p> - * Any existing registration is overwritten. - * - * @param commandType - * the name of the command, i.e., "displayURI". - * @param command - * the <code>CommandRunner</code> instance that should handle the - * command. - */ - public void registerCommand(String commandType, CommandRunner command) { - commands.put(commandType, command); - } - - /** - * Process a command in the context of the given global session. - * - * @param session - * the <code>GlobalSession</code> instance currently executing. - * @param unparsedCommand - * command as a <code>ExtendedJSONObject</code> instance. - */ - public void processCommand(final GlobalSession session, ExtendedJSONObject unparsedCommand) { - Command command = parseCommand(unparsedCommand); - if (command == null) { - Logger.debug(LOG_TAG, "Invalid command: " + unparsedCommand + " will not be processed."); - return; - } - - CommandRunner executableCommand = commands.get(command.commandType); - if (executableCommand == null) { - Logger.debug(LOG_TAG, "Command \"" + command.commandType + "\" not registered and will not be processed."); - return; - } - - executableCommand.executeCommand(session, command.getArgsList()); - } - - /** - * Parse a JSON command into a ParsedCommand object for easier handling. - * - * @param unparsedCommand - command as ExtendedJSONObject - * @return - null if command is invalid, else return ParsedCommand with - * no null attributes. - */ - protected static Command parseCommand(ExtendedJSONObject unparsedCommand) { - String type = (String) unparsedCommand.get("command"); - if (type == null) { - return null; - } - - try { - JSONArray unparsedArgs = unparsedCommand.getArray("args"); - if (unparsedArgs == null) { - return null; - } - - return new Command(type, unparsedArgs); - } catch (NonArrayJSONException e) { - Logger.debug(LOG_TAG, "Unable to parse args array. Invalid command"); - return null; - } - } - - @SuppressWarnings("unchecked") - public void sendURIToClientForDisplay(String uri, String clientID, String title, String sender, Context context) { - Logger.info(LOG_TAG, "Sending URI to client " + clientID + "."); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "URI is " + uri + "; title is '" + title + "'."); - } - - final JSONArray args = new JSONArray(); - args.add(uri); - args.add(sender); - args.add(title); - - final Command displayURICommand = new Command("displayURI", args); - this.sendCommand(clientID, displayURICommand, context); - } - - /** - * Validates and sends a command to a client or all clients. - * - * Calling this does not actually sync the command data to the server. If the - * client already has the command/args pair, it won't receive a duplicate - * command. - * - * @param clientID - * Client ID to send command to. If null, send to all remote - * clients. - * @param command - * Command to invoke on remote clients - */ - public void sendCommand(String clientID, Command command, Context context) { - Logger.debug(LOG_TAG, "In sendCommand."); - - CommandRunner commandData = commands.get(command.commandType); - - // Don't send commands that we don't know about. - if (commandData == null) { - Logger.error(LOG_TAG, "Unknown command to send: " + command); - return; - } - - // Don't send a command with the wrong number of arguments. - if (!commandData.argumentsAreValid(command.getArgsList())) { - Logger.error(LOG_TAG, "Expected " + commandData.argCount + " args for '" + - command + "', but got " + command.args); - return; - } - - if (clientID != null) { - this.sendCommandToClient(clientID, command, context); - return; - } - - ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); - try { - Map<String, ClientRecord> clientMap = db.fetchAllClients(); - for (ClientRecord client : clientMap.values()) { - this.sendCommandToClient(client.guid, command, context); - } - } catch (NullCursorException e) { - Logger.error(LOG_TAG, "NullCursorException when fetching all GUIDs"); - } finally { - db.close(); - } - } - - protected void sendCommandToClient(String clientID, Command command, Context context) { - Logger.info(LOG_TAG, "Sending " + command.commandType + " to " + clientID); - - ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context); - try { - db.store(clientID, command); - } catch (NullCursorException e) { - Logger.error(LOG_TAG, "NullCursorException: Unable to send command."); - } finally { - db.close(); - } - } - - public static void displayURI(final List<String> args, final Context context) { - // We trust the client sender that these exist. - final String uri = args.get(0); - final String clientId = args.get(1); - Logger.pii(LOG_TAG, "Received a URI for display: " + uri + " from " + clientId); - - if (uri == null) { - Logger.pii(LOG_TAG, "URI is null – ignoring"); - return; - } - - String title = null; - if (args.size() == 3) { - title = args.get(2); - } - - final Intent sendTabNotificationIntent = new Intent(); - sendTabNotificationIntent.setClassName(context, BrowserContract.TAB_RECEIVED_SERVICE_CLASS_NAME); - sendTabNotificationIntent.setData(Uri.parse(uri)); - sendTabNotificationIntent.putExtra(Intent.EXTRA_TITLE, title); - sendTabNotificationIntent.putExtra(BrowserContract.EXTRA_CLIENT_GUID, clientId); - final ComponentName componentName = context.startService(sendTabNotificationIntent); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java deleted file mode 100644 index c7a0f1762..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CommandRunner.java +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.util.List; - -public abstract class CommandRunner { - public final int argCount; - - public CommandRunner(int argCount) { - this.argCount = argCount; - } - - public abstract void executeCommand(GlobalSession session, List<String> args); - - public boolean argumentsAreValid(List<String> args) { - return args != null && - args.size() == argCount; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java deleted file mode 100644 index f9004e14c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CredentialException.java +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -/** - * There was a problem with the Sync account's credentials: bad username, - * missing password, malformed sync key, etc. - */ -public abstract class CredentialException extends SyncException { - private static final long serialVersionUID = 833010553314100538L; - - public CredentialException() { - super(); - } - - public CredentialException(final Throwable e) { - super(e); - } - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - syncResult.stats.numAuthExceptions += 1; - } - - /** - * No credentials at all. - */ - public static class MissingAllCredentialsException extends CredentialException { - private static final long serialVersionUID = 3763937096217604611L; - - public MissingAllCredentialsException() { - super(); - } - - public MissingAllCredentialsException(final Throwable e) { - super(e); - } - } - - /** - * Some credential is missing. - */ - public static class MissingCredentialException extends CredentialException { - private static final long serialVersionUID = -7543031216547596248L; - - public final String missingCredential; - - public MissingCredentialException(final String missingCredential) { - this.missingCredential = missingCredential; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java deleted file mode 100644 index 65563d344..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java +++ /dev/null @@ -1,255 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; - -import org.json.simple.JSONObject; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.sync.crypto.CryptoException; -import org.mozilla.gecko.sync.crypto.CryptoInfo; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.crypto.MissingCryptoInputException; -import org.mozilla.gecko.sync.crypto.NoKeyBundleException; -import org.mozilla.gecko.sync.repositories.domain.Record; -import org.mozilla.gecko.sync.repositories.domain.RecordParseException; - -/** - * A Sync crypto record has: - * - * <ul> - * <li>a collection of fields which are not encrypted (id and collection);</il> - * <li>a set of metadata fields (index, modified, ttl);</il> - * <li>a payload, which is encrypted and decrypted on request.</il> - * </ul> - * - * The payload flips between being a blob of JSON with hmac/IV/ciphertext - * attributes and the cleartext itself. - * - * Until there's some benefit to the abstraction, we're simply going to call - * this <code>CryptoRecord</code>. - * - * <code>CryptoRecord</code> uses <code>CryptoInfo</code> to do the actual - * encryption and decryption. - */ -public class CryptoRecord extends Record { - - // JSON related constants. - private static final String KEY_ID = "id"; - private static final String KEY_COLLECTION = "collection"; - private static final String KEY_PAYLOAD = "payload"; - private static final String KEY_MODIFIED = "modified"; - private static final String KEY_SORTINDEX = "sortindex"; - private static final String KEY_TTL = "ttl"; - private static final String KEY_CIPHERTEXT = "ciphertext"; - private static final String KEY_HMAC = "hmac"; - private static final String KEY_IV = "IV"; - - /** - * Helper method for doing actual decryption. - * - * Input: JSONObject containing a valid payload (cipherText, IV, HMAC), - * KeyBundle with keys for decryption. Output: byte[] clearText - * @throws CryptoException - * @throws UnsupportedEncodingException - */ - private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException { - byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8")); - byte[] iv = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8")); - byte[] hmac = Utils.hex2Byte((String) payload.get(KEY_HMAC)); - - return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage(); - } - - // The encrypted JSON body object. - // The decrypted JSON body object. Fields are copied from `body`. - - public ExtendedJSONObject payload; - public KeyBundle keyBundle; - - /** - * Don't forget to set cleartext or body! - */ - public CryptoRecord() { - super(null, null, 0, false); - } - - public CryptoRecord(ExtendedJSONObject payload) { - super(null, null, 0, false); - if (payload == null) { - throw new IllegalArgumentException( - "No payload provided to CryptoRecord constructor."); - } - this.payload = payload; - } - - public CryptoRecord(String jsonString) throws IOException, NonObjectJSONException { - - this(new ExtendedJSONObject(jsonString)); - } - - /** - * Create a new CryptoRecord with the same metadata as an existing record. - * - * @param source - */ - public CryptoRecord(Record source) { - super(source.guid, source.collection, source.lastModified, source.deleted); - this.ttl = source.ttl; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - CryptoRecord out = new CryptoRecord(this); - out.guid = guid; - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - out.payload = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object); - out.keyBundle = this.keyBundle; // TODO: copy me? - return out; - } - - /** - * Take a whole record as JSON -- i.e., something like - * - * {"payload": "{...}", "id":"foobarbaz"} - * - * and turn it into a CryptoRecord object. - * - * @param jsonRecord - * @return - * A CryptoRecord that encapsulates the provided record. - * - * @throws NonObjectJSONException - * @throws IOException - */ - public static CryptoRecord fromJSONRecord(String jsonRecord) - throws NonObjectJSONException, IOException, RecordParseException { - byte[] bytes = jsonRecord.getBytes("UTF-8"); - ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes); - - return CryptoRecord.fromJSONRecord(object); - } - - // TODO: defensive programming. - public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord) - throws IOException, NonObjectJSONException, RecordParseException { - String id = (String) jsonRecord.get(KEY_ID); - String collection = (String) jsonRecord.get(KEY_COLLECTION); - String jsonEncodedPayload = (String) jsonRecord.get(KEY_PAYLOAD); - - ExtendedJSONObject payload = new ExtendedJSONObject(jsonEncodedPayload); - - CryptoRecord record = new CryptoRecord(payload); - record.guid = id; - record.collection = collection; - if (jsonRecord.containsKey(KEY_MODIFIED)) { - Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED); - if (timestamp == null) { - throw new RecordParseException("timestamp could not be parsed"); - } - record.lastModified = timestamp; - } - if (jsonRecord.containsKey(KEY_SORTINDEX)) { - // getLong tries to cast to Long, and might return null. We catch all - // exceptions, just to be safe. - try { - record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX); - } catch (Exception e) { - throw new RecordParseException("timestamp could not be parsed"); - } - } - if (jsonRecord.containsKey(KEY_TTL)) { - // TTLs are never returned by the sync server, so should never be true if - // the record was fetched. - try { - record.ttl = jsonRecord.getLong(KEY_TTL); - } catch (Exception e) { - throw new RecordParseException("TTL could not be parsed"); - } - } - // TODO: deleted? - return record; - } - - public void setKeyBundle(KeyBundle bundle) { - this.keyBundle = bundle; - } - - public CryptoRecord decrypt() throws CryptoException, IOException, NonObjectJSONException { - if (keyBundle == null) { - throw new NoKeyBundleException(); - } - - // Check that payload contains all pieces for crypto. - if (!payload.containsKey(KEY_CIPHERTEXT) || - !payload.containsKey(KEY_IV) || - !payload.containsKey(KEY_HMAC)) { - throw new MissingCryptoInputException(); - } - - // There's no difference between handling the crypto/keys object and - // anything else; we just get this.keyBundle from a different source. - byte[] cleartext = decryptPayload(payload, keyBundle); - payload = ExtendedJSONObject.parseUTF8AsJSONObject(cleartext); - return this; - } - - public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException { - if (this.keyBundle == null) { - throw new NoKeyBundleException(); - } - String cleartext = payload.toJSONString(); - byte[] cleartextBytes = cleartext.getBytes("UTF-8"); - CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle); - String message = new String(Base64.encodeBase64(info.getMessage())); - String iv = new String(Base64.encodeBase64(info.getIV())); - String hmac = Utils.byte2Hex(info.getHMAC()); - ExtendedJSONObject ciphertext = new ExtendedJSONObject(); - ciphertext.put(KEY_CIPHERTEXT, message); - ciphertext.put(KEY_HMAC, hmac); - ciphertext.put(KEY_IV, iv); - this.payload = ciphertext; - return this; - } - - @Override - public void initFromEnvelope(CryptoRecord payload) { - throw new IllegalStateException("Can't do this with a CryptoRecord."); - } - - @Override - public CryptoRecord getEnvelope() { - throw new IllegalStateException("Can't do this with a CryptoRecord."); - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - throw new IllegalStateException("Can't do this with a CryptoRecord."); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - throw new IllegalStateException("Can't do this with a CryptoRecord."); - } - - // TODO: this only works with encrypted object, and has other limitations. - public JSONObject toJSONObject() { - ExtendedJSONObject o = new ExtendedJSONObject(); - o.put(KEY_PAYLOAD, payload.toJSONString()); - o.put(KEY_ID, this.guid); - if (this.ttl > 0) { - o.put(KEY_TTL, this.ttl); - } - return o.object; - } - - @Override - public String toJSONString() { - return toJSONObject().toJSONString(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java deleted file mode 100644 index ddcb5411c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java +++ /dev/null @@ -1,69 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.background.common.log.Logger; - -/** - * A little class to allow us to maintain a count of extant - * things (in our case, callbacks that need to fire), and - * some work that we want done when that count hits 0. - * - * @author rnewman - * - */ -public class DelayedWorkTracker { - private static final String LOG_TAG = "DelayedWorkTracker"; - protected Runnable workItem = null; - protected int outstandingCount = 0; - - public int incrementOutstanding() { - Logger.trace(LOG_TAG, "Incrementing outstanding."); - synchronized(this) { - return ++outstandingCount; - } - } - public int decrementOutstanding() { - Logger.trace(LOG_TAG, "Decrementing outstanding."); - Runnable job = null; - int count; - synchronized(this) { - if ((count = --outstandingCount) == 0 && - workItem != null) { - job = workItem; - workItem = null; - } else { - return count; - } - } - job.run(); - // In case it's changed. - return getOutstandingOperations(); - } - public int getOutstandingOperations() { - synchronized(this) { - return outstandingCount; - } - } - public void delayWorkItem(Runnable item) { - Logger.trace(LOG_TAG, "delayWorkItem."); - boolean runnableNow = false; - synchronized(this) { - Logger.trace(LOG_TAG, "outstandingCount: " + outstandingCount); - if (outstandingCount == 0) { - runnableNow = true; - } else { - if (workItem != null) { - throw new IllegalStateException("Work item already set!"); - } - workItem = item; - } - } - if (runnableNow) { - Logger.trace(LOG_TAG, "Running item now."); - item.run(); - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java deleted file mode 100644 index 035816088..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/EngineSettings.java +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class EngineSettings { - public final String syncID; - public final int version; - - public EngineSettings(final String syncID, final int version) { - this.syncID = syncID; - this.version = version; - } - - public EngineSettings(ExtendedJSONObject object) { - try { - this.syncID = object.getString("syncID"); - this.version = object.getIntegerSafely("version"); - } catch (Exception e ) { - throw new IllegalArgumentException(e); - } - } - - public ExtendedJSONObject toJSONObject() { - ExtendedJSONObject json = new ExtendedJSONObject(); - json.put("syncID", syncID); - json.put("version", version); - return json; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java deleted file mode 100644 index f5fac0009..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java +++ /dev/null @@ -1,426 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; - -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -/** - * Extend JSONObject to do little things, like, y'know, accessing members. - * - * @author rnewman - * - */ -public class ExtendedJSONObject { - - public JSONObject object; - - /** - * Return a <code>JSONParser</code> instance for immediate use. - * <p> - * <code>JSONParser</code> is not thread-safe, so we return a new instance - * each call. This is extremely inefficient in execution time and especially - * memory use -- each instance allocates a 16kb temporary buffer -- and we - * hope to improve matters eventually. - */ - protected static JSONParser getJSONParser() { - return new JSONParser(); - } - - /** - * Parse a JSON encoded string. - * - * @param in <code>Reader</code> over a JSON-encoded input to parse; not - * necessarily a JSON object. - * @return a regular Java <code>Object</code>. - * @throws ParseException - * @throws IOException - */ - protected static Object parseRaw(Reader in) throws ParseException, IOException { - try { - return getJSONParser().parse(in); - } catch (Error e) { - // Don't be stupid, org.json.simple. Bug 1042929. - throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e); - } - } - - /** - * Parse a JSON encoded string. - * <p> - * You should prefer the streaming interface {@link #parseRaw(Reader)}. - * - * @param input JSON-encoded input string to parse; not necessarily a JSON object. - * @return a regular Java <code>Object</code>. - * @throws ParseException - */ - protected static Object parseRaw(String input) throws ParseException { - try { - return getJSONParser().parse(input); - } catch (Error e) { - // Don't be stupid, org.json.simple. Bug 1042929. - throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e); - } - } - - /** - * Helper method to get a JSON array from a stream. - * - * @param in <code>Reader</code> over a JSON-encoded array to parse. - * @throws ParseException - * @throws IOException - * @throws NonArrayJSONException if the object is valid JSON, but not an array. - */ - public static JSONArray parseJSONArray(Reader in) - throws IOException, ParseException, NonArrayJSONException { - Object o = parseRaw(in); - - if (o == null) { - return null; - } - - if (o instanceof JSONArray) { - return (JSONArray) o; - } - - throw new NonArrayJSONException("value must be a JSON array"); - } - - /** - * Helper method to get a JSON array from a string. - * <p> - * You should prefer the stream interface {@link #parseJSONArray(Reader)}. - * - * @param jsonString input. - * @throws IOException - * @throws NonArrayJSONException if the object is invalid JSON or not an array. - */ - public static JSONArray parseJSONArray(String jsonString) - throws IOException, NonArrayJSONException { - Object o = null; - try { - o = parseRaw(jsonString); - } catch (ParseException e) { - throw new NonArrayJSONException(e); - } - - if (o == null) { - return null; - } - - if (o instanceof JSONArray) { - return (JSONArray) o; - } - - throw new NonArrayJSONException("value must be a JSON array"); - } - - /** - * Helper method to get a JSON object from a UTF-8 byte array. - * - * @param in UTF-8 bytes. - * @throws NonObjectJSONException if the object is not valid JSON or not an object. - * @throws IOException - */ - public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in) - throws NonObjectJSONException, IOException { - return new ExtendedJSONObject(new String(in, "UTF-8")); - } - - public ExtendedJSONObject() { - this.object = new JSONObject(); - } - - public ExtendedJSONObject(JSONObject o) { - this.object = o; - } - - public ExtendedJSONObject(Reader in) throws IOException, NonObjectJSONException { - if (in == null) { - this.object = new JSONObject(); - return; - } - - Object obj = null; - try { - obj = parseRaw(in); - } catch (ParseException e) { - throw new NonObjectJSONException(e); - } - - if (obj instanceof JSONObject) { - this.object = ((JSONObject) obj); - } else { - throw new NonObjectJSONException("value must be a JSON object"); - } - } - - public ExtendedJSONObject(String jsonString) throws IOException, NonObjectJSONException { - this(jsonString == null ? null : new StringReader(jsonString)); - } - - @Override - public ExtendedJSONObject clone() { - return new ExtendedJSONObject((JSONObject) this.object.clone()); - } - - // Passthrough methods. - public Object get(String key) { - return this.object.get(key); - } - - public long getLong(String key, long def) { - if (!object.containsKey(key)) { - return def; - } - - Long val = getLong(key); - if (val == null) { - return def; - } - return val.longValue(); - } - - public Long getLong(String key) { - return (Long) this.get(key); - } - - public String getString(String key) { - return (String) this.get(key); - } - - public Boolean getBoolean(String key) { - return (Boolean) this.get(key); - } - - /** - * Return an Integer if the value for this key is an Integer, Long, or String - * that can be parsed as a base 10 Integer. - * Passes through null. - * - * @throws NumberFormatException - */ - public Integer getIntegerSafely(String key) throws NumberFormatException { - Object val = this.object.get(key); - if (val == null) { - return null; - } - if (val instanceof Integer) { - return (Integer) val; - } - if (val instanceof Long) { - return ((Long) val).intValue(); - } - if (val instanceof String) { - return Integer.parseInt((String) val, 10); - } - throw new NumberFormatException("Expecting Integer, got " + val.getClass()); - } - - /** - * Return a server timestamp value as milliseconds since epoch. - * - * @param key - * @return A Long, or null if the value is non-numeric or doesn't exist. - */ - public Long getTimestamp(String key) { - Object val = this.object.get(key); - - // This is absurd. - if (val instanceof Double) { - double millis = ((Double) val) * 1000; - return Double.valueOf(millis).longValue(); - } - if (val instanceof Float) { - double millis = ((Float) val).doubleValue() * 1000; - return Double.valueOf(millis).longValue(); - } - if (val instanceof Number) { - // Must be an integral number. - return ((Number) val).longValue() * 1000; - } - - return null; - } - - public boolean containsKey(String key) { - return this.object.containsKey(key); - } - - public String toJSONString() { - return this.object.toJSONString(); - } - - @Override - public String toString() { - return this.object.toString(); - } - - protected void putRaw(String key, Object value) { - @SuppressWarnings("unchecked") - Map<Object, Object> map = this.object; - map.put(key, value); - } - - public void put(String key, String value) { - this.putRaw(key, value); - } - - public void put(String key, boolean value) { - this.putRaw(key, value); - } - - public void put(String key, long value) { - this.putRaw(key, value); - } - - public void put(String key, int value) { - this.putRaw(key, value); - } - - public void put(String key, ExtendedJSONObject value) { - this.putRaw(key, value); - } - - public void put(String key, JSONArray value) { - this.putRaw(key, value); - } - - @SuppressWarnings("unchecked") - public void putArray(String key, List<String> value) { - // Frustratingly inefficient, but there you have it. - final JSONArray jsonArray = new JSONArray(); - jsonArray.addAll(value); - this.putRaw(key, jsonArray); - } - - /** - * Remove key-value pair from JSONObject. - * - * @param key - * to be removed. - * @return true if key exists and was removed, false otherwise. - */ - public boolean remove(String key) { - Object res = this.object.remove(key); - return (res != null); - } - - public ExtendedJSONObject getObject(String key) throws NonObjectJSONException { - Object o = this.object.get(key); - if (o == null) { - return null; - } - if (o instanceof ExtendedJSONObject) { - return (ExtendedJSONObject) o; - } - if (o instanceof JSONObject) { - return new ExtendedJSONObject((JSONObject) o); - } - throw new NonObjectJSONException("value must be a JSON object for key: " + key); - } - - @SuppressWarnings("unchecked") - public Set<Entry<String, Object>> entrySet() { - return this.object.entrySet(); - } - - @SuppressWarnings("unchecked") - public Set<String> keySet() { - return this.object.keySet(); - } - - public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException { - Object o = this.object.get(key); - if (o == null) { - return null; - } - if (o instanceof JSONArray) { - return (JSONArray) o; - } - throw new NonArrayJSONException("key must be a JSON array: " + key); - } - - public int size() { - return this.object.size(); - } - - @Override - public int hashCode() { - if (this.object == null) { - return getClass().hashCode(); - } - return this.object.hashCode() ^ getClass().hashCode(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ExtendedJSONObject)) { - return false; - } - if (o == this) { - return true; - } - ExtendedJSONObject other = (ExtendedJSONObject) o; - if (this.object == null) { - return other.object == null; - } - return this.object.equals(other.object); - } - - /** - * Throw if keys are missing or values have wrong types. - * - * @param requiredFields list of required keys. - * @param requiredFieldClass class that values must be coercable to; may be null, which means don't check. - * @throws UnexpectedJSONException - */ - public void throwIfFieldsMissingOrMisTyped(String[] requiredFields, Class<?> requiredFieldClass) throws BadRequiredFieldJSONException { - // Defensive as possible: verify object has expected key(s) with string value. - for (String k : requiredFields) { - Object value = get(k); - if (value == null) { - throw new BadRequiredFieldJSONException("Expected key not present in result: " + k); - } - if (requiredFieldClass != null && !(requiredFieldClass.isInstance(value))) { - throw new BadRequiredFieldJSONException("Value for key not an instance of " + requiredFieldClass + ": " + k); - } - } - } - - /** - * Return a base64-encoded string value as a byte array. - */ - public byte[] getByteArrayBase64(String key) { - String s = (String) this.object.get(key); - if (s == null) { - return null; - } - return Base64.decodeBase64(s); - } - - /** - * Return a hex-encoded string value as a byte array. - */ - public byte[] getByteArrayHex(String key) { - String s = (String) this.object.get(key); - if (s == null) { - return null; - } - return Utils.hex2Byte(s); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java deleted file mode 100644 index e28bbe4cc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java +++ /dev/null @@ -1,1167 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.Context; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.crypto.CryptoException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.delegates.FreshStartDelegate; -import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; -import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; -import org.mozilla.gecko.sync.delegates.KeyUploadDelegate; -import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; -import org.mozilla.gecko.sync.delegates.WipeServerDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.HttpResponseObserver; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage; -import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage; -import org.mozilla.gecko.sync.stage.CheckPreconditionsStage; -import org.mozilla.gecko.sync.stage.CompletedStage; -import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage; -import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage; -import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage; -import org.mozilla.gecko.sync.stage.FetchInfoConfigurationStage; -import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage; -import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage; -import org.mozilla.gecko.sync.stage.GlobalSyncStage; -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; -import org.mozilla.gecko.sync.stage.NoSuchStageException; -import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage; -import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; -import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; - -public class GlobalSession implements HttpResponseObserver { - private static final String LOG_TAG = "GlobalSession"; - - public static final long STORAGE_VERSION = 5; - - public SyncConfiguration config = null; - - protected Map<Stage, GlobalSyncStage> stages; - public Stage currentState = Stage.idle; - - public final GlobalSessionCallback callback; - protected final Context context; - protected final ClientsDataDelegate clientsDelegate; - - /** - * Map from engine name to new settings for an updated meta/global record. - * Engines to remove will have <code>null</code> EngineSettings. - */ - public final Map<String, EngineSettings> enginesToUpdate = new HashMap<String, EngineSettings>(); - - /* - * Key accessors. - */ - public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException { - return config.getCollectionKeys().keyBundleForCollection(collection); - } - - /* - * Config passthrough for convenience. - */ - public AuthHeaderProvider getAuthHeaderProvider() { - return config.getAuthHeaderProvider(); - } - - public URI wboURI(String collection, String id) throws URISyntaxException { - return config.wboURI(collection, id); - } - - public GlobalSession(SyncConfiguration config, - GlobalSessionCallback callback, - Context context, - ClientsDataDelegate clientsDelegate) - throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { - - if (callback == null) { - throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor."); - } - - this.callback = callback; - this.context = context; - this.clientsDelegate = clientsDelegate; - - this.config = config; - registerCommands(); - prepareStages(); - - if (config.stagesToSync == null) { - Logger.info(LOG_TAG, "No stages to sync specified; defaulting to all valid engine names."); - config.stagesToSync = Collections.unmodifiableCollection(SyncConfiguration.validEngineNames()); - } - - // TODO: data-driven plan for the sync, referring to prepareStages. - } - - /** - * Register commands this global session knows how to process. - * <p> - * Re-registering a command overwrites any existing registration. - */ - protected static void registerCommands() { - final CommandProcessor processor = CommandProcessor.getProcessor(); - - processor.registerCommand("resetEngine", new CommandRunner(1) { - @Override - public void executeCommand(final GlobalSession session, List<String> args) { - HashSet<String> names = new HashSet<String>(); - names.add(args.get(0)); - session.resetStagesByName(names); - } - }); - - processor.registerCommand("resetAll", new CommandRunner(0) { - @Override - public void executeCommand(final GlobalSession session, List<String> args) { - session.resetAllStages(); - } - }); - - processor.registerCommand("wipeEngine", new CommandRunner(1) { - @Override - public void executeCommand(final GlobalSession session, List<String> args) { - HashSet<String> names = new HashSet<String>(); - names.add(args.get(0)); - session.wipeStagesByName(names); - } - }); - - processor.registerCommand("wipeAll", new CommandRunner(0) { - @Override - public void executeCommand(final GlobalSession session, List<String> args) { - session.wipeAllStages(); - } - }); - - processor.registerCommand("displayURI", new CommandRunner(3) { - @Override - public void executeCommand(final GlobalSession session, List<String> args) { - CommandProcessor.displayURI(args, session.getContext()); - } - }); - } - - protected void prepareStages() { - Map<Stage, GlobalSyncStage> stages = new EnumMap<Stage, GlobalSyncStage>(Stage.class); - - stages.put(Stage.checkPreconditions, new CheckPreconditionsStage()); - stages.put(Stage.fetchInfoCollections, new FetchInfoCollectionsStage()); - stages.put(Stage.fetchMetaGlobal, new FetchMetaGlobalStage()); - stages.put(Stage.fetchInfoConfiguration, new FetchInfoConfigurationStage( - config.infoConfigurationURL(), getAuthHeaderProvider())); - stages.put(Stage.ensureKeysStage, new EnsureCrypto5KeysStage()); - - stages.put(Stage.syncClientsEngine, new SyncClientsEngineStage()); - - stages.put(Stage.syncTabs, new FennecTabsServerSyncStage()); - stages.put(Stage.syncPasswords, new PasswordsServerSyncStage()); - stages.put(Stage.syncBookmarks, new AndroidBrowserBookmarksServerSyncStage()); - stages.put(Stage.syncHistory, new AndroidBrowserHistoryServerSyncStage()); - stages.put(Stage.syncFormHistory, new FormHistoryServerSyncStage()); - - stages.put(Stage.uploadMetaGlobal, new UploadMetaGlobalStage()); - stages.put(Stage.completed, new CompletedStage()); - - this.stages = Collections.unmodifiableMap(stages); - } - - public GlobalSyncStage getSyncStageByName(String name) throws NoSuchStageException { - return getSyncStageByName(Stage.byName(name)); - } - - public GlobalSyncStage getSyncStageByName(Stage next) throws NoSuchStageException { - GlobalSyncStage stage = stages.get(next); - if (stage == null) { - throw new NoSuchStageException(next); - } - return stage; - } - - public Collection<GlobalSyncStage> getSyncStagesByEnum(Collection<Stage> enums) { - ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>(); - for (Stage name : enums) { - try { - GlobalSyncStage stage = this.getSyncStageByName(name); - out.add(stage); - } catch (NoSuchStageException e) { - Logger.warn(LOG_TAG, "Unable to find stage with name " + name); - } - } - return out; - } - - public Collection<GlobalSyncStage> getSyncStagesByName(Collection<String> names) { - ArrayList<GlobalSyncStage> out = new ArrayList<GlobalSyncStage>(); - for (String name : names) { - try { - GlobalSyncStage stage = this.getSyncStageByName(name); - out.add(stage); - } catch (NoSuchStageException e) { - Logger.warn(LOG_TAG, "Unable to find stage with name " + name); - } - } - return out; - } - - /** - * Advance and loop around the stages of a sync. - * @param current - * @return - * The next stage to execute. - */ - public static Stage nextStage(Stage current) { - int index = current.ordinal() + 1; - int max = Stage.completed.ordinal() + 1; - return Stage.values()[index % max]; - } - - /** - * Move to the next stage in the syncing process. - */ - public void advance() { - // If we have a backoff, request a backoff and don't advance to next stage. - long existingBackoff = largestBackoffObserved.get(); - if (existingBackoff > 0) { - this.abort(null, "Aborting sync because of backoff of " + existingBackoff + " milliseconds."); - return; - } - - this.callback.handleStageCompleted(this.currentState, this); - Stage next = nextStage(this.currentState); - GlobalSyncStage nextStage; - try { - nextStage = this.getSyncStageByName(next); - } catch (NoSuchStageException e) { - this.abort(e, "No such stage " + next); - return; - } - this.currentState = next; - Logger.info(LOG_TAG, "Running next stage " + next + " (" + nextStage + ")..."); - try { - nextStage.execute(this); - } catch (Exception ex) { - Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next); - this.abort(ex, "Uncaught exception in stage."); - return; - } - } - - public Context getContext() { - return this.context; - } - - /** - * Begin a sync. - * <p> - * The caller is responsible for: - * <ul> - * <li>Verifying that any backoffs/minimum next sync requests are respected.</li> - * <li>Ensuring that the device is online.</li> - * <li>Ensuring that dependencies are ready.</li> - * </ul> - * - * @throws AlreadySyncingException - */ - public void start() throws AlreadySyncingException { - if (this.currentState != GlobalSyncStage.Stage.idle) { - throw new AlreadySyncingException(this.currentState); - } - installAsHttpResponseObserver(); // Uninstalled by completeSync or abort. - this.advance(); - } - - /** - * Stop this sync and start again. - * @throws AlreadySyncingException - */ - protected void restart() throws AlreadySyncingException { - this.currentState = GlobalSyncStage.Stage.idle; - if (callback.shouldBackOffStorage()) { - this.callback.handleAborted(this, "Told to back off."); - return; - } - this.start(); - } - - /** - * We're finished (aborted or succeeded): release resources. - */ - protected void cleanUp() { - uninstallAsHttpResponseObserver(); - this.stages = null; - } - - public void completeSync() { - cleanUp(); - this.currentState = GlobalSyncStage.Stage.idle; - this.callback.handleSuccess(this); - } - - /** - * Record that an updated meta/global record should be uploaded with the given - * settings for the given engine. - * - * @param engineName engine to update. - * @param engineSettings new syncID and version. - */ - public void recordForMetaGlobalUpdate(String engineName, EngineSettings engineSettings) { - enginesToUpdate.put(engineName, engineSettings); - } - - /** - * Record that an updated meta/global record should be uploaded without the - * given engine name. - * - * @param engineName - * engine to remove. - */ - public void removeEngineFromMetaGlobal(String engineName) { - enginesToUpdate.put(engineName, null); - } - - public boolean hasUpdatedMetaGlobal() { - if (enginesToUpdate.isEmpty()) { - Logger.info(LOG_TAG, "Not uploading updated meta/global record since there are no engines requesting upload."); - return false; - } - - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Uploading updated meta/global record since there are engine changes to meta/global."); - Logger.trace(LOG_TAG, "Engines requesting update [" + Utils.toCommaSeparatedString(enginesToUpdate.keySet()) + "]"); - } - - return true; - } - - public void updateMetaGlobalInPlace() { - config.metaGlobal.declined = this.declinedEngineNames(); - ExtendedJSONObject engines = config.metaGlobal.getEngines(); - for (Entry<String, EngineSettings> pair : enginesToUpdate.entrySet()) { - if (pair.getValue() == null) { - engines.remove(pair.getKey()); - } else { - engines.put(pair.getKey(), pair.getValue().toJSONObject()); - } - } - - enginesToUpdate.clear(); - } - - /** - * Synchronously upload an updated meta/global. - * <p> - * All problems are logged and ignored. - */ - public void uploadUpdatedMetaGlobal() { - updateMetaGlobalInPlace(); - - Logger.debug(LOG_TAG, "Uploading updated meta/global record."); - final Object monitor = new Object(); - - Runnable doUpload = new Runnable() { - @Override - public void run() { - config.metaGlobal.upload(new MetaGlobalDelegate() { - @Override - public void handleSuccess(MetaGlobal global, SyncStorageResponse response) { - Logger.info(LOG_TAG, "Successfully uploaded updated meta/global record."); - // Engine changes are stored as diffs, so update enabled engines in config to match uploaded meta/global. - config.enabledEngineNames = config.metaGlobal.getEnabledEngineNames(); - // Clear userSelectedEngines because they are updated in config and meta/global. - config.userSelectedEngines = null; - - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void handleMissing(MetaGlobal global, SyncStorageResponse response) { - Logger.warn(LOG_TAG, "Got 404 missing uploading updated meta/global record; shouldn't happen. Ignoring."); - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void handleFailure(SyncStorageResponse response) { - Logger.warn(LOG_TAG, "Failed to upload updated meta/global record; ignoring."); - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void handleError(Exception e) { - Logger.warn(LOG_TAG, "Got exception trying to upload updated meta/global record; ignoring.", e); - synchronized (monitor) { - monitor.notify(); - } - } - }); - } - }; - - final Thread upload = new Thread(doUpload); - synchronized (monitor) { - try { - upload.start(); - monitor.wait(); - Logger.debug(LOG_TAG, "Uploaded updated meta/global record."); - } catch (InterruptedException e) { - Logger.error(LOG_TAG, "Uploading updated meta/global interrupted; continuing."); - } - } - } - - - public void abort(Exception e, String reason) { - Logger.warn(LOG_TAG, "Aborting sync: " + reason, e); - cleanUp(); - long existingBackoff = largestBackoffObserved.get(); - if (existingBackoff > 0) { - callback.requestBackoff(existingBackoff); - } - if (!(e instanceof HTTPFailureException)) { - // e is null, or we aborted for a non-HTTP reason; okay to upload new meta/global record. - if (this.hasUpdatedMetaGlobal()) { - this.uploadUpdatedMetaGlobal(); // Only logs errors; does not call abort. - } - } - this.callback.handleError(this, e); - } - - public void handleHTTPError(SyncStorageResponse response, String reason) { - // TODO: handling of 50x (backoff), 401 (node reassignment or auth error). - // Fall back to aborting. - Logger.warn(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode()); - this.interpretHTTPFailure(response.httpResponse()); - this.abort(new HTTPFailureException(response), reason); - } - - /** - * Perform appropriate backoff etc. extraction. - */ - public void interpretHTTPFailure(HttpResponse response) { - // TODO: handle permanent rejection. - long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); - if (responseBackoff > 0) { - callback.requestBackoff(responseBackoff); - } - - if (response.getStatusLine() != null) { - final int statusCode = response.getStatusLine().getStatusCode(); - switch(statusCode) { - - case 400: - SyncStorageResponse storageResponse = new SyncStorageResponse(response); - this.interpretHTTPBadRequestBody(storageResponse); - break; - - case 401: - /* - * Alert our callback we have a 401 on a cluster URL. This GlobalSession - * will fail, but the next one will fetch a new cluster URL and will - * distinguish between "node reassignment" and "user password changed". - */ - callback.informUnauthorizedResponse(this, config.getClusterURL()); - break; - } - } - } - - protected void interpretHTTPBadRequestBody(final SyncStorageResponse storageResponse) { - try { - final String body = storageResponse.body(); - if (body == null) { - return; - } - if (SyncStorageResponse.RESPONSE_CLIENT_UPGRADE_REQUIRED.equals(body)) { - callback.informUpgradeRequiredResponse(this); - return; - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception parsing HTTP 400 body.", e); - } - } - - public void fetchInfoCollections(JSONRecordFetchDelegate callback) throws URISyntaxException { - final JSONRecordFetcher fetcher = new JSONRecordFetcher(config.infoCollectionsURL(), getAuthHeaderProvider()); - fetcher.fetch(callback); - } - - /** - * Upload new crypto/keys. - * - * @param keys - * new keys. - * @param keyUploadDelegate - * a delegate. - */ - public void uploadKeys(final CollectionKeys keys, - final KeyUploadDelegate keyUploadDelegate) { - SyncStorageRecordRequest request; - try { - request = new SyncStorageRecordRequest(this.config.keysURI()); - } catch (URISyntaxException e) { - keyUploadDelegate.onKeyUploadFailed(e); - return; - } - - request.delegate = new SyncStorageRequestDelegate() { - - @Override - public String ifUnmodifiedSince() { - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - Logger.debug(LOG_TAG, "Keys uploaded."); - BaseResource.consumeEntity(response); // We don't need the response at all. - keyUploadDelegate.onKeysUploaded(); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - Logger.debug(LOG_TAG, "Failed to upload keys."); - GlobalSession.this.interpretHTTPFailure(response.httpResponse()); - BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response. - keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(Exception ex) { - Logger.warn(LOG_TAG, "Got exception trying to upload keys", ex); - keyUploadDelegate.onKeyUploadFailed(ex); - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return GlobalSession.this.getAuthHeaderProvider(); - } - }; - - // Convert keys to an encrypted crypto record. - CryptoRecord keysRecord; - try { - keysRecord = keys.asCryptoRecord(); - keysRecord.setKeyBundle(config.syncKeyBundle); - keysRecord.encrypt(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception trying creating crypto record from keys", e); - keyUploadDelegate.onKeyUploadFailed(e); - return; - } - - request.put(keysRecord); - } - - /* - * meta/global callbacks. - */ - public void processMetaGlobal(MetaGlobal global) { - config.metaGlobal = global; - - Long storageVersion = global.getStorageVersion(); - if (storageVersion == null) { - Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote storage version."); - freshStart(); - return; - } - if (storageVersion < STORAGE_VERSION) { - Logger.warn(LOG_TAG, "Outdated server: reported " + - "remote storage version " + storageVersion + " < " + - "local storage version " + STORAGE_VERSION); - freshStart(); - return; - } - if (storageVersion > STORAGE_VERSION) { - Logger.warn(LOG_TAG, "Outdated client: reported " + - "remote storage version " + storageVersion + " > " + - "local storage version " + STORAGE_VERSION); - requiresUpgrade(); - return; - } - String remoteSyncID = global.getSyncID(); - if (remoteSyncID == null) { - Logger.warn(LOG_TAG, "Malformed remote meta/global: could not retrieve remote syncID."); - freshStart(); - return; - } - String localSyncID = config.syncID; - if (!remoteSyncID.equals(localSyncID)) { - Logger.warn(LOG_TAG, "Remote syncID different from local syncID: resetting client and assuming remote syncID."); - resetAllStages(); - config.purgeCryptoKeys(); - config.syncID = remoteSyncID; - } - // Compare lastModified timestamps for remote/local engine selection times. - Logger.debug(LOG_TAG, "Comparing local engine selection timestamp [" + config.userSelectedEnginesTimestamp + "] to server meta/global timestamp [" + config.persistedMetaGlobal().lastModified() + "]."); - if (config.userSelectedEnginesTimestamp < config.persistedMetaGlobal().lastModified()) { - // Remote has later meta/global timestamp. Don't upload engine changes. - config.userSelectedEngines = null; - } - // Persist enabled engine names. - config.enabledEngineNames = global.getEnabledEngineNames(); - if (config.enabledEngineNames == null) { - Logger.warn(LOG_TAG, "meta/global reported no enabled engine names!"); - } else { - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Persisting enabled engine names '" + - Utils.toCommaSeparatedString(config.enabledEngineNames) + "' from meta/global."); - } - } - - // Persist declined. - // Our declined engines at any point are: - // Whatever they were remotely, plus whatever they were locally, less any - // engines that were just enabled locally or remotely. - // If remote just 'won', our recently enabled list just got cleared. - final HashSet<String> allDeclined = new HashSet<String>(); - - final Set<String> newRemoteDeclined = global.getDeclinedEngineNames(); - final Set<String> oldLocalDeclined = config.declinedEngineNames; - - allDeclined.addAll(newRemoteDeclined); - allDeclined.addAll(oldLocalDeclined); - - if (config.userSelectedEngines != null) { - for (Entry<String, Boolean> selection : config.userSelectedEngines.entrySet()) { - if (selection.getValue()) { - allDeclined.remove(selection.getKey()); - } - } - } - - config.declinedEngineNames = allDeclined; - if (config.declinedEngineNames.isEmpty()) { - Logger.debug(LOG_TAG, "meta/global reported no declined engine names, and we have none declined locally."); - } else { - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Persisting declined engine names '" + - Utils.toCommaSeparatedString(config.declinedEngineNames) + "' from meta/global."); - } - } - - config.persistToPrefs(); - advance(); - } - - public void processMissingMetaGlobal(MetaGlobal global) { - freshStart(); - } - - /** - * Do a fresh start then quietly finish the sync, starting another. - */ - public void freshStart() { - final GlobalSession globalSession = this; - freshStart(this, new FreshStartDelegate() { - - @Override - public void onFreshStartFailed(Exception e) { - globalSession.abort(e, "Fresh start failed."); - } - - @Override - public void onFreshStart() { - try { - Logger.warn(LOG_TAG, "Fresh start succeeded; restarting global session."); - globalSession.config.persistToPrefs(); - globalSession.restart(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception when restarting sync after freshStart.", e); - globalSession.abort(e, "Got exception after freshStart."); - } - } - }); - } - - /** - * Clean the server, aborting the current sync. - * <p> - * <ol> - * <li>Wipe the server storage.</li> - * <li>Reset all stages and purge cached state: (meta/global and crypto/keys records).</li> - * <li>Upload fresh meta/global record.</li> - * <li>Upload fresh crypto/keys record.</li> - * <li>Restart the sync entirely in order to re-download meta/global and crypto/keys record.</li> - * </ol> - * @param session the current session. - * @param freshStartDelegate delegate to notify on fresh start or failure. - */ - protected static void freshStart(final GlobalSession session, final FreshStartDelegate freshStartDelegate) { - Logger.debug(LOG_TAG, "Fresh starting."); - - final MetaGlobal mg = session.generateNewMetaGlobal(); - - session.wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() { - - @Override - public void onWiped(long timestamp) { - Logger.debug(LOG_TAG, "Successfully wiped server. Resetting all stages and purging cached meta/global and crypto/keys records."); - - session.resetAllStages(); - session.config.purgeMetaGlobal(); - session.config.purgeCryptoKeys(); - session.config.persistToPrefs(); - - Logger.info(LOG_TAG, "Uploading new meta/global with sync ID " + mg.syncID + "."); - - // It would be good to set the X-If-Unmodified-Since header to `timestamp` - // for this PUT to ensure at least some level of transactionality. - // Unfortunately, the servers don't support it after a wipe right now - // (bug 693893), so we're going to defer this until bug 692700. - mg.upload(new MetaGlobalDelegate() { - @Override - public void handleSuccess(MetaGlobal uploadedGlobal, SyncStorageResponse uploadResponse) { - Logger.info(LOG_TAG, "Uploaded new meta/global with sync ID " + uploadedGlobal.syncID + "."); - - // Generate new keys. - CollectionKeys keys = null; - try { - keys = session.generateNewCryptoKeys(); - } catch (CryptoException e) { - Logger.warn(LOG_TAG, "Got exception generating new keys; failing fresh start.", e); - freshStartDelegate.onFreshStartFailed(e); - } - if (keys == null) { - Logger.warn(LOG_TAG, "Got null keys from generateNewKeys; failing fresh start."); - freshStartDelegate.onFreshStartFailed(null); - } - - // Upload new keys. - Logger.info(LOG_TAG, "Uploading new crypto/keys."); - session.uploadKeys(keys, new KeyUploadDelegate() { - @Override - public void onKeysUploaded() { - Logger.info(LOG_TAG, "Uploaded new crypto/keys."); - freshStartDelegate.onFreshStart(); - } - - @Override - public void onKeyUploadFailed(Exception e) { - Logger.warn(LOG_TAG, "Got exception uploading new keys.", e); - freshStartDelegate.onFreshStartFailed(e); - } - }); - } - - @Override - public void handleMissing(MetaGlobal global, SyncStorageResponse response) { - // Shouldn't happen on upload. - Logger.warn(LOG_TAG, "Got 'missing' response uploading new meta/global."); - freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing while uploading.")); - } - - @Override - public void handleFailure(SyncStorageResponse response) { - Logger.warn(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global."); - session.interpretHTTPFailure(response.httpResponse()); - freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response)); - } - - @Override - public void handleError(Exception e) { - Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e); - freshStartDelegate.onFreshStartFailed(e); - } - }); - } - - @Override - public void onWipeFailed(Exception e) { - Logger.warn(LOG_TAG, "Wipe failed."); - freshStartDelegate.onFreshStartFailed(e); - } - }); - } - - // Note that we do not yet implement wipeRemote: it's only necessary for - // first sync options. - // -- reset local stages, wipe server for each stage *except* clients - // (stages only, not whole server!), send wipeEngine commands to each client. - // - // Similarly for startOver (because we don't receive that notification). - // -- remove client data from server, reset local stages, clear keys, reset - // backoff, clear all prefs, discard credentials. - // - // Change passphrase: wipe entire server, reset client to force upload, sync. - // - // When an engine is disabled: wipe its collections on the server, reupload - // meta/global. - // - // On syncing each stage: if server has engine version 0 or old, wipe server, - // reset client to prompt reupload. - // If sync ID mismatch: take that syncID and reset client. - - protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) { - SyncStorageRequest request; - final GlobalSession self = this; - - try { - request = new SyncStorageRequest(config.storageURL()); - } catch (URISyntaxException ex) { - Logger.warn(LOG_TAG, "Invalid URI in wipeServer."); - wipeDelegate.onWipeFailed(ex); - return; - } - - request.delegate = new SyncStorageRequestDelegate() { - - @Override - public String ifUnmodifiedSince() { - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - BaseResource.consumeEntity(response); - wipeDelegate.onWiped(response.normalizedWeaveTimestamp()); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer."); - // Process HTTP failures here to pick up backoffs, etc. - self.interpretHTTPFailure(response.httpResponse()); - BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response. - wipeDelegate.onWipeFailed(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(Exception ex) { - Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex); - wipeDelegate.onWipeFailed(ex); - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return GlobalSession.this.getAuthHeaderProvider(); - } - }; - request.delete(); - } - - public void wipeAllStages() { - Logger.info(LOG_TAG, "Wiping all stages."); - // Includes "clients". - this.wipeStagesByEnum(Stage.getNamedStages()); - } - - public void wipeStages(Collection<GlobalSyncStage> stages) { - for (GlobalSyncStage stage : stages) { - try { - Logger.info(LOG_TAG, "Wiping " + stage); - stage.wipeLocal(this); - } catch (Exception e) { - Logger.error(LOG_TAG, "Ignoring wipe failure for stage " + stage, e); - } - } - } - - public void wipeStagesByEnum(Collection<Stage> stages) { - wipeStages(this.getSyncStagesByEnum(stages)); - } - - public void wipeStagesByName(Collection<String> names) { - wipeStages(this.getSyncStagesByName(names)); - } - - public void resetAllStages() { - Logger.info(LOG_TAG, "Resetting all stages."); - // Includes "clients". - this.resetStagesByEnum(Stage.getNamedStages()); - } - - public void resetStages(Collection<GlobalSyncStage> stages) { - for (GlobalSyncStage stage : stages) { - try { - Logger.info(LOG_TAG, "Resetting " + stage); - stage.resetLocal(this); - } catch (Exception e) { - Logger.error(LOG_TAG, "Ignoring reset failure for stage " + stage, e); - } - } - } - - public void resetStagesByEnum(Collection<Stage> stages) { - resetStages(this.getSyncStagesByEnum(stages)); - } - - public void resetStagesByName(Collection<String> names) { - resetStages(this.getSyncStagesByName(names)); - } - - /** - * Engines to explicitly mark as declined in a fresh meta/global record. - * <p> - * Returns an empty array if the user hasn't elected to customize data types, - * or an array of engines that the user un-checked during customization. - * <p> - * Engines that Android Sync doesn't recognize are <b>not</b> included in - * the returned array. - * - * @return a new JSONArray of engine names. - */ - @SuppressWarnings("unchecked") - protected JSONArray declinedEngineNames() { - final JSONArray declined = new JSONArray(); - for (String engine : config.declinedEngineNames) { - declined.add(engine); - }; - - return declined; - } - - /** - * Engines to include in a fresh meta/global record. - * <p> - * Returns either the persisted engine names (perhaps we have been node - * re-assigned and are initializing a clean server: we want to upload the - * persisted engine names so that we don't accidentally disable engines that - * Android Sync doesn't recognize), or the set of engines names that Android - * Sync implements. - * - * @return set of engine names. - */ - protected Set<String> enabledEngineNames() { - if (config.enabledEngineNames != null) { - return config.enabledEngineNames; - } - - // These are the default set of engine names. - Set<String> validEngineNames = SyncConfiguration.validEngineNames(); - - // If the user hasn't set any selected engines, that's okay -- default to - // everything. - if (config.userSelectedEngines == null) { - return validEngineNames; - } - - // userSelectedEngines has keys that are engine names, and boolean values - // corresponding to whether the user asked for the engine to sync or not. If - // an engine is not present, that means the user didn't change its sync - // setting. Since we default to everything on, that means the user didn't - // turn it off; therefore, it's included in the set of engines to sync. - Set<String> validAndSelectedEngineNames = new HashSet<String>(); - for (String engineName : validEngineNames) { - if (config.userSelectedEngines.containsKey(engineName) && - !config.userSelectedEngines.get(engineName)) { - continue; - } - validAndSelectedEngineNames.add(engineName); - } - return validAndSelectedEngineNames; - } - - /** - * Generate fresh crypto/keys collection. - * @return crypto/keys collection. - * @throws CryptoException - */ - @SuppressWarnings("static-method") - public CollectionKeys generateNewCryptoKeys() throws CryptoException { - return CollectionKeys.generateCollectionKeys(); - } - - /** - * Generate a fresh meta/global record. - * @return meta/global record. - */ - public MetaGlobal generateNewMetaGlobal() { - final String newSyncID = Utils.generateGuid(); - final String metaURL = this.config.metaURL(); - - ExtendedJSONObject engines = new ExtendedJSONObject(); - for (String engineName : enabledEngineNames()) { - EngineSettings engineSettings = null; - try { - GlobalSyncStage globalStage = this.getSyncStageByName(engineName); - Integer version = globalStage.getStorageVersion(); - if (version == null) { - continue; // Don't want this stage to be included in meta/global. - } - engineSettings = new EngineSettings(Utils.generateGuid(), version); - } catch (NoSuchStageException e) { - // No trouble; Android Sync might not recognize this engine yet. - // By default, version 0. Other clients will see the 0 version and reset/wipe accordingly. - engineSettings = new EngineSettings(Utils.generateGuid(), 0); - } - engines.put(engineName, engineSettings.toJSONObject()); - } - - MetaGlobal metaGlobal = new MetaGlobal(metaURL, this.getAuthHeaderProvider()); - metaGlobal.setSyncID(newSyncID); - metaGlobal.setStorageVersion(STORAGE_VERSION); - metaGlobal.setEngines(engines); - - // We assume that the config's declined engines have been updated - // according to the user's selections. - metaGlobal.setDeclinedEngineNames(this.declinedEngineNames()); - - return metaGlobal; - } - - /** - * Suggest that your Sync client needs to be upgraded to work - * with this server. - */ - public void requiresUpgrade() { - Logger.info(LOG_TAG, "Client outdated storage version; requires update."); - // TODO: notify UI. - this.abort(null, "Requires upgrade"); - } - - /** - * If meta/global is missing or malformed, throws a MetaGlobalException. - * Otherwise, returns true if there is an entry for this engine in the - * meta/global "engines" object. - * <p> - * This is a global/permanent setting, not a local/temporary setting. For the - * latter, see {@link GlobalSession#isEngineLocallyEnabled(String)}. - * - * @param engineName the name to check (e.g., "bookmarks"). - * @param engineSettings - * if non-null, verify that the server engine settings are congruent - * with this, throwing the appropriate MetaGlobalException if not. - * @return - * true if the engine with the provided name is present in the - * meta/global "engines" object, and verification passed. - * - * @throws MetaGlobalException - */ - public boolean isEngineRemotelyEnabled(String engineName, EngineSettings engineSettings) throws MetaGlobalException { - if (this.config.metaGlobal == null) { - throw new MetaGlobalNotSetException(); - } - - // This should not occur. - if (this.config.enabledEngineNames == null) { - Logger.error(LOG_TAG, "No enabled engines in config. Giving up."); - throw new MetaGlobalMissingEnginesException(); - } - - if (!(this.config.enabledEngineNames.contains(engineName))) { - Logger.debug(LOG_TAG, "Engine " + engineName + " not enabled: no meta/global entry."); - return false; - } - - // If we have a meta/global, check that it's safe for us to sync. - // (If we don't, we'll create one later, which is why we return `true` above.) - if (engineSettings != null) { - // Throws if there's a problem. - this.config.metaGlobal.verifyEngineSettings(engineName, engineSettings); - } - - return true; - } - - - /** - * Return true if the named stage should be synced this session. - * <p> - * This is a local/temporary setting, in contrast to the meta/global record, - * which is a global/permanent setting. For the latter, see - * {@link GlobalSession#isEngineRemotelyEnabled(String, EngineSettings)}. - * - * @param stageName - * to query. - * @return true if named stage is enabled for this sync. - */ - public boolean isEngineLocallyEnabled(String stageName) { - if (config.stagesToSync == null) { - return true; - } - return config.stagesToSync.contains(stageName); - } - - public ClientsDataDelegate getClientsDelegate() { - return this.clientsDelegate; - } - - /** - * The longest backoff observed to date; -1 means no backoff observed. - */ - protected final AtomicLong largestBackoffObserved = new AtomicLong(-1); - - /** - * Reset any observed backoff and start observing HTTP responses for backoff - * requests. - */ - protected void installAsHttpResponseObserver() { - Logger.debug(LOG_TAG, "Adding " + this + " as a BaseResource HttpResponseObserver."); - BaseResource.addHttpResponseObserver(this); - largestBackoffObserved.set(-1); - } - - /** - * Stop observing HttpResponses for backoff requests. - */ - protected void uninstallAsHttpResponseObserver() { - Logger.debug(LOG_TAG, "Removing " + this + " as a BaseResource HttpResponseObserver."); - BaseResource.removeHttpResponseObserver(this); - } - - /** - * Observe all HTTP response for backoff requests on all status codes, not just errors. - */ - @Override - public void observeHttpResponse(HttpUriRequest request, HttpResponse response) { - // Ignore non-Sync storage requests. - final URI clusterURL = config.getClusterURL(); - if (clusterURL != null && !clusterURL.getHost().equals(request.getURI().getHost())) { - // It's possible to see requests without a clusterURL (in particular, - // during testing); allow some extra backoffs in this case. - return; - } - - long responseBackoff = (new SyncResponse(response)).totalBackoffInMilliseconds(); // TODO: don't allocate object? - if (responseBackoff <= 0) { - return; - } - - Logger.debug(LOG_TAG, "Observed " + responseBackoff + " millisecond backoff request."); - while (true) { - long existingBackoff = largestBackoffObserved.get(); - if (existingBackoff >= responseBackoff) { - return; - } - if (largestBackoffObserved.compareAndSet(existingBackoff, responseBackoff)) { - return; - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java deleted file mode 100644 index 69bba8841..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/HTTPFailureException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import android.content.SyncResult; - -public class HTTPFailureException extends SyncException { - private static final long serialVersionUID = -5415864029780770619L; - public SyncStorageResponse response; - - public HTTPFailureException(SyncStorageResponse response) { - this.response = response; - } - - @Override - public String toString() { - String errorMessage; - try { - errorMessage = this.response.getErrorMessage(); - } catch (Exception e) { - // Oh well. - errorMessage = "[unknown error message]"; - } - return "<HTTPFailureException " + this.response.getStatusCode() + - " :: (" + errorMessage + ")>"; - } - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - switch (response.getStatusCode()) { - case 401: - // Node reassignment 401s get handled internally. - syncResult.stats.numAuthExceptions++; - return; - case 500: - case 501: - case 503: - // TODO: backoff. - syncResult.stats.numIoExceptions++; - return; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java deleted file mode 100644 index 374fa5cf5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCollections.java +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import org.mozilla.gecko.background.common.log.Logger; - -/** - * Fetches the timestamp information in <code>info/collections</code> on the - * Sync server. Provides access to those timestamps, along with logic to check - * for whether a collection requires an update. - */ -public class InfoCollections { - private static final String LOG_TAG = "InfoCollections"; - - /** - * Fields fetched from the server, or <code>null</code> if not yet fetched. - * <p> - * Rather than storing decimal/double timestamps, as provided by the server, - * we convert immediately to milliseconds since epoch. - */ - final Map<String, Long> timestamps; - - public InfoCollections() { - this(new ExtendedJSONObject()); - } - - public InfoCollections(final ExtendedJSONObject record) { - Logger.debug(LOG_TAG, "info/collections is " + record.toJSONString()); - HashMap<String, Long> map = new HashMap<String, Long>(); - - for (Entry<String, Object> entry : record.entrySet()) { - final String key = entry.getKey(); - final Object value = entry.getValue(); - - // These objects are most likely going to be Doubles. Regardless, we - // want to get them in a more sane time format. - if (value instanceof Double) { - map.put(key, Utils.decimalSecondsToMilliseconds((Double) value)); - continue; - } - if (value instanceof Long) { - map.put(key, Utils.decimalSecondsToMilliseconds((Long) value)); - continue; - } - if (value instanceof Integer) { - map.put(key, Utils.decimalSecondsToMilliseconds((Integer) value)); - continue; - } - Logger.warn(LOG_TAG, "Skipping info/collections entry for " + key); - } - - this.timestamps = Collections.unmodifiableMap(map); - } - - /** - * Return the timestamp for the given collection, or null if the timestamps - * have not been fetched or the given collection does not have a timestamp. - * - * @param collection - * The collection to inspect. - * @return the timestamp in milliseconds since epoch. - */ - public Long getTimestamp(String collection) { - if (timestamps == null) { - return null; - } - return timestamps.get(collection); - } - - /** - * Test if a given collection needs to be updated. - * - * @param collection - * The collection to test. - * @param lastModified - * Timestamp when local record was last modified. - */ - public boolean updateNeeded(String collection, long lastModified) { - Logger.trace(LOG_TAG, "Testing " + collection + " for updateNeeded. Local last modified is " + lastModified + "."); - - // No local record of modification time? Need an update. - if (lastModified <= 0) { - return true; - } - - // No meta/global on the server? We need an update. The server fetch will fail and - // then we will upload a fresh meta/global. - Long serverLastModified = getTimestamp(collection); - if (serverLastModified == null) { - return true; - } - - // Otherwise, we need an update if our modification time is stale. - return serverLastModified > lastModified; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java deleted file mode 100644 index eb2428433..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoConfiguration.java +++ /dev/null @@ -1,93 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.util.Log; - -import org.mozilla.gecko.background.common.log.Logger; - -/** - * Wraps and provides access to configuration data returned from info/configuration. - * Docs: https://docs.services.mozilla.com/storage/apis-1.5.html#general-info - * - * - <bold>max_request_bytes</bold>: the maximum size in bytes of the overall - * HTTP request body that will be accepted by the server. - * - * - <bold>max_post_records</bold>: the maximum number of records that can be - * uploaded to a collection in a single POST request. - * - * - <bold>max_post_bytes</bold>: the maximum combined size in bytes of the - * record payloads that can be uploaded to a collection in a single - * POST request. - * - * - <bold>max_total_records</bold>: the maximum number of records that can be - * uploaded to a collection as part of a batched upload. - * - * - <bold>max_total_bytes</bold>: the maximum combined size in bytes of the - * record payloads that can be uploaded to a collection as part of - * a batched upload. - */ -public class InfoConfiguration { - private static final String LOG_TAG = "InfoConfiguration"; - - public static final String MAX_REQUEST_BYTES = "max_request_bytes"; - public static final String MAX_POST_RECORDS = "max_post_records"; - public static final String MAX_POST_BYTES = "max_post_bytes"; - public static final String MAX_TOTAL_RECORDS = "max_total_records"; - public static final String MAX_TOTAL_BYTES = "max_total_bytes"; - - private static final long DEFAULT_MAX_REQUEST_BYTES = 1048576; - private static final long DEFAULT_MAX_POST_RECORDS = 100; - private static final long DEFAULT_MAX_POST_BYTES = 1048576; - private static final long DEFAULT_MAX_TOTAL_RECORDS = 10000; - private static final long DEFAULT_MAX_TOTAL_BYTES = 104857600; - - // While int's upper range is (2^31-1), which in bytes is equivalent to 2.147 GB, let's be optimistic - // about the future and use long here, so that this code works if the server decides its clients are - // all on fiber and have congress-library sized bookmark collections. - // Record counts are long for the sake of simplicity. - public final long maxRequestBytes; - public final long maxPostRecords; - public final long maxPostBytes; - public final long maxTotalRecords; - public final long maxTotalBytes; - - public InfoConfiguration() { - Logger.debug(LOG_TAG, "info/configuration is unavailable, using defaults"); - - maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES; - maxPostRecords = DEFAULT_MAX_POST_RECORDS; - maxPostBytes = DEFAULT_MAX_POST_BYTES; - maxTotalRecords = DEFAULT_MAX_TOTAL_RECORDS; - maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES; - } - - public InfoConfiguration(final ExtendedJSONObject record) { - Logger.debug(LOG_TAG, "info/configuration is " + record.toJSONString()); - - maxRequestBytes = getValueFromRecord(record, MAX_REQUEST_BYTES, DEFAULT_MAX_REQUEST_BYTES); - maxPostRecords = getValueFromRecord(record, MAX_POST_RECORDS, DEFAULT_MAX_POST_RECORDS); - maxPostBytes = getValueFromRecord(record, MAX_POST_BYTES, DEFAULT_MAX_POST_BYTES); - maxTotalRecords = getValueFromRecord(record, MAX_TOTAL_RECORDS, DEFAULT_MAX_TOTAL_RECORDS); - maxTotalBytes = getValueFromRecord(record, MAX_TOTAL_BYTES, DEFAULT_MAX_TOTAL_BYTES); - } - - private static Long getValueFromRecord(ExtendedJSONObject record, String key, long defaultValue) { - if (!record.containsKey(key)) { - return defaultValue; - } - - try { - Long val = record.getLong(key); - if (val == null) { - return defaultValue; - } - return val; - } catch (NumberFormatException e) { - Log.w(LOG_TAG, "Could not parse key " + key + " from record: " + record, e); - return defaultValue; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java deleted file mode 100644 index 832e97d10..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/InfoCounts.java +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; - -public class InfoCounts { - static final String LOG_TAG = "InfoCounts"; - - /** - * Counts fetched from the server, or <code>null</code> if not yet fetched. - */ - private Map<String, Integer> counts = null; - - @SuppressWarnings("unchecked") - public InfoCounts(final ExtendedJSONObject record) { - Logger.debug(LOG_TAG, "info/collection_counts is " + record.toJSONString()); - HashMap<String, Integer> map = new HashMap<String, Integer>(); - - Set<Entry<String, Object>> entrySet = record.object.entrySet(); - - String key; - Object value; - - for (Entry<String, Object> entry : entrySet) { - key = entry.getKey(); - value = entry.getValue(); - - if (value instanceof Integer) { - map.put(key, (Integer) value); - continue; - } - - if (value instanceof Long) { - map.put(key, ((Long) value).intValue()); - continue; - } - - Logger.warn(LOG_TAG, "Skipping info/collection_counts entry for " + key); - } - - this.counts = Collections.unmodifiableMap(map); - } - - /** - * Return the server count for the given collection, or null if the counts - * have not been fetched or the given collection does not have a count. - * - * @param collection - * The collection to inspect. - * @return the number of elements in the named collection. - */ - public Integer getCount(String collection) { - if (counts == null) { - return null; - } - return counts.get(collection); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java deleted file mode 100644 index 982b5b026..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/JSONRecordFetcher.java +++ /dev/null @@ -1,145 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -/** - * An object which fetches a chunk of JSON from a URI, using certain credentials, - * and informs its delegate of the result. - */ -public class JSONRecordFetcher { - private static final long DEFAULT_AWAIT_TIMEOUT_MSEC = 2 * 60 * 1000; // Two minutes. - private static final String LOG_TAG = "JSONRecordFetcher"; - - protected final AuthHeaderProvider authHeaderProvider; - protected final String uri; - protected JSONRecordFetchDelegate delegate; - - public JSONRecordFetcher(final String uri, final AuthHeaderProvider authHeaderProvider) { - if (uri == null) { - throw new IllegalArgumentException("uri must not be null"); - } - this.uri = uri; - this.authHeaderProvider = authHeaderProvider; - } - - protected String getURI() { - return this.uri; - } - - private class JSONFetchHandler implements SyncStorageRequestDelegate { - - // SyncStorageRequestDelegate methods for fetching. - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - - @Override - public String ifUnmodifiedSince() { - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - if (response.wasSuccessful()) { - try { - delegate.handleSuccess(response.jsonObjectBody()); - } catch (Exception e) { - handleRequestError(e); - } - return; - } - handleRequestFailure(response); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - delegate.handleFailure(response); - } - - @Override - public void handleRequestError(Exception ex) { - delegate.handleError(ex); - } - } - - public void fetch(final JSONRecordFetchDelegate delegate) { - this.delegate = delegate; - try { - final SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.getURI()); - r.delegate = new JSONFetchHandler(); - r.get(); - } catch (Exception e) { - delegate.handleError(e); - } - } - - private class LatchedJSONRecordFetchDelegate implements JSONRecordFetchDelegate { - public ExtendedJSONObject body = null; - public Exception exception = null; - private final CountDownLatch latch; - - public LatchedJSONRecordFetchDelegate(CountDownLatch latch) { - this.latch = latch; - } - - @Override - public void handleFailure(SyncStorageResponse response) { - this.exception = new HTTPFailureException(response); - latch.countDown(); - } - - @Override - public void handleError(Exception e) { - this.exception = e; - latch.countDown(); - } - - @Override - public void handleSuccess(ExtendedJSONObject body) { - this.body = body; - latch.countDown(); - } - } - - /** - * Fetch the info record, blocking until it returns. - * @return the info record. - */ - public ExtendedJSONObject fetchBlocking() throws HTTPFailureException, Exception { - CountDownLatch latch = new CountDownLatch(1); - LatchedJSONRecordFetchDelegate delegate = new LatchedJSONRecordFetchDelegate(latch); - this.delegate = delegate; - this.fetch(delegate); - - // Sanity wait: the resource itself will time out and throw after two - // minutes, so we just want to avoid coding errors causing us to block - // endlessly. - if (!latch.await(DEFAULT_AWAIT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)) { - Logger.warn(LOG_TAG, "Interrupted fetching info record."); - throw new InterruptedException("info fetch timed out."); - } - - if (delegate.body != null) { - return delegate.body; - } - - if (delegate.exception != null) { - throw delegate.exception; - } - - throw new Exception("Unknown error."); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java deleted file mode 100644 index 4a2be2a9b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/KeyBundleProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.sync.crypto.KeyBundle; - -public interface KeyBundleProvider { - public abstract KeyBundle keyBundle(); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java deleted file mode 100644 index a90c0fee8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java +++ /dev/null @@ -1,372 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException; -import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedVersionException; -import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public class MetaGlobal implements SyncStorageRequestDelegate { - private static final String LOG_TAG = "MetaGlobal"; - protected String metaURL; - - // Fields. - protected ExtendedJSONObject engines; - protected JSONArray declined; - protected Long storageVersion; - protected String syncID; - - // Lookup tables. - protected Map<String, String> syncIDs; - protected Map<String, Integer> versions; - protected Map<String, MetaGlobalException> exceptions; - - // Temporary location to store our callback. - private MetaGlobalDelegate callback; - - // A little hack so we can use the same delegate implementation for upload and download. - private boolean isUploading; - protected final AuthHeaderProvider authHeaderProvider; - - public MetaGlobal(String metaURL, AuthHeaderProvider authHeaderProvider) { - this.metaURL = metaURL; - this.authHeaderProvider = authHeaderProvider; - } - - public void fetch(MetaGlobalDelegate delegate) { - this.callback = delegate; - try { - this.isUploading = false; - SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL); - r.delegate = this; - r.get(); - } catch (URISyntaxException e) { - this.callback.handleError(e); - } - } - - public void upload(MetaGlobalDelegate callback) { - try { - this.isUploading = true; - SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL); - - r.delegate = this; - this.callback = callback; - r.put(this.asCryptoRecord()); - } catch (Exception e) { - callback.handleError(e); - } - } - - protected ExtendedJSONObject asRecordContents() { - ExtendedJSONObject json = new ExtendedJSONObject(); - json.put("storageVersion", storageVersion); - json.put("engines", engines); - json.put("syncID", syncID); - json.put("declined", declined); - return json; - } - - /** - * Return a copy ready for upload. - * @return an unencrypted <code>CryptoRecord</code>. - */ - public CryptoRecord asCryptoRecord() { - ExtendedJSONObject payload = this.asRecordContents(); - CryptoRecord record = new CryptoRecord(payload); - record.collection = "meta"; - record.guid = "global"; - record.deleted = false; - return record; - } - - public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, NonObjectJSONException, NonArrayJSONException { - if (record == null) { - throw new IllegalArgumentException("Cannot set meta/global from null record"); - } - Logger.debug(LOG_TAG, "meta/global is " + record.payload.toJSONString()); - this.storageVersion = (Long) record.payload.get("storageVersion"); - this.syncID = (String) record.payload.get("syncID"); - - setEngines(record.payload.getObject("engines")); - - // Accepts null -- declined can be missing. - setDeclinedEngineNames(record.payload.getArray("declined")); - } - - public Long getStorageVersion() { - return this.storageVersion; - } - - public void setStorageVersion(Long version) { - this.storageVersion = version; - } - - public ExtendedJSONObject getEngines() { - return engines; - } - - @SuppressWarnings("unchecked") - public void declineEngine(String engine) { - if (this.declined == null) { - JSONArray replacement = new JSONArray(); - replacement.add(engine); - setDeclinedEngineNames(replacement); - return; - } - - this.declined.add(engine); - } - - @SuppressWarnings("unchecked") - public void declineEngineNames(Collection<String> additional) { - if (this.declined == null) { - JSONArray replacement = new JSONArray(); - replacement.addAll(additional); - setDeclinedEngineNames(replacement); - return; - } - - for (String engine : additional) { - if (!this.declined.contains(engine)) { - this.declined.add(engine); - } - } - } - - public void setDeclinedEngineNames(JSONArray declined) { - if (declined == null) { - this.declined = new JSONArray(); - return; - } - this.declined = declined; - } - - /** - * Return the set of engines that we support (given as an argument) - * but the user hasn't explicitly declined on another device. - * - * Can return the input if the user hasn't declined any engines. - */ - public Set<String> getNonDeclinedEngineNames(Set<String> supported) { - if (this.declined == null || - this.declined.isEmpty()) { - return supported; - } - - final Set<String> result = new HashSet<String>(supported); - result.removeAll(this.declined); - return result; - } - - public void setEngines(ExtendedJSONObject engines) { - if (engines == null) { - engines = new ExtendedJSONObject(); - } - this.engines = engines; - final int count = engines.size(); - versions = new HashMap<String, Integer>(count); - syncIDs = new HashMap<String, String>(count); - exceptions = new HashMap<String, MetaGlobalException>(count); - for (String engineName : engines.keySet()) { - try { - ExtendedJSONObject engineEntry = engines.getObject(engineName); - recordEngineState(engineName, engineEntry); - } catch (NonObjectJSONException e) { - Logger.error(LOG_TAG, "Engine field for " + engineName + " in meta/global is not an object."); - recordEngineState(engineName, new ExtendedJSONObject()); // Doesn't have a version or syncID, for example, so will be server wiped. - } - } - } - - /** - * Take a JSON object corresponding to the 'engines' field for the provided engine name, - * updating {@link #syncIDs} and {@link #versions} accordingly. - * - * If the record is malformed, an entry is added to {@link #exceptions}, to be rethrown - * during validation. - */ - protected void recordEngineState(String engineName, ExtendedJSONObject engineEntry) { - if (engineEntry == null) { - throw new IllegalArgumentException("engineEntry cannot be null."); - } - - // Record syncID first, so that engines with bad versions are recorded. - try { - String syncID = engineEntry.getString("syncID"); - if (syncID == null) { - Logger.warn(LOG_TAG, "No syncID for " + engineName + ". Recording exception."); - exceptions.put(engineName, new MetaGlobalMalformedSyncIDException()); - } - syncIDs.put(engineName, syncID); - } catch (ClassCastException e) { - // Malformed syncID on the server. Wipe the server. - Logger.warn(LOG_TAG, "Malformed syncID " + engineEntry.get("syncID") + - " for " + engineName + ". Recording exception."); - exceptions.put(engineName, new MetaGlobalMalformedSyncIDException()); - } - - try { - Integer version = engineEntry.getIntegerSafely("version"); - Logger.trace(LOG_TAG, "Engine " + engineName + " has server version " + version); - if (version == null || - version == 0) { - // Invalid version. Wipe the server. - Logger.warn(LOG_TAG, "Malformed version " + version + - " for " + engineName + ". Recording exception."); - exceptions.put(engineName, new MetaGlobalMalformedVersionException()); - return; - } - versions.put(engineName, version); - } catch (NumberFormatException e) { - // Invalid version. Wipe the server. - Logger.warn(LOG_TAG, "Malformed version " + engineEntry.get("version") + - " for " + engineName + ". Recording exception."); - exceptions.put(engineName, new MetaGlobalMalformedVersionException()); - return; - } - } - - /** - * Get enabled engine names. - * - * @return a collection of engine names or <code>null</code> if meta/global - * was malformed. - */ - public Set<String> getEnabledEngineNames() { - if (engines == null) { - return null; - } - return new HashSet<String>(engines.keySet()); - } - - @SuppressWarnings("unchecked") - public Set<String> getDeclinedEngineNames() { - if (declined == null) { - return null; - } - return new HashSet<String>(declined); - } - - /** - * Returns if the server settings and local settings match. - * Throws a specific MetaGlobalException if that's not the case. - */ - public void verifyEngineSettings(String engineName, EngineSettings engineSettings) - throws MetaGlobalException { - - // We use syncIDs as our canary. - if (syncIDs == null) { - throw new IllegalStateException("No meta/global record yet processed."); - } - - if (engineSettings == null) { - throw new IllegalArgumentException("engineSettings cannot be null."); - } - - // First, see if we had a parsing problem. - final MetaGlobalException exception = exceptions.get(engineName); - if (exception != null) { - throw exception; - } - - final String syncID = syncIDs.get(engineName); - if (syncID == null) { - // We have checked engineName against enabled engine names before this, so - // we should either have a syncID or an exception for this engine already. - throw new IllegalArgumentException("Unknown engine " + engineName); - } - - // Since we don't have an exception, and we do have a syncID, we should have a version. - final Integer version = versions.get(engineName); - if (version > engineSettings.version) { - // We're out of date. - throw new MetaGlobalException.MetaGlobalStaleClientVersionException(version); - } - - if (!syncID.equals(engineSettings.syncID)) { - // Our syncID is wrong. Reset client and take the server syncID. - throw new MetaGlobalException.MetaGlobalStaleClientSyncIDException(syncID); - } - } - - public String getSyncID() { - return syncID; - } - - public void setSyncID(String syncID) { - this.syncID = syncID; - } - - // SyncStorageRequestDelegate methods for fetching. - public String credentials() { - return null; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - - @Override - public String ifUnmodifiedSince() { - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - if (this.isUploading) { - this.handleUploadSuccess(response); - } else { - this.handleDownloadSuccess(response); - } - } - - private void handleUploadSuccess(SyncStorageResponse response) { - this.callback.handleSuccess(this, response); - } - - private void handleDownloadSuccess(SyncStorageResponse response) { - if (response.wasSuccessful()) { - try { - CryptoRecord record = CryptoRecord.fromJSONRecord(response.jsonObjectBody()); - this.setFromRecord(record); - this.callback.handleSuccess(this, response); - } catch (Exception e) { - this.callback.handleError(e); - } - return; - } - this.callback.handleFailure(response); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - if (response.getStatusCode() == 404) { - this.callback.handleMissing(this, response); - return; - } - this.callback.handleFailure(response); - } - - @Override - public void handleRequestError(Exception e) { - this.callback.handleError(e); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java deleted file mode 100644 index bec531d11..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalException.java +++ /dev/null @@ -1,45 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class MetaGlobalException extends SyncException { - private static final long serialVersionUID = -6182315615113508925L; - - public static class MetaGlobalMalformedSyncIDException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - } - - public static class MetaGlobalMalformedVersionException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - } - - public static class MetaGlobalOutdatedVersionException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - } - - public static class MetaGlobalStaleClientVersionException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - public final int serverVersion; - public MetaGlobalStaleClientVersionException(final int version) { - this.serverVersion = version; - } - } - - public static class MetaGlobalStaleClientSyncIDException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - public final String serverSyncID; - public MetaGlobalStaleClientSyncIDException(final String syncID) { - this.serverSyncID = syncID; - } - } - - public static class MetaGlobalEngineStateChangedException extends MetaGlobalException { - private static final long serialVersionUID = 1L; - public final boolean isEnabled; - public MetaGlobalEngineStateChangedException(boolean isEnabled) { - this.isEnabled = isEnabled; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java deleted file mode 100644 index 91bfd2f76..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalMissingEnginesException.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class MetaGlobalMissingEnginesException extends MetaGlobalException { - private static final long serialVersionUID = -2662107402622277865L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java deleted file mode 100644 index ef059c71d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobalNotSetException.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class MetaGlobalNotSetException extends MetaGlobalException { - private static final long serialVersionUID = 2959032409571832970L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java deleted file mode 100644 index 323e355b4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NoCollectionKeysSetException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -public class NoCollectionKeysSetException extends SyncException { - private static final long serialVersionUID = -6185128075412771120L; - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - syncResult.stats.numAuthExceptions++; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java deleted file mode 100644 index a5cd5f0eb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NodeAuthenticationException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -public class NodeAuthenticationException extends SyncException { - private static final long serialVersionUID = 8156745873212364352L; - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - syncResult.stats.numAuthExceptions++; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java deleted file mode 100644 index 554645b11..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class NonArrayJSONException extends UnexpectedJSONException { - private static final long serialVersionUID = 5582918057432365749L; - - public NonArrayJSONException(String detailMessage) { - super(detailMessage); - } - - public NonArrayJSONException(Throwable throwable) { - super(throwable); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java deleted file mode 100644 index fd50d465e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class NonObjectJSONException extends UnexpectedJSONException { - private static final long serialVersionUID = 2214238763035650087L; - - public NonObjectJSONException(String detailMessage) { - super(detailMessage); - } - - public NonObjectJSONException(Throwable throwable) { - super(throwable); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java deleted file mode 100644 index c1d8833b6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NullClusterURLException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -public class NullClusterURLException extends SyncException { - private static final long serialVersionUID = 4277845518548393161L; - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - syncResult.stats.numAuthExceptions++; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java deleted file mode 100644 index d3467545c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PersistedMetaGlobal.java +++ /dev/null @@ -1,86 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; - -import android.content.SharedPreferences; - -public class PersistedMetaGlobal { - public static final String LOG_TAG = "PersistedMetaGlobal"; - - public static final String META_GLOBAL_SERVER_RESPONSE_BODY = "metaGlobalServerResponseBody"; - public static final String META_GLOBAL_LAST_MODIFIED = "metaGlobalLastModified"; - - protected SharedPreferences prefs; - - public PersistedMetaGlobal(SharedPreferences prefs) { - this.prefs = prefs; - } - - /** - * Sets a <code>MetaGlobal</code> from persisted prefs. - * - * @param metaUrl - * meta/global server URL - * @param credentials - * Sync credentials - * - * @return <MetaGlobal> set from previously fetched meta/global record from - * server - */ - public MetaGlobal metaGlobal(String metaUrl, AuthHeaderProvider authHeaderProvider) { - String json = prefs.getString(META_GLOBAL_SERVER_RESPONSE_BODY, null); - if (json == null) { - return null; - } - MetaGlobal metaGlobal = null; - try { - CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(json); - MetaGlobal mg = new MetaGlobal(metaUrl, authHeaderProvider); - mg.setFromRecord(cryptoRecord); - metaGlobal = mg; - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception decrypting persisted meta/global.", e); - } - return metaGlobal; - } - - public void persistMetaGlobal(MetaGlobal metaGlobal) { - if (metaGlobal == null) { - Logger.debug(LOG_TAG, "Clearing persisted meta/global."); - prefs.edit().remove(META_GLOBAL_SERVER_RESPONSE_BODY).commit(); - return; - } - try { - CryptoRecord cryptoRecord = metaGlobal.asCryptoRecord(); - String json = cryptoRecord.toJSONString(); - Logger.debug(LOG_TAG, "Persisting meta/global."); - prefs.edit().putString(META_GLOBAL_SERVER_RESPONSE_BODY, json).commit(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception encrypting while persisting meta/global.", e); - } - } - - public long lastModified() { - return prefs.getLong(META_GLOBAL_LAST_MODIFIED, -1); - } - - public void persistLastModified(long lastModified) { - if (lastModified <= 0) { - Logger.debug(LOG_TAG, "Clearing persisted meta/global last modified timestamp."); - prefs.edit().remove(META_GLOBAL_LAST_MODIFIED).commit(); - return; - } - Logger.debug(LOG_TAG, "Persisting meta/global last modified timestamp " + lastModified + "."); - prefs.edit().putLong(META_GLOBAL_LAST_MODIFIED, lastModified).commit(); - } - - public void purge() { - persistLastModified(-1); - persistMetaGlobal(null); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java deleted file mode 100644 index 63f6446da..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/PrefsBackoffHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -public class PrefsBackoffHandler implements BackoffHandler { - public static final String PREF_EARLIEST_NEXT = "earliestnext"; - - private final SharedPreferences prefs; - private final String prefEarliest; - - public PrefsBackoffHandler(final SharedPreferences prefs, final String prefSuffix) { - if (prefs == null) { - throw new IllegalArgumentException("prefs must not be null."); - } - this.prefs = prefs; - this.prefEarliest = PREF_EARLIEST_NEXT + "." + prefSuffix; - } - - @Override - public synchronized long getEarliestNextRequest() { - return prefs.getLong(prefEarliest, 0); - } - - @Override - public synchronized void setEarliestNextRequest(final long next) { - final Editor edit = prefs.edit(); - edit.putLong(prefEarliest, next); - edit.commit(); - } - - @Override - public synchronized void extendEarliestNextRequest(final long next) { - if (prefs.getLong(prefEarliest, 0) >= next) { - return; - } - final Editor edit = prefs.edit(); - edit.putLong(prefEarliest, next); - edit.commit(); - } - - /** - * Return the number of milliseconds until we're allowed to touch the server again, - * or 0 if now is fine. - */ - @Override - public long delayMilliseconds() { - long earliestNextRequest = getEarliestNextRequest(); - if (earliestNextRequest <= 0) { - return 0; - } - long now = System.currentTimeMillis(); - return Math.max(0, earliestNextRequest - now); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt deleted file mode 100644 index cf4624ca4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/README.txt +++ /dev/null @@ -1 +0,0 @@ -These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost. diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java deleted file mode 100644 index 4ea77f37c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11PreviousPostFailedException.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -/** - * A previous POST failed, so we won't send any more records this session. - */ -public class Server11PreviousPostFailedException extends SyncException { - private static final long serialVersionUID = -3582490631414624310L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java deleted file mode 100644 index d654d3116..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Server11RecordPostFailedException.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -/** - * The server rejected a record in its "failure" array. - */ -public class Server11RecordPostFailedException extends SyncException { - private static final long serialVersionUID = -8517471217486190314L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java deleted file mode 100644 index 4c1584d5a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java +++ /dev/null @@ -1,121 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.background.fxa.FxAccountUtils; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.util.HardwareUtils; - -import android.accounts.Account; -import android.content.Context; -import android.content.SharedPreferences; - -/** - * A <code>ClientsDataDelegate</code> implementation that persists to a - * <code>SharedPreferences</code> instance. - */ -public class SharedPreferencesClientsDataDelegate implements ClientsDataDelegate { - protected final SharedPreferences sharedPreferences; - protected final Context context; - - public SharedPreferencesClientsDataDelegate(SharedPreferences sharedPreferences, Context context) { - this.sharedPreferences = sharedPreferences; - this.context = context; - - // It's safe to init this multiple times. - HardwareUtils.init(context); - } - - @Override - public synchronized String getAccountGUID() { - String accountGUID = sharedPreferences.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null); - if (accountGUID == null) { - accountGUID = Utils.generateGuid(); - sharedPreferences.edit().putString(SyncConfiguration.PREF_ACCOUNT_GUID, accountGUID).commit(); - } - return accountGUID; - } - - private synchronized void saveClientNameToSharedPreferences(String clientName, long now) { - sharedPreferences - .edit() - .putString(SyncConfiguration.PREF_CLIENT_NAME, clientName) - .putLong(SyncConfiguration.PREF_CLIENT_DATA_TIMESTAMP, now) - .apply(); - } - - /** - * Set client name. - * - * @param clientName to change to. - */ - @Override - public synchronized void setClientName(String clientName, long now) { - saveClientNameToSharedPreferences(clientName, now); - - // Update the FxA device registration - final Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account != null) { - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - fxAccount.resetDeviceRegistrationVersion(); - } - } - - @Override - public String getDefaultClientName() { - return FxAccountUtils.defaultClientName(context); - } - - @Override - public synchronized String getClientName() { - String clientName = sharedPreferences.getString(SyncConfiguration.PREF_CLIENT_NAME, null); - if (clientName == null) { - clientName = getDefaultClientName(); - long now = System.currentTimeMillis(); - saveClientNameToSharedPreferences(clientName, now); // Save locally only to avoid a recursion loop - } - return clientName; - } - - @Override - public synchronized void setClientsCount(int clientsCount) { - sharedPreferences.edit().putLong(SyncConfiguration.PREF_NUM_CLIENTS, clientsCount).commit(); - } - - @Override - public boolean isLocalGUID(String guid) { - return getAccountGUID().equals(guid); - } - - @Override - public synchronized int getClientsCount() { - return (int) sharedPreferences.getLong(SyncConfiguration.PREF_NUM_CLIENTS, 0); - } - - @Override - public long getLastModifiedTimestamp() { - return sharedPreferences.getLong(SyncConfiguration.PREF_CLIENT_DATA_TIMESTAMP, 0); - } - - @Override - public String getFormFactor() { - if (HardwareUtils.isLargeTablet()) { - return "largetablet"; - } - - if (HardwareUtils.isSmallTablet()) { - return "smalltablet"; - } - - if (HardwareUtils.isTelevision()) { - return "tv"; - } - - return "phone"; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java deleted file mode 100644 index 4b2280895..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Sync11Configuration.java +++ /dev/null @@ -1,84 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.net.URI; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -/** - * Override SyncConfiguration to restore the old behavior of clusterURL -- - * that is, a URL without the protocol version etc. - * - */ -public class Sync11Configuration extends SyncConfiguration { - private static final String LOG_TAG = "Sync11Configuration"; - private static final String API_VERSION = "1.1"; - - public Sync11Configuration(String username, - AuthHeaderProvider authHeaderProvider, - SharedPreferences prefs) { - super(username, authHeaderProvider, prefs); - } - - public Sync11Configuration(String username, - AuthHeaderProvider authHeaderProvider, - SharedPreferences prefs, - KeyBundle keyBundle) { - super(username, authHeaderProvider, prefs, keyBundle); - } - - @Override - public String getAPIVersion() { - return API_VERSION; - } - - @Override - public String storageURL() { - return clusterURL + API_VERSION + "/" + username + "/storage"; - } - - @Override - protected String infoBaseURL() { - return clusterURL + API_VERSION + "/" + username + "/info/"; - } - - protected void setAndPersistClusterURL(URI u, SharedPreferences prefs) { - boolean shouldPersist = (prefs != null) && (clusterURL == null); - - Logger.trace(LOG_TAG, "Setting cluster URL to " + u.toASCIIString() + - (shouldPersist ? ". Persisting." : ". Not persisting.")); - clusterURL = u; - if (shouldPersist) { - Editor edit = prefs.edit(); - edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString()); - edit.commit(); - } - } - - protected void setClusterURL(URI u, SharedPreferences prefs) { - if (u == null) { - Logger.warn(LOG_TAG, "Refusing to set cluster URL to null."); - return; - } - URI uri = u.normalize(); - if (uri.toASCIIString().endsWith("/")) { - setAndPersistClusterURL(u, prefs); - return; - } - setAndPersistClusterURL(uri.resolve("/"), prefs); - Logger.trace(LOG_TAG, "Set cluster URL to " + clusterURL.toASCIIString() + ", given input " + u.toASCIIString()); - } - - @Override - public void setClusterURL(URI u) { - setClusterURL(u, this.getPrefs()); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java deleted file mode 100644 index 53edf5f84..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java +++ /dev/null @@ -1,480 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.mozilla.gecko.background.common.PrefsBranch; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; - -public class SyncConfiguration { - private static final String LOG_TAG = "SyncConfiguration"; - - // These must be set in GlobalSession's constructor. - public URI clusterURL; - public KeyBundle syncKeyBundle; - - public InfoConfiguration infoConfiguration; - - public CollectionKeys collectionKeys; - public InfoCollections infoCollections; - public MetaGlobal metaGlobal; - public String syncID; - - protected final String username; - - /** - * Persisted collection of enabledEngineNames. - * <p> - * Can contain engines Android Sync is not currently aware of, such as "prefs" - * or "addons". - * <p> - * Copied from latest downloaded meta/global record and used to generate a - * fresh meta/global record for upload. - */ - public Set<String> enabledEngineNames; - public Set<String> declinedEngineNames = new HashSet<String>(); - - /** - * Names of stages to sync <it>this sync</it>, or <code>null</code> to sync - * all known stages. - * <p> - * Generated <it>each sync</it> from extras bundle passed to - * <code>SyncAdapter.onPerformSync</code> and not persisted. - * <p> - * Not synchronized! Set this exactly once per global session and don't modify - * it -- especially not from multiple threads. - */ - public Collection<String> stagesToSync; - - /** - * Engines whose sync state has been modified by the user through - * SelectEnginesActivity, where each key-value pair is an engine name and - * its sync state. - * - * This differs from <code>enabledEngineNames</code> in that - * <code>enabledEngineNames</code> reflects the downloaded meta/global, - * whereas <code>userSelectedEngines</code> stores the differences in engines to - * sync that the user has selected. - * - * Each engine stage will check for engine changes at the beginning of the - * stage. - * - * If no engine sync state changes have been made by the user, userSelectedEngines - * will be null, and Sync will proceed normally. - * - * If the user has made changes to engine syncing state, each engine will sync - * according to the sync state specified in userSelectedEngines and propagate that - * state to meta/global, to be uploaded. - */ - public Map<String, Boolean> userSelectedEngines; - public long userSelectedEnginesTimestamp; - - public SharedPreferences prefs; - - protected final AuthHeaderProvider authHeaderProvider; - - public static final String PREF_PREFS_VERSION = "prefs.version"; - public static final long CURRENT_PREFS_VERSION = 1; - - public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp"; // When the collection was touched. - public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp"; // When our record was touched. - public static final String MIGRATION_SENTINEL_CHECK_TIMESTAMP = "migrationSentinelCheckTimestamp"; // When we last looked in meta/fxa_credentials. - - public static final String PREF_CLUSTER_URL = "clusterURL"; - public static final String PREF_SYNC_ID = "syncID"; - - public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames"; - public static final String PREF_DECLINED_ENGINE_NAMES = "declinedEngineNames"; - public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC = "userSelectedEngines"; - public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP = "userSelectedEnginesTimestamp"; - - public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale"; - - public static final String PREF_ACCOUNT_GUID = "account.guid"; - public static final String PREF_CLIENT_NAME = "account.clientName"; - public static final String PREF_NUM_CLIENTS = "account.numClients"; - public static final String PREF_CLIENT_DATA_TIMESTAMP = "account.clientDataTimestamp"; - - private static final String API_VERSION = "1.5"; - - public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs) { - this.username = username; - this.authHeaderProvider = authHeaderProvider; - this.prefs = prefs; - this.loadFromPrefs(prefs); - } - - public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) { - this(username, authHeaderProvider, prefs); - this.syncKeyBundle = syncKeyBundle; - } - - public String getAPIVersion() { - return API_VERSION; - } - - public SharedPreferences getPrefs() { - return this.prefs; - } - - /** - * Valid engines supported by Android Sync. - * - * @return Set<String> of valid engine names that Android Sync implements. - */ - public static Set<String> validEngineNames() { - Set<String> engineNames = new HashSet<String>(); - for (Stage stage : Stage.getNamedStages()) { - engineNames.add(stage.getRepositoryName()); - } - return engineNames; - } - - /** - * Return a convenient accessor for part of prefs. - * @return - * A PrefsBranch object representing this - * section of the preferences space. - */ - public PrefsBranch getBranch(String prefix) { - return new PrefsBranch(this.getPrefs(), prefix); - } - - /** - * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global. - * - * @param prefs - * SharedPreferences that the engines are associated with. - * @param pref - * The preference name to use. E.g, PREF_ENABLED_ENGINE_NAMES. - * @return Set<String> of the enabled engine names if they have been stored, - * or null otherwise. - */ - protected static Set<String> getEngineNamesFromPref(SharedPreferences prefs, String pref) { - final String json = prefs.getString(pref, null); - if (json == null) { - return null; - } - try { - final ExtendedJSONObject o = new ExtendedJSONObject(json); - return new HashSet<String>(o.keySet()); - } catch (Exception e) { - return null; - } - } - - /** - * Returns the set of engine names that the user has enabled. If none - * have been stored in prefs, <code>null</code> is returned. - */ - public static Set<String> getEnabledEngineNames(SharedPreferences prefs) { - return getEngineNamesFromPref(prefs, PREF_ENABLED_ENGINE_NAMES); - } - - /** - * Returns the set of engine names that the user has declined. - */ - public static Set<String> getDeclinedEngineNames(SharedPreferences prefs) { - final Set<String> names = getEngineNamesFromPref(prefs, PREF_DECLINED_ENGINE_NAMES); - if (names == null) { - return new HashSet<String>(); - } - return names; - } - - /** - * Gets the engines whose sync states have been changed by the user through the - * SelectEnginesActivity. - * - * @param prefs - * SharedPreferences of account that the engines are associated with. - * @return Map<String, Boolean> of changed engines. Key is the lower-cased - * engine name, Value is the new sync state. - */ - public static Map<String, Boolean> getUserSelectedEngines(SharedPreferences prefs) { - String json = prefs.getString(PREF_USER_SELECTED_ENGINES_TO_SYNC, null); - if (json == null) { - return null; - } - try { - ExtendedJSONObject o = new ExtendedJSONObject(json); - Map<String, Boolean> map = new HashMap<String, Boolean>(); - for (Entry<String, Object> e : o.entrySet()) { - String key = e.getKey(); - Boolean value = (Boolean) e.getValue(); - map.put(key, value); - // Forms depends on history. Add forms if history is selected. - if ("history".equals(key)) { - map.put("forms", value); - } - } - // Sanity check: remove forms if history does not exist. - if (!map.containsKey("history")) { - map.remove("forms"); - } - return map; - } catch (Exception e) { - return null; - } - } - - /** - * Store a Map of engines and their sync states to prefs. - * - * Any engine that's disabled in the input is also recorded - * as a declined engine, overwriting the stored values. - * - * @param prefs - * SharedPreferences that the engines are associated with. - * @param selectedEngines - * Map<String, Boolean> of engine name to sync state - */ - public static void storeSelectedEnginesToPrefs(SharedPreferences prefs, Map<String, Boolean> selectedEngines) { - ExtendedJSONObject jObj = new ExtendedJSONObject(); - HashSet<String> declined = new HashSet<String>(); - for (Entry<String, Boolean> e : selectedEngines.entrySet()) { - final Boolean enabled = e.getValue(); - final String engine = e.getKey(); - jObj.put(engine, enabled); - if (!enabled) { - declined.add(engine); - } - } - - // Our history checkbox drives form history, too. - // We don't need to do this for enablement: that's done at retrieval time. - if (selectedEngines.containsKey("history") && !selectedEngines.get("history")) { - declined.add("forms"); - } - - String json = jObj.toJSONString(); - long currentTime = System.currentTimeMillis(); - Editor edit = prefs.edit(); - edit.putString(PREF_USER_SELECTED_ENGINES_TO_SYNC, json); - edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declined)); - edit.putLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, currentTime); - Logger.error(LOG_TAG, "Storing user-selected engines at [" + currentTime + "]."); - edit.commit(); - } - - public void loadFromPrefs(SharedPreferences prefs) { - if (prefs.contains(PREF_CLUSTER_URL)) { - String u = prefs.getString(PREF_CLUSTER_URL, null); - try { - clusterURL = new URI(u); - Logger.trace(LOG_TAG, "Set clusterURL from bundle: " + u); - } catch (URISyntaxException e) { - Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e); - } - } - if (prefs.contains(PREF_SYNC_ID)) { - syncID = prefs.getString(PREF_SYNC_ID, null); - Logger.trace(LOG_TAG, "Set syncID from bundle: " + syncID); - } - enabledEngineNames = getEnabledEngineNames(prefs); - declinedEngineNames = getDeclinedEngineNames(prefs); - userSelectedEngines = getUserSelectedEngines(prefs); - userSelectedEnginesTimestamp = prefs.getLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, 0); - // We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON - // and we won't have it on construction. - // TODO: MetaGlobal, password, infoCollections. - } - - public void persistToPrefs() { - this.persistToPrefs(this.getPrefs()); - } - - private static String setToJSONObjectString(Set<String> set) { - ExtendedJSONObject o = new ExtendedJSONObject(); - for (String name : set) { - o.put(name, 0); - } - return o.toJSONString(); - } - - public void persistToPrefs(SharedPreferences prefs) { - Editor edit = prefs.edit(); - if (clusterURL == null) { - edit.remove(PREF_CLUSTER_URL); - } else { - edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString()); - } - if (syncID != null) { - edit.putString(PREF_SYNC_ID, syncID); - } - if (enabledEngineNames == null) { - edit.remove(PREF_ENABLED_ENGINE_NAMES); - } else { - edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames)); - } - if (declinedEngineNames == null || declinedEngineNames.isEmpty()) { - edit.remove(PREF_DECLINED_ENGINE_NAMES); - } else { - edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames)); - } - if (userSelectedEngines == null) { - edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC); - edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP); - } - // Don't bother saving userSelectedEngines - these should only be changed by - // SelectEnginesActivity. - edit.commit(); - // TODO: keys. - } - - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - - public CollectionKeys getCollectionKeys() { - return collectionKeys; - } - - public void setCollectionKeys(CollectionKeys k) { - collectionKeys = k; - } - - /** - * Return path to storage endpoint without trailing slash. - * - * @return storage endpoint without trailing slash. - */ - public String storageURL() { - return clusterURL + "/storage"; - } - - protected String infoBaseURL() { - return clusterURL + "/info/"; - } - - public String infoCollectionsURL() { - return infoBaseURL() + "collections"; - } - - public String infoConfigurationURL() { - return infoBaseURL() + "configuration"; - } - - public String infoCollectionCountsURL() { - return infoBaseURL() + "collection_counts"; - } - - public String metaURL() { - return storageURL() + "/meta/global"; - } - - public URI collectionURI(String collection) throws URISyntaxException { - return new URI(storageURL() + "/" + collection); - } - - public URI collectionURI(String collection, boolean full) throws URISyntaxException { - // Do it this way to make it easier to add more params later. - // It's pretty ugly, I'll grant. - boolean anyParams = full; - String uriParams = ""; - if (anyParams) { - StringBuilder params = new StringBuilder("?"); - if (full) { - params.append("full=1"); - } - uriParams = params.toString(); - } - String uri = storageURL() + "/" + collection + uriParams; - return new URI(uri); - } - - public URI wboURI(String collection, String id) throws URISyntaxException { - return new URI(storageURL() + "/" + collection + "/" + id); - } - - public URI keysURI() throws URISyntaxException { - return wboURI("crypto", "keys"); - } - - public URI getClusterURL() { - return clusterURL; - } - - public String getClusterURLString() { - if (clusterURL == null) { - return null; - } - return clusterURL.toASCIIString(); - } - - public void setClusterURL(URI u) { - this.clusterURL = u; - } - - /** - * Used for direct management of related prefs. - */ - public Editor getEditor() { - return this.getPrefs().edit(); - } - - /** - * We persist two different clients timestamps: our own record's, - * and the timestamp for the collection. - */ - public void persistServerClientRecordTimestamp(long timestamp) { - getEditor().putLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, timestamp).commit(); - } - - public long getPersistedServerClientRecordTimestamp() { - return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0L); - } - - public void persistServerClientsTimestamp(long timestamp) { - getEditor().putLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, timestamp).commit(); - } - - public long getPersistedServerClientsTimestamp() { - return getPrefs().getLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, 0L); - } - - public void persistLastMigrationSentinelCheckTimestamp(long timestamp) { - getEditor().putLong(SyncConfiguration.MIGRATION_SENTINEL_CHECK_TIMESTAMP, timestamp).commit(); - } - - public long getLastMigrationSentinelCheckTimestamp() { - return getPrefs().getLong(SyncConfiguration.MIGRATION_SENTINEL_CHECK_TIMESTAMP, 0L); - } - - public void purgeCryptoKeys() { - if (collectionKeys != null) { - collectionKeys.clear(); - } - persistedCryptoKeys().purge(); - } - - public void purgeMetaGlobal() { - metaGlobal = null; - persistedMetaGlobal().purge(); - } - - public PersistedCrypto5Keys persistedCryptoKeys() { - return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle); - } - - public PersistedMetaGlobal persistedMetaGlobal() { - return new PersistedMetaGlobal(getPrefs()); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java deleted file mode 100644 index 02ba118c5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfigurationException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -public class SyncConfigurationException extends SyncException { - private static final long serialVersionUID = 1107080177269358381L; - - @Override - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - syncResult.stats.numAuthExceptions++; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java deleted file mode 100644 index 5dc7b289f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConstants.java +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import org.mozilla.gecko.AppConstants; - -public class SyncConstants { - public static final String GLOBAL_LOG_TAG = "FxSync"; - public static final String SYNC_MAJOR_VERSION = "1"; - public static final String SYNC_MINOR_VERSION = "0"; - public static final String SYNC_VERSION_STRING = SYNC_MAJOR_VERSION + "." + - AppConstants.MOZ_APP_VERSION + "." + - SYNC_MINOR_VERSION; - - public static final String USER_AGENT = "Firefox AndroidSync " + - SYNC_VERSION_STRING + " (" + - AppConstants.MOZ_APP_UA_NAME + ")"; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java deleted file mode 100644 index ee0902568..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SyncResult; - -public abstract class SyncException extends Exception { - private static final long serialVersionUID = -6928990004393234738L; - - public SyncException() { - super(); - } - - public SyncException(final Throwable e) { - super(e); - } - - /** - * Update sync result statistics with information particular to this - * exception. - * - * @param globalSession - * current session, or null. - * @param syncResult - * Android sync result to update. - */ - public void updateStats(GlobalSession globalSession, SyncResult syncResult) { - // Assume storage error. - // TODO: this logic is overly simplistic. - syncResult.databaseError = true; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java deleted file mode 100644 index 2b08be9c4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java +++ /dev/null @@ -1,68 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import android.content.SharedPreferences.Editor; - -import org.mozilla.gecko.background.common.PrefsBranch; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; - -import java.io.IOException; - -public class SynchronizerConfiguration { - private static final String LOG_TAG = "SynczrConfiguration"; - - public String syncID; - public RepositorySessionBundle remoteBundle; - public RepositorySessionBundle localBundle; - - public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException { - this.load(config); - } - - public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) { - this.syncID = syncID; - this.remoteBundle = remoteBundle; - this.localBundle = localBundle; - } - - // This should get partly shuffled back into SyncConfiguration, I think. - public void load(PrefsBranch config) throws NonObjectJSONException, IOException { - if (config == null) { - throw new IllegalArgumentException("config cannot be null."); - } - String remoteJSON = config.getString("remote", null); - String localJSON = config.getString("local", null); - RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON); - RepositorySessionBundle lB = new RepositorySessionBundle(localJSON); - if (remoteJSON == null) { - rB.setTimestamp(0); - } - if (localJSON == null) { - lB.setTimestamp(0); - } - syncID = config.getString("syncID", null); - remoteBundle = rB; - localBundle = lB; - Logger.debug(LOG_TAG, "Loaded SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle); - } - - public void persist(PrefsBranch config) { - if (config == null) { - throw new IllegalArgumentException("config cannot be null."); - } - String jsonRemote = remoteBundle.toJSONString(); - String jsonLocal = localBundle.toJSONString(); - Editor editor = config.edit(); - editor.putString("remote", jsonRemote); - editor.putString("local", jsonLocal); - editor.putString("syncID", syncID); - - // Synchronous. - editor.commit(); - Logger.debug(LOG_TAG, "Persisted SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java deleted file mode 100644 index 7f2029566..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ThreadPool.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class ThreadPool { - public static ExecutorService executorService = Executors.newCachedThreadPool(); - public static void run(Runnable runnable) { - executorService.submit(runnable); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java deleted file mode 100644 index e5771452c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class UnexpectedJSONException extends Exception { - private static final long serialVersionUID = 4797570033096443169L; - - public UnexpectedJSONException(String detailMessage) { - super(detailMessage); - } - - public UnexpectedJSONException(Throwable throwable) { - super(throwable); - } - - public static class BadRequiredFieldJSONException extends UnexpectedJSONException { - private static final long serialVersionUID = -9207736984784497612L; - - public BadRequiredFieldJSONException(String string) { - super(string); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java deleted file mode 100644 index e2350095e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnknownSynchronizerConfigurationVersionException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -public class UnknownSynchronizerConfigurationVersionException extends - SyncConfigurationException { - public int badVersion; - private static final long serialVersionUID = -8497255862099517395L; - - public UnknownSynchronizerConfigurationVersionException(int version) { - super(); - badVersion = version; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java deleted file mode 100644 index ef8859b4a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java +++ /dev/null @@ -1,575 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.URLDecoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; -import java.util.concurrent.Executor; - -import org.json.simple.JSONArray; -import org.mozilla.apache.commons.codec.binary.Base32; -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.nativecode.NativeCrypto; -import org.mozilla.gecko.sync.setup.Constants; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; - -public class Utils { - - private static final String LOG_TAG = "Utils"; - - private static final SecureRandom sharedSecureRandom = new SecureRandom(); - - // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29> - public static final int SHARED_PREFERENCES_MODE = 0; - - public static String generateGuid() { - byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false); - return new String(encodedBytes).replace("+", "-").replace("/", "_"); - } - - /** - * Helper to generate secure random bytes. - * - * @param length - * Number of bytes to generate. - */ - public static byte[] generateRandomBytes(int length) { - byte[] bytes = new byte[length]; - sharedSecureRandom.nextBytes(bytes); - return bytes; - } - - /** - * Helper to generate a random integer in a specified range. - * - * @param r - * Generate an integer between 0 and r-1 inclusive. - */ - public static BigInteger generateBigIntegerLessThan(BigInteger r) { - int maxBytes = (int) Math.ceil(((double) r.bitLength()) / 8); - BigInteger randInt = new BigInteger(generateRandomBytes(maxBytes)); - return randInt.mod(r); - } - - /** - * Helper to convert a byte array to a hex-encoded string - */ - public static String byte2Hex(final byte[] b) { - return byte2Hex(b, 2 * b.length); - } - - public static String byte2Hex(final byte[] b, int hexLength) { - final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength)); - String stmp; - - for (int n = 0; n < hexLength - 2*b.length; n++) { - hs.append("0"); - } - - for (int n = 0; n < b.length; n++) { - stmp = Integer.toHexString(b[n] & 0XFF); - - if (stmp.length() == 1) { - hs.append("0"); - } - hs.append(stmp); - } - - return hs.toString(); - } - - public static byte[] concatAll(byte[] first, byte[]... rest) { - int totalLength = first.length; - for (byte[] array : rest) { - totalLength += array.length; - } - - byte[] result = new byte[totalLength]; - int offset = first.length; - - System.arraycopy(first, 0, result, 0, offset); - - for (byte[] array : rest) { - System.arraycopy(array, 0, result, offset, array.length); - offset += array.length; - } - return result; - } - - /** - * Utility for Base64 decoding. Should ensure that the correct - * Apache Commons version is used. - * - * @param base64 - * An input string. Will be decoded as UTF-8. - * @return - * A byte array of decoded values. - * @throws UnsupportedEncodingException - * Should not occur. - */ - public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException { - return Base64.decodeBase64(base64.getBytes("UTF-8")); - } - - public static byte[] decodeFriendlyBase32(String base32) { - Base32 converter = new Base32(); - final String translated = base32.replace('8', 'l').replace('9', 'o'); - return converter.decode(translated.toUpperCase(Locale.US)); - } - - public static byte[] hex2Byte(String str, int byteLength) { - byte[] second = hex2Byte(str); - if (second.length >= byteLength) { - return second; - } - // New Java arrays are zeroed: - // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 - byte[] first = new byte[byteLength - second.length]; - return Utils.concatAll(first, second); - } - - public static byte[] hex2Byte(String str) { - if (str.length() % 2 == 1) { - str = "0" + str; - } - - byte[] bytes = new byte[str.length() / 2]; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16); - } - return bytes; - } - - public static String millisecondsToDecimalSecondsString(long ms) { - return millisecondsToDecimalSeconds(ms).toString(); - } - - // For dumping into JSON without quotes. - public static BigDecimal millisecondsToDecimalSeconds(long ms) { - return new BigDecimal(ms).movePointLeft(3); - } - - // This lives until Bug 708956 lands, and we don't have to do it any more. - public static long decimalSecondsToMilliseconds(String decimal) { - try { - return new BigDecimal(decimal).movePointRight(3).longValue(); - } catch (Exception e) { - return -1; - } - } - - // Oh, Java. - public static long decimalSecondsToMilliseconds(Double decimal) { - // Truncates towards 0. - return (long)(decimal * 1000); - } - - public static long decimalSecondsToMilliseconds(Long decimal) { - return decimal * 1000; - } - - public static long decimalSecondsToMilliseconds(Integer decimal) { - return (decimal * 1000); - } - - public static byte[] sha256(byte[] in) - throws NoSuchAlgorithmException { - MessageDigest sha1 = MessageDigest.getInstance("SHA-256"); - return sha1.digest(in); - } - - protected static byte[] sha1(final String utf8) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - final byte[] bytes = utf8.getBytes("UTF-8"); - try { - return NativeCrypto.sha1(bytes); - } catch (final LinkageError e) { - // This will throw UnsatisifiedLinkError (missing mozglue) the first time it is called, and - // ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this - // is called; LinkageError is their common ancestor. - Logger.warn(LOG_TAG, "Got throwable stretching password using native sha1 implementation; " + - "ignoring and using Java implementation.", e); - final MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - return sha1.digest(utf8.getBytes("UTF-8")); - } - } - - protected static String sha1Base32(final String utf8) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - return new Base32().encodeAsString(sha1(utf8)).toLowerCase(Locale.US); - } - - /** - * If we encounter characters not allowed by the API (as found for - * instance in an email address), hash the value. - * @param account - * An account string. - * @return - * An acceptable string. - * @throws UnsupportedEncodingException - * @throws NoSuchAlgorithmException - */ - public static String usernameFromAccount(final String account) throws NoSuchAlgorithmException, UnsupportedEncodingException { - if (account == null || account.equals("")) { - throw new IllegalArgumentException("No account name provided."); - } - if (account.matches("^[A-Za-z0-9._-]+$")) { - return account.toLowerCase(Locale.US); - } - return sha1Base32(account.toLowerCase(Locale.US)); - } - - public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - String prefsPath = getPrefsPath(product, username, serverURL, profile, version); - return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE); - } - - /** - * Get shared preferences path for a Sync account. - * - * @param product the Firefox Sync product package name (like "org.mozilla.firefox"). - * @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>. - * @param serverURL the Sync account server URL. - * @param profile the Firefox profile name. - * @param version the version of preferences to reference. - * @return the path. - * @throws NoSuchAlgorithmException - * @throws UnsupportedEncodingException - */ - public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username)); - - if (version <= 0) { - return "sync.prefs." + encodedAccount; - } else { - final String sanitizedProduct = product.replace('.', '!').replace(' ', '!'); - return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version; - } - } - - public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) { - ArrayList<String> bucket = map.get(index); - if (bucket == null) { - bucket = new ArrayList<String>(); - } - bucket.add(value); - map.put(index, bucket); - } - - /** - * Yes, an equality method that's null-safe. - */ - private static boolean same(Object a, Object b) { - if (a == b) { - return true; - } - if (a == null || b == null) { - return false; // If both null, case above applies. - } - return a.equals(b); - } - - /** - * Return true if the two arrays are both null, or are both arrays - * containing the same elements in the same order. - */ - public static boolean sameArrays(JSONArray a, JSONArray b) { - if (a == b) { - return true; - } - if (a == null || b == null) { - return false; - } - final int size = a.size(); - if (size != b.size()) { - return false; - } - for (int i = 0; i < size; ++i) { - if (!same(a.get(i), b.get(i))) { - return false; - } - } - return true; - } - - /** - * Takes a URI, extracting URI components. - * @param scheme the URI scheme on which to match. - */ - @SuppressWarnings("deprecation") - public static Map<String, String> extractURIComponents(String scheme, String uri) { - if (uri.indexOf(scheme) != 0) { - throw new IllegalArgumentException("URI scheme does not match: " + scheme); - } - - // Do this the hard way to avoid taking a large dependency on - // HttpClient or getting all regex-tastic. - String components = uri.substring(scheme.length()); - HashMap<String, String> out = new HashMap<String, String>(); - String[] parts = components.split("&"); - for (int i = 0; i < parts.length; ++i) { - String part = parts[i]; - if (part.length() == 0) { - continue; - } - String[] pair = part.split("=", 2); - switch (pair.length) { - case 0: - continue; - case 1: - out.put(URLDecoder.decode(pair[0]), null); - break; - case 2: - out.put(URLDecoder.decode(pair[0]), URLDecoder.decode(pair[1])); - break; - } - } - return out; - } - - // Because TextUtils.join is not stubbed. - public static String toDelimitedString(String delimiter, Collection<? extends Object> items) { - if (items == null || items.size() == 0) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - int i = 0; - int c = items.size(); - for (Object object : items) { - sb.append(object.toString()); - if (++i < c) { - sb.append(delimiter); - } - } - return sb.toString(); - } - - public static String toCommaSeparatedString(Collection<? extends Object> items) { - return toDelimitedString(", ", items); - } - - /** - * Names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). - * - * @param knownStageNames collection of known stage names (set ALL above). - * @param toSync set SYNC above, or <code>null</code> to sync all known stages. - * @param toSkip set SKIP above, or <code>null</code> to not skip any stages. - * @return stage names. - */ - public static Collection<String> getStagesToSync(final Collection<String> knownStageNames, Collection<String> toSync, Collection<String> toSkip) { - if (toSkip == null) { - toSkip = new HashSet<String>(); - } else { - toSkip = new HashSet<String>(toSkip); - } - - if (toSync == null) { - toSync = new HashSet<String>(knownStageNames); - } else { - toSync = new HashSet<String>(toSync); - } - toSync.retainAll(knownStageNames); - toSync.removeAll(toSkip); - return toSync; - } - - /** - * Get names of stages to sync: (ALL intersect SYNC) intersect (ALL minus SKIP). - * - * @param knownStageNames collection of known stage names (set ALL above). - * @param extras - * a <code>Bundle</code> instance (possibly null) optionally containing keys - * <code>EXTRAS_KEY_STAGES_TO_SYNC</code> (set SYNC above) and - * <code>EXTRAS_KEY_STAGES_TO_SKIP</code> (set SKIP above). - * @return stage names. - */ - public static Collection<String> getStagesToSyncFromBundle(final Collection<String> knownStageNames, final Bundle extras) { - if (extras == null) { - return knownStageNames; - } - String toSyncString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SYNC); - String toSkipString = extras.getString(Constants.EXTRAS_KEY_STAGES_TO_SKIP); - if (toSyncString == null && toSkipString == null) { - return knownStageNames; - } - - ArrayList<String> toSync = null; - ArrayList<String> toSkip = null; - if (toSyncString != null) { - try { - toSync = new ArrayList<String>(new ExtendedJSONObject(toSyncString).keySet()); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e); - } - } - if (toSkipString != null) { - try { - toSkip = new ArrayList<String>(new ExtendedJSONObject(toSkipString).keySet()); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e); - } - } - - Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) + - "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'."); - return getStagesToSync(knownStageNames, toSync, toSkip); - } - - /** - * Put names of stages to sync and to skip into sync extras bundle. - * - * @param bundle - * a <code>Bundle</code> instance (possibly null). - * @param stagesToSync - * collection of stage names to sync: key - * <code>EXTRAS_KEY_STAGES_TO_SYNC</code>; ignored if <code>null</code>. - * @param stagesToSkip - * collection of stage names to skip: key - * <code>EXTRAS_KEY_STAGES_TO_SKIP</code>; ignored if <code>null</code>. - */ - public static void putStageNamesToSync(final Bundle bundle, final String[] stagesToSync, final String[] stagesToSkip) { - if (bundle == null) { - return; - } - - if (stagesToSync != null) { - ExtendedJSONObject o = new ExtendedJSONObject(); - for (String stageName : stagesToSync) { - o.put(stageName, 0); - } - bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SYNC, o.toJSONString()); - } - - if (stagesToSkip != null) { - ExtendedJSONObject o = new ExtendedJSONObject(); - for (String stageName : stagesToSkip) { - o.put(stageName, 0); - } - bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString()); - } - } - - /** - * Read contents of file as a string. - * - * @param context Android context. - * @param filename name of file to read; must not be null. - * @return <code>String</code> instance. - */ - public static String readFile(final Context context, final String filename) { - if (filename == null) { - throw new IllegalArgumentException("Passed null filename in readFile."); - } - - FileInputStream fis = null; - InputStreamReader isr = null; - BufferedReader br = null; - - try { - fis = context.openFileInput(filename); - isr = new InputStreamReader(fis); - br = new BufferedReader(isr); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line); - } - return sb.toString(); - } catch (Exception e) { - return null; - } finally { - if (isr != null) { - try { - isr.close(); - } catch (IOException e) { - // Ignore. - } - } - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - // Ignore. - } - } - } - } - - /** - * Format a duration as a string, like "0.56 seconds". - * - * @param startMillis start time in milliseconds. - * @param endMillis end time in milliseconds. - * @return formatted string. - */ - public static String formatDuration(long startMillis, long endMillis) { - final long duration = endMillis - startMillis; - return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000); - } - - /** - * This will take a string containing a UTF-8 representation of a UTF-8 - * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1"). - * - * This is the format produced by desktop Firefox when exchanging credentials - * containing non-ASCII characters. - */ - public static String decodeUTF8(final String in) throws UnsupportedEncodingException { - final int length = in.length(); - final byte[] asciiBytes = new byte[length]; - for (int i = 0; i < length; ++i) { - asciiBytes[i] = (byte) in.codePointAt(i); - } - return new String(asciiBytes, "UTF-8"); - } - - /** - * Replace "foo@bar.com" with "XXX@XXX.XXX". - */ - public static String obfuscateEmail(final String in) { - return in.replaceAll("[^@\\.]", "X"); - } - - public static void throwIfNull(Object... objects) { - for (Object object : objects) { - if (object == null) { - throw new IllegalArgumentException("object must not be null"); - } - } - } - - public static Executor newSynchronousExecutor() { - return new Executor() { - @Override - public void execute(Runnable runnable) { - runnable.run(); - } - }; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java deleted file mode 100644 index a8d0483c9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoException.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import java.security.GeneralSecurityException; - -public class CryptoException extends Exception { - public GeneralSecurityException cause; - public CryptoException(GeneralSecurityException e) { - this(); - this.cause = e; - } - public CryptoException() { - - } - private static final long serialVersionUID = -5219310989960126830L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java deleted file mode 100644 index 355571c6a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/CryptoInfo.java +++ /dev/null @@ -1,232 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.Mac; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import org.mozilla.apache.commons.codec.binary.Base64; - -/* - * All info in these objects should be decoded (i.e. not BaseXX encoded). - */ -public class CryptoInfo { - private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; - private static final String KEY_ALGORITHM_SPEC = "AES"; - - private byte[] message; - private byte[] iv; - private byte[] hmac; - private KeyBundle keys; - - /** - * Return a CryptoInfo with given plaintext encrypted using given keys. - */ - public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException { - CryptoInfo info = new CryptoInfo(plaintextBytes, keys); - info.encrypt(); - return info; - } - - /** - * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector. - */ - public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException { - CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys); - info.encrypt(); - return info; - } - - /** - * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates. - */ - public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException { - CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys); - info.decrypt(); - return info; - } - - /* - * Constructor typically used when encrypting. - */ - public CryptoInfo(byte[] message, KeyBundle keys) { - this.setMessage(message); - this.setKeys(keys); - } - - /* - * Constructor typically used when decrypting. - */ - public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) { - this.setMessage(message); - this.setIV(iv); - this.setHMAC(hmac); - this.setKeys(keys); - } - - public byte[] getMessage() { - return message; - } - - public void setMessage(byte[] message) { - this.message = message; - } - - public byte[] getIV() { - return iv; - } - - public void setIV(byte[] iv) { - this.iv = iv; - } - - public byte[] getHMAC() { - return hmac; - } - - public void setHMAC(byte[] hmac) { - this.hmac = hmac; - } - - public KeyBundle getKeys() { - return keys; - } - - public void setKeys(KeyBundle keys) { - this.keys = keys; - } - - /* - * Generate HMAC for given cipher text. - */ - public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException { - Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey()); - return hmacHasher.doFinal(Base64.encodeBase64(message)); - } - - /* - * Return true if generated HMAC is the same as the specified HMAC. - */ - public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException { - byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys()); - byte[] expectedHMAC = getHMAC(); - return Arrays.equals(generatedHMAC, expectedHMAC); - } - - /** - * Performs functionality common to both encryption and decryption. - * - * @param cipher - * @param inputMessage non-BaseXX-encoded message - * @return encrypted/decrypted message - * @throws CryptoException - */ - private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage) - throws CryptoException { - byte[] outputMessage = null; - try { - outputMessage = cipher.doFinal(inputMessage); - } catch (IllegalBlockSizeException | BadPaddingException e) { - throw new CryptoException(e); - } - return outputMessage; - } - - /** - * Encrypt a CryptoInfo in-place. - * - * @throws CryptoException - */ - public void encrypt() throws CryptoException { - - Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); - try { - byte[] encryptionKey = getKeys().getEncryptionKey(); - SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); - - // If no IV is provided, we allow the cipher to provide one. - if (getIV() == null || getIV().length == 0) { - cipher.init(Cipher.ENCRYPT_MODE, spec); - } else { - cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV())); - } - } catch (GeneralSecurityException ex) { - throw new CryptoException(ex); - } - - // Encrypt. - byte[] encryptedBytes = commonCrypto(cipher, getMessage()); - byte[] iv = cipher.getIV(); - - byte[] hmac; - // Generate HMAC. - try { - hmac = generatedHMACFor(encryptedBytes, keys); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new CryptoException(e); - } - - // Update in place. keys is already set. - this.setHMAC(hmac); - this.setIV(iv); - this.setMessage(encryptedBytes); - } - - /** - * Decrypt a CryptoInfo in-place. - * - * @throws CryptoException - */ - public void decrypt() throws CryptoException { - - // Check HMAC. - try { - if (!generatedHMACIsHMAC()) { - throw new HMACVerificationException(); - } - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new CryptoException(e); - } - - Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); - try { - byte[] encryptionKey = getKeys().getEncryptionKey(); - SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); - cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV())); - } catch (GeneralSecurityException ex) { - throw new CryptoException(ex); - } - byte[] decryptedBytes = commonCrypto(cipher, getMessage()); - byte[] iv = cipher.getIV(); - - // Update in place. keys is already set. - this.setHMAC(null); - this.setIV(iv); - this.setMessage(decryptedBytes); - } - - /** - * Helper to get a Cipher object. - * - * @param transformation The type of Cipher to get. - */ - private static Cipher getCipher(String transformation) throws CryptoException { - try { - return Cipher.getInstance(transformation); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new CryptoException(e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java deleted file mode 100644 index 16c0d8147..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HKDF.java +++ /dev/null @@ -1,128 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.mozilla.gecko.sync.Utils; - -/* - * A standards-compliant implementation of RFC 5869 - * for HMAC-based Key Derivation Function. - * HMAC uses HMAC SHA256 standard. - */ -public class HKDF { - public static String HMAC_ALGORITHM = "hmacSHA256"; - - /** - * Used for conversion in cases in which you *know* the encoding exists. - */ - public static final byte[] bytes(String in) { - try { - return in.getBytes("UTF-8"); - } catch (java.io.UnsupportedEncodingException e) { - return null; - } - } - - public static final int BLOCKSIZE = 256 / 8; - public static final byte[] HMAC_INPUT = bytes("Sync-AES_256_CBC-HMAC256"); - - /* - * Step 1 of RFC 5869 - * Get sha256HMAC Bytes - * Input: salt (message), IKM (input keyring material) - * Output: PRK (pseudorandom key) - */ - public static byte[] hkdfExtract(byte[] salt, byte[] IKM) throws NoSuchAlgorithmException, InvalidKeyException { - return digestBytes(IKM, makeHMACHasher(salt)); - } - - /* - * Step 2 of RFC 5869. - * Input: PRK from step 1, info, length. - * Output: OKM (output keyring material). - */ - public static byte[] hkdfExpand(byte[] prk, byte[] info, int len) throws NoSuchAlgorithmException, InvalidKeyException { - Mac hmacHasher = makeHMACHasher(prk); - - byte[] T = {}; - byte[] Tn = {}; - - int iterations = (int) Math.ceil(((double)len) / (BLOCKSIZE)); - for (int i = 0; i < iterations; i++) { - Tn = digestBytes(Utils.concatAll(Tn, info, Utils.hex2Byte(Integer.toHexString(i + 1))), - hmacHasher); - T = Utils.concatAll(T, Tn); - } - - byte[] result = new byte[len]; - System.arraycopy(T, 0, result, 0, len); - return result; - } - - /* - * Make HMAC key - * Input: key (salt) - * Output: Key HMAC-Key - */ - public static Key makeHMACKey(byte[] key) { - if (key.length == 0) { - key = new byte[BLOCKSIZE]; - } - return new SecretKeySpec(key, HMAC_ALGORITHM); - } - - /* - * Make an HMAC hasher - * Input: Key hmacKey - * Ouput: An HMAC Hasher - */ - public static Mac makeHMACHasher(byte[] key) throws NoSuchAlgorithmException, InvalidKeyException { - Mac hmacHasher = null; - hmacHasher = Mac.getInstance(HMAC_ALGORITHM); - - // If Mac.getInstance doesn't throw NoSuchAlgorithmException, hmacHasher is - // non-null. - assert(hmacHasher != null); - - hmacHasher.init(makeHMACKey(key)); - return hmacHasher; - } - - /* - * Hash bytes with given hasher - * Input: message to hash, HMAC hasher - * Output: hashed byte[]. - */ - public static byte[] digestBytes(byte[] message, Mac hasher) { - hasher.update(message); - byte[] ret = hasher.doFinal(); - hasher.reset(); - return ret; - } - - public static byte[] derive(byte[] skm, byte[] xts, byte[] ctxInfo, int dkLen) throws InvalidKeyException, NoSuchAlgorithmException { - return hkdfExpand(hkdfExtract(xts, skm), ctxInfo, dkLen); - } - - public static void deriveMany(byte[] skm, byte[] xts, byte[] ctxInfo, byte[]... keys) throws InvalidKeyException, NoSuchAlgorithmException { - int length = 0; - for (byte[] key : keys) { - length += key.length; - } - byte[] derived = hkdfExpand(hkdfExtract(xts, skm), ctxInfo, length); - int offset = 0; - for (byte[] key : keys) { - System.arraycopy(derived, offset, key, 0, key.length); - offset += key.length; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java deleted file mode 100644 index f33babd52..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/HMACVerificationException.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -public class HMACVerificationException extends CryptoException { - private static final long serialVersionUID = 1235311303567074897L; - public HMACVerificationException() { - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java deleted file mode 100644 index 2063b1e32..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/KeyBundle.java +++ /dev/null @@ -1,135 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import java.io.UnsupportedEncodingException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.KeyGenerator; -import javax.crypto.Mac; - -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.sync.Utils; - -public class KeyBundle { - private static final String KEY_ALGORITHM_SPEC = "AES"; - private static final int KEY_SIZE = 256; - - private byte[] encryptionKey; - private byte[] hmacKey; - - // These are the same for every sync key bundle. - private static final byte[] EMPTY_BYTES = {}; - private static final byte[] ENCR_INPUT_BYTES = {1}; - private static final byte[] HMAC_INPUT_BYTES = {2}; - - /* - * Mozilla's use of HKDF for getting keys from the Sync Key string. - * - * We do exactly 2 HKDF iterations and make the first iteration the - * encryption key and the second iteration the HMAC key. - * - */ - public KeyBundle(String username, String base32SyncKey) throws CryptoException { - if (base32SyncKey == null) { - throw new IllegalArgumentException("No sync key provided."); - } - if (username == null || username.equals("")) { - throw new IllegalArgumentException("No username provided."); - } - // Hash appropriately. - try { - username = Utils.usernameFromAccount(username); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new IllegalArgumentException("Invalid username."); - } - - byte[] syncKey = Utils.decodeFriendlyBase32(base32SyncKey); - byte[] user = username.getBytes(); - - Mac hmacHasher; - try { - hmacHasher = HKDF.makeHMACHasher(syncKey); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new CryptoException(e); - } - assert(hmacHasher != null); // If makeHMACHasher doesn't throw, then hmacHasher is non-null. - - byte[] encrBytes = Utils.concatAll(EMPTY_BYTES, HKDF.HMAC_INPUT, user, ENCR_INPUT_BYTES); - byte[] encrKey = HKDF.digestBytes(encrBytes, hmacHasher); - byte[] hmacBytes = Utils.concatAll(encrKey, HKDF.HMAC_INPUT, user, HMAC_INPUT_BYTES); - - this.hmacKey = HKDF.digestBytes(hmacBytes, hmacHasher); - this.encryptionKey = encrKey; - } - - public KeyBundle(byte[] encryptionKey, byte[] hmacKey) { - this.setEncryptionKey(encryptionKey); - this.setHMACKey(hmacKey); - } - - /** - * Make a KeyBundle with the specified base64-encoded keys. - * - * @return A KeyBundle with the specified keys. - */ - public static KeyBundle fromBase64EncodedKeys(String base64EncryptionKey, String base64HmacKey) throws UnsupportedEncodingException { - return new KeyBundle(Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")), - Base64.decodeBase64(base64HmacKey.getBytes("UTF-8"))); - } - - /** - * Make a KeyBundle with two random 256 bit keys (encryption and HMAC). - * - * @return A KeyBundle with random keys. - */ - public static KeyBundle withRandomKeys() throws CryptoException { - KeyGenerator keygen; - try { - keygen = KeyGenerator.getInstance(KEY_ALGORITHM_SPEC); - } catch (NoSuchAlgorithmException e) { - throw new CryptoException(e); - } - - keygen.init(KEY_SIZE); - byte[] encryptionKey = keygen.generateKey().getEncoded(); - byte[] hmacKey = keygen.generateKey().getEncoded(); - - return new KeyBundle(encryptionKey, hmacKey); - } - - public byte[] getEncryptionKey() { - return encryptionKey; - } - - public void setEncryptionKey(byte[] encryptionKey) { - this.encryptionKey = encryptionKey; - } - - public byte[] getHMACKey() { - return hmacKey; - } - - public void setHMACKey(byte[] hmacKey) { - this.hmacKey = hmacKey; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof KeyBundle)) { - return false; - } - KeyBundle other = (KeyBundle) o; - return Arrays.equals(other.encryptionKey, this.encryptionKey) && - Arrays.equals(other.hmacKey, this.hmacKey); - } - - @Override - public int hashCode() { - throw new UnsupportedOperationException("No hashCode for KeyBundle."); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java deleted file mode 100644 index 8add1cf11..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/MissingCryptoInputException.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -public class MissingCryptoInputException extends CryptoException { - private static final long serialVersionUID = 5334412407012972445L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java deleted file mode 100644 index 00e0f8b18..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/NoKeyBundleException.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -public class NoKeyBundleException extends CryptoException { - private static final long serialVersionUID = -6627154503154040915L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java deleted file mode 100644 index 636b2105c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PBKDF2.java +++ /dev/null @@ -1,78 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import java.security.GeneralSecurityException; -import java.util.Arrays; - -import javax.crypto.Mac; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; - -public class PBKDF2 { - public static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen) - throws GeneralSecurityException { - final String algorithm = "HmacSHA256"; - SecretKeySpec keyspec = new SecretKeySpec(password, algorithm); - Mac prf = Mac.getInstance(algorithm); - prf.init(keyspec); - - int hLen = prf.getMacLength(); - - byte U_r[] = new byte[hLen]; - byte U_i[] = new byte[salt.length + 4]; - byte scratch[] = new byte[hLen]; - - int l = Math.max(dkLen, hLen); - int r = dkLen - (l - 1) * hLen; - byte T[] = new byte[l * hLen]; - int ti_offset = 0; - for (int i = 1; i <= l; i++) { - Arrays.fill(U_r, (byte) 0); - F(T, ti_offset, prf, salt, c, i, U_r, U_i, scratch); - ti_offset += hLen; - } - - if (r < hLen) { - // Incomplete last block. - byte DK[] = new byte[dkLen]; - System.arraycopy(T, 0, DK, 0, dkLen); - return DK; - } - - return T; - } - - private static void F(byte[] dest, int offset, Mac prf, byte[] S, int c, int blockIndex, byte U_r[], byte U_i[], byte[] scratch) - throws ShortBufferException, IllegalStateException { - final int hLen = prf.getMacLength(); - - // U0 = S || INT (i); - System.arraycopy(S, 0, U_i, 0, S.length); - INT(U_i, S.length, blockIndex); - - for (int i = 0; i < c; i++) { - prf.update(U_i); - prf.doFinal(scratch, 0); - U_i = scratch; - xor(U_r, U_i); - } - - System.arraycopy(U_r, 0, dest, offset, hLen); - } - - private static void xor(byte[] dest, byte[] src) { - for (int i = 0; i < dest.length; i++) { - dest[i] ^= src[i]; - } - } - - private static void INT(byte[] dest, int offset, int i) { - dest[offset + 0] = (byte) (i / (256 * 256 * 256)); - dest[offset + 1] = (byte) (i / (256 * 256)); - dest[offset + 2] = (byte) (i / (256)); - dest[offset + 3] = (byte) (i); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java deleted file mode 100644 index 4dba4f258..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/crypto/PersistedCrypto5Keys.java +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.crypto; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CollectionKeys; -import org.mozilla.gecko.sync.CryptoRecord; - -import android.content.SharedPreferences; - -public class PersistedCrypto5Keys { - public static final String LOG_TAG = "PersistedC5Keys"; - - public static final String CRYPTO5_KEYS_SERVER_RESPONSE_BODY = "crypto5KeysServerResponseBody"; - public static final String CRYPTO5_KEYS_LAST_MODIFIED = "crypto5KeysLastModified"; - - protected SharedPreferences prefs; - protected KeyBundle syncKeyBundle; - - public PersistedCrypto5Keys(SharedPreferences prefs, KeyBundle syncKeyBundle) { - if (syncKeyBundle == null) { - throw new IllegalArgumentException("Null syncKeyBundle passed in to PersistedCrypto5Keys constructor."); - } - this.prefs = prefs; - this.syncKeyBundle = syncKeyBundle; - } - - /** - * Get persisted crypto/keys. - * <p> - * crypto/keys is fetched from an encrypted JSON-encoded <code>CryptoRecord</code>. - * - * @return A <code>CollectionKeys</code> instance or <code>null</code> if none - * is currently persisted. - */ - public CollectionKeys keys() { - String keysJSON = prefs.getString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, null); - if (keysJSON == null) { - return null; - } - try { - CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(keysJSON); - CollectionKeys keys = new CollectionKeys(); - keys.setKeyPairsFromWBO(cryptoRecord, syncKeyBundle); - return keys; - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception decrypting persisted crypto/keys.", e); - return null; - } - } - - /** - * Persist crypto/keys. - * <p> - * crypto/keys is stored as an encrypted JSON-encoded <code>CryptoRecord</code>. - * - * @param keys - * The <code>CollectionKeys</code> object to persist, which should - * have the same default key bundle as the sync key bundle. - */ - public void persistKeys(CollectionKeys keys) { - if (keys == null) { - Logger.debug(LOG_TAG, "Clearing persisted crypto/keys."); - prefs.edit().remove(CRYPTO5_KEYS_SERVER_RESPONSE_BODY).commit(); - return; - } - try { - CryptoRecord cryptoRecord = keys.asCryptoRecord(); - cryptoRecord.keyBundle = syncKeyBundle; - cryptoRecord.encrypt(); - String keysJSON = cryptoRecord.toJSONString(); - Logger.debug(LOG_TAG, "Persisting crypto/keys."); - prefs.edit().putString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, keysJSON).commit(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception encrypting while persisting crypto/keys.", e); - } - } - - public boolean persistedKeysExist() { - return lastModified() > 0; - } - - public long lastModified() { - return prefs.getLong(CRYPTO5_KEYS_LAST_MODIFIED, -1); - } - - public void persistLastModified(long lastModified) { - if (lastModified <= 0) { - Logger.debug(LOG_TAG, "Clearing persisted crypto/keys last modified timestamp."); - prefs.edit().remove(CRYPTO5_KEYS_LAST_MODIFIED).commit(); - return; - } - Logger.debug(LOG_TAG, "Persisting crypto/keys last modified timestamp " + lastModified + "."); - prefs.edit().putLong(CRYPTO5_KEYS_LAST_MODIFIED, lastModified).commit(); - } - - public void purge() { - persistLastModified(-1); - persistKeys(null); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java deleted file mode 100644 index 07e9179f0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/ClientsDataDelegate.java +++ /dev/null @@ -1,28 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -public interface ClientsDataDelegate { - public String getAccountGUID(); - public String getDefaultClientName(); - public void setClientName(String clientName, long now); - public String getClientName(); - public void setClientsCount(int clientsCount); - public int getClientsCount(); - public boolean isLocalGUID(String guid); - public String getFormFactor(); - - /** - * The last time the client's data was modified in a way that should be - * reflected remotely. - * <p> - * Changing the client's name should be reflected remotely, while changing the - * clients count should not (since that data is only used to inform local - * policy.) - * - * @return timestamp in milliseconds. - */ - public long getLastModifiedTimestamp(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java deleted file mode 100644 index 2e5347061..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/FreshStartDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -public interface FreshStartDelegate { - void onFreshStart(); - void onFreshStartFailed(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java deleted file mode 100644 index 9829f5b34..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/GlobalSessionCallback.java +++ /dev/null @@ -1,49 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -import java.net.URI; - -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; - -public interface GlobalSessionCallback { - /** - * Request that no further syncs occur within the next `backoff` milliseconds. - * @param backoff a duration in milliseconds. - */ - void requestBackoff(long backoff); - - /** - * Called on a 401 HTTP response. - */ - void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL); - - - /** - * Called when an HTTP failure indicates that a software upgrade is required. - */ - void informUpgradeRequiredResponse(GlobalSession session); - - /** - * Called when a migration sentinel has been found and processed successfully. - * <p> - * This account should stop syncing immediately, and arrange to delete itself. - */ - void informMigrated(GlobalSession session); - - void handleAborted(GlobalSession globalSession, String reason); - void handleError(GlobalSession globalSession, Exception ex); - void handleSuccess(GlobalSession globalSession); - void handleStageCompleted(Stage currentState, GlobalSession globalSession); - - /** - * Called when a {@link GlobalSession} wants to know if it should continue - * to make storage requests. - * - * @return false if the session should make no further requests. - */ - boolean shouldBackOffStorage(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java deleted file mode 100644 index 90b73a33a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/JSONRecordFetchDelegate.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -/** - * A fairly generic delegate to handle fetches of single JSON object blobs, as - * provided by <code>info/configuration</code>, <code>info/collections</code> - * and <code>info/collection_counts</code>. - */ -public interface JSONRecordFetchDelegate { - public void handleSuccess(ExtendedJSONObject body); - public void handleFailure(SyncStorageResponse response); - public void handleError(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java deleted file mode 100644 index 0cd5ec732..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/KeyUploadDelegate.java +++ /dev/null @@ -1,21 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -public interface KeyUploadDelegate { - /** - * Called when keys have been successfully uploaded to the server. - * <p> - * The uploaded keys are intentionally not exposed. It is possible for two - * clients to simultaneously upload keys and for each client to conclude that - * its keys are current (since the server returned 200 on upload). To shorten - * the window wherein two such clients can race, all clients should upload and - * then immediately re-download the fetched keys. - * <p> - * See Bug 692700, Bug 693893. - */ - void onKeysUploaded(); - void onKeyUploadFailed(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java deleted file mode 100644 index 13854cb5a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/MetaGlobalDelegate.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -import org.mozilla.gecko.sync.MetaGlobal; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public interface MetaGlobalDelegate { - public void handleSuccess(MetaGlobal global, SyncStorageResponse response); - public void handleMissing(MetaGlobal global, SyncStorageResponse response); - public void handleFailure(SyncStorageResponse response); - public void handleError(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java deleted file mode 100644 index ef3565812..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/delegates/WipeServerDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.delegates; - -public interface WipeServerDelegate { - public void onWiped(long timestamp); - public void onWipeFailed(Exception e); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java deleted file mode 100644 index 79319aff5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepository.java +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.middleware; - -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.repositories.IdentityRecordFactory; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -/** - * Wrap an existing repository in middleware that encrypts and decrypts records - * passing through. - * - * @author rnewman - * - */ -public class Crypto5MiddlewareRepository extends MiddlewareRepository { - - public RecordFactory recordFactory = new IdentityRecordFactory(); - - public class Crypto5MiddlewareRepositorySessionCreationDelegate extends MiddlewareRepository.SessionCreationDelegate { - private final Crypto5MiddlewareRepository repository; - private final RepositorySessionCreationDelegate outerDelegate; - - public Crypto5MiddlewareRepositorySessionCreationDelegate(Crypto5MiddlewareRepository repository, RepositorySessionCreationDelegate outerDelegate) { - this.repository = repository; - this.outerDelegate = outerDelegate; - } - - @Override - public void onSessionCreateFailed(Exception ex) { - this.outerDelegate.onSessionCreateFailed(ex); - } - - @Override - public void onSessionCreated(RepositorySession session) { - // Do some work, then report success with the wrapping session. - Crypto5MiddlewareRepositorySession cryptoSession; - try { - // Synchronous, baby. - cryptoSession = new Crypto5MiddlewareRepositorySession(session, this.repository, recordFactory); - } catch (Exception ex) { - this.outerDelegate.onSessionCreateFailed(ex); - return; - } - this.outerDelegate.onSessionCreated(cryptoSession); - } - } - - public KeyBundle keyBundle; - private final Repository inner; - - public Crypto5MiddlewareRepository(Repository inner, KeyBundle keys) { - super(); - this.inner = inner; - this.keyBundle = keys; - } - @Override - public void createSession(RepositorySessionCreationDelegate delegate, Context context) { - Crypto5MiddlewareRepositorySessionCreationDelegate delegateWrapper = new Crypto5MiddlewareRepositorySessionCreationDelegate(this, delegate); - inner.createSession(delegateWrapper, context); - } - - @Override - public void clean(boolean success, RepositorySessionCleanDelegate delegate, - Context context) { - this.inner.clean(success, delegate, context); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java deleted file mode 100644 index 46de7a236..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/Crypto5MiddlewareRepositorySession.java +++ /dev/null @@ -1,172 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.middleware; - -import java.io.UnsupportedEncodingException; -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.crypto.CryptoException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -/** - * It's a RepositorySession that accepts Records as input, producing CryptoRecords - * for submission to a remote service. - * Takes a RecordFactory as a parameter. This is in charge of taking decrypted CryptoRecords - * as input and producing some expected kind of Record as output for local use. - * - * - - - - +------------------------------------+ - | Server11RepositorySession | - +-------------------------+----------+ - ^ | - | | - Encrypted CryptoRecords - | | - | v - +---------+--------------------------+ - | Crypto5MiddlewareRepositorySession | - +------------------------------------+ - ^ | - | | Decrypted CryptoRecords - | | - | +---------------+ - | | RecordFactory | - | +--+------------+ - | | - Local Record instances - | | - | v - +---------+--------------------------+ - | Local RepositorySession instance | - +------------------------------------+ - - - * @author rnewman - * - */ -public class Crypto5MiddlewareRepositorySession extends MiddlewareRepositorySession { - private final KeyBundle keyBundle; - private final RecordFactory recordFactory; - - public Crypto5MiddlewareRepositorySession(RepositorySession session, Crypto5MiddlewareRepository repository, RecordFactory recordFactory) { - super(session, repository); - this.keyBundle = repository.keyBundle; - this.recordFactory = recordFactory; - } - - public class DecryptingTransformingFetchDelegate implements RepositorySessionFetchRecordsDelegate { - private final RepositorySessionFetchRecordsDelegate next; - private final KeyBundle keyBundle; - private final RecordFactory recordFactory; - - DecryptingTransformingFetchDelegate(RepositorySessionFetchRecordsDelegate next, KeyBundle bundle, RecordFactory recordFactory) { - this.next = next; - this.keyBundle = bundle; - this.recordFactory = recordFactory; - } - - @Override - public void onFetchFailed(Exception ex, Record record) { - next.onFetchFailed(ex, record); - } - - @Override - public void onFetchedRecord(Record record) { - CryptoRecord r; - try { - r = (CryptoRecord) record; - } catch (ClassCastException e) { - next.onFetchFailed(e, record); - return; - } - r.keyBundle = keyBundle; - try { - r.decrypt(); - } catch (Exception e) { - next.onFetchFailed(e, r); - return; - } - Record transformed; - try { - transformed = this.recordFactory.createRecord(r); - } catch (Exception e) { - next.onFetchFailed(e, r); - return; - } - next.onFetchedRecord(transformed); - } - - @Override - public void onFetchCompleted(final long fetchEnd) { - next.onFetchCompleted(fetchEnd); - } - - @Override - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) { - // Synchronously perform *our* work, passing through appropriately. - RepositorySessionFetchRecordsDelegate deferredNext = next.deferredFetchDelegate(executor); - return new DecryptingTransformingFetchDelegate(deferredNext, keyBundle, recordFactory); - } - } - - private DecryptingTransformingFetchDelegate makeUnwrappingDelegate(RepositorySessionFetchRecordsDelegate inner) { - if (inner == null) { - throw new IllegalArgumentException("Inner delegate cannot be null!"); - } - return new DecryptingTransformingFetchDelegate(inner, this.keyBundle, this.recordFactory); - } - - @Override - public void fetchSince(long timestamp, - RepositorySessionFetchRecordsDelegate delegate) { - inner.fetchSince(timestamp, makeUnwrappingDelegate(delegate)); - } - - @Override - public void fetch(String[] guids, - RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException { - inner.fetch(guids, makeUnwrappingDelegate(delegate)); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - inner.fetchAll(makeUnwrappingDelegate(delegate)); - } - - @Override - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - // TODO: it remains to be seen how this will work. - inner.setStoreDelegate(delegate); - this.delegate = delegate; // So we can handle errors without involving inner. - } - - @Override - public void store(Record record) throws NoStoreDelegateException { - if (delegate == null) { - throw new NoStoreDelegateException(); - } - CryptoRecord rec = record.getEnvelope(); - rec.keyBundle = this.keyBundle; - try { - rec.encrypt(); - } catch (UnsupportedEncodingException | CryptoException e) { - delegate.onRecordStoreFailed(e, record.guid); - return; - } - // Allow the inner session to do delegate handling. - inner.store(rec); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java deleted file mode 100644 index d807aa5c0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.middleware; - -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -public abstract class MiddlewareRepository extends Repository { - - public abstract class SessionCreationDelegate implements - RepositorySessionCreationDelegate { - - // We call through to our inner repository, so we don't need our own - // deferral scheme. - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - return this; - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java deleted file mode 100644 index e14ef5226..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/middleware/MiddlewareRepositorySession.java +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.middleware; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; - -public abstract class MiddlewareRepositorySession extends RepositorySession { - private static final String LOG_TAG = "MiddlewareSession"; - protected final RepositorySession inner; - - public MiddlewareRepositorySession(RepositorySession innerSession, MiddlewareRepository repository) { - super(repository); - this.inner = innerSession; - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - inner.wipe(delegate); - } - - public class MiddlewareRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate { - - private final MiddlewareRepositorySession outerSession; - private final RepositorySessionBeginDelegate next; - - public MiddlewareRepositorySessionBeginDelegate(MiddlewareRepositorySession outerSession, RepositorySessionBeginDelegate next) { - this.outerSession = outerSession; - this.next = next; - } - - @Override - public void onBeginFailed(Exception ex) { - next.onBeginFailed(ex); - } - - @Override - public void onBeginSucceeded(RepositorySession session) { - next.onBeginSucceeded(outerSession); - } - - @Override - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) { - final RepositorySessionBeginDelegate deferred = next.deferredBeginDelegate(executor); - return new RepositorySessionBeginDelegate() { - @Override - public void onBeginSucceeded(RepositorySession session) { - if (inner != session) { - Logger.warn(LOG_TAG, "Got onBeginSucceeded for session " + session + ", not our inner session!"); - } - deferred.onBeginSucceeded(outerSession); - } - - @Override - public void onBeginFailed(Exception ex) { - deferred.onBeginFailed(ex); - } - - @Override - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) { - return this; - } - }; - } - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - inner.begin(new MiddlewareRepositorySessionBeginDelegate(this, delegate)); - } - - public class MiddlewareRepositorySessionFinishDelegate implements RepositorySessionFinishDelegate { - private final MiddlewareRepositorySession outerSession; - private final RepositorySessionFinishDelegate next; - - public MiddlewareRepositorySessionFinishDelegate(MiddlewareRepositorySession outerSession, RepositorySessionFinishDelegate next) { - this.outerSession = outerSession; - this.next = next; - } - - @Override - public void onFinishFailed(Exception ex) { - next.onFinishFailed(ex); - } - - @Override - public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) { - next.onFinishSucceeded(outerSession, bundle); - } - - @Override - public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) { - return this; - } - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - inner.finish(new MiddlewareRepositorySessionFinishDelegate(this, delegate)); - } - - - @Override - public synchronized void ensureActive() throws InactiveSessionException { - inner.ensureActive(); - } - - @Override - public synchronized boolean isActive() { - return inner.isActive(); - } - - @Override - public synchronized SessionStatus getStatus() { - return inner.getStatus(); - } - - @Override - public synchronized void setStatus(SessionStatus status) { - inner.setStatus(status); - } - - @Override - public synchronized void transitionFrom(SessionStatus from, SessionStatus to) - throws InvalidSessionTransitionException { - inner.transitionFrom(from, to); - } - - @Override - public void abort() { - inner.abort(); - } - - @Override - public void abort(RepositorySessionFinishDelegate delegate) { - inner.abort(new MiddlewareRepositorySessionFinishDelegate(this, delegate)); - } - - @Override - public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) { - // TODO: need to do anything here? - inner.guidsSince(timestamp, delegate); - } - - @Override - public void storeDone() { - inner.storeDone(); - } - - @Override - public void storeDone(long storeEnd) { - inner.storeDone(storeEnd); - } - - @Override - public boolean shouldSkip() { - return inner.shouldSkip(); - } - - @Override - public boolean dataAvailable() { - return inner.dataAvailable(); - } - - @Override - public void unbundle(RepositorySessionBundle bundle) { - inner.unbundle(bundle); - } - - @Override - public long getLastSyncTimestamp() { - return inner.getLastSyncTimestamp(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java deleted file mode 100644 index e3b4f25b1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AbstractBearerTokenAuthHeaderProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.message.BasicHeader; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; - -/** - * An <code>AuthHeaderProvider</code> that returns an Authorization header for - * bearer tokens, adding a simple prefix. - */ -public abstract class AbstractBearerTokenAuthHeaderProvider implements AuthHeaderProvider { - protected final String header; - - public AbstractBearerTokenAuthHeaderProvider(String token) { - if (token == null) { - throw new IllegalArgumentException("token must not be null."); - } - - this.header = getPrefix() + " " + token; - } - - protected abstract String getPrefix(); - - @Override - public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) { - return new BasicHeader("Authorization", header); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java deleted file mode 100644 index 7be6fef3d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/AuthHeaderProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.security.GeneralSecurityException; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; - -/** - * An <code>AuthHeaderProvider</code> generates HTTP Authorization headers for - * HTTP requests. - */ -public interface AuthHeaderProvider { - /** - * Generate an HTTP Authorization header. - * - * @param request HTTP request. - * @param context HTTP context. - * @param client HTTP client. - * @return HTTP Authorization header. - * @throws GeneralSecurityException usually wrapping a more specific exception. - */ - Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) - throws GeneralSecurityException; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java deleted file mode 100644 index 60bbc86bb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResource.java +++ /dev/null @@ -1,565 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.net.ssl.SSLContext; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpVersion; -import ch.boye.httpclientandroidlib.client.AuthCache; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.entity.GzipCompressingEntity; -import ch.boye.httpclientandroidlib.client.methods.HttpDelete; -import ch.boye.httpclientandroidlib.client.methods.HttpGet; -import ch.boye.httpclientandroidlib.client.methods.HttpPatch; -import ch.boye.httpclientandroidlib.client.methods.HttpPost; -import ch.boye.httpclientandroidlib.client.methods.HttpPut; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; -import ch.boye.httpclientandroidlib.client.protocol.ClientContext; -import ch.boye.httpclientandroidlib.conn.ClientConnectionManager; -import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory; -import ch.boye.httpclientandroidlib.conn.scheme.Scheme; -import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry; -import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory; -import ch.boye.httpclientandroidlib.entity.StringEntity; -import ch.boye.httpclientandroidlib.impl.client.BasicAuthCache; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager; -import ch.boye.httpclientandroidlib.params.HttpConnectionParams; -import ch.boye.httpclientandroidlib.params.HttpParams; -import ch.boye.httpclientandroidlib.params.HttpProtocolParams; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; -import ch.boye.httpclientandroidlib.protocol.HttpContext; -import ch.boye.httpclientandroidlib.util.EntityUtils; - -/** - * Provide simple HTTP access to a Sync server or similar. - * Implements Basic Auth by asking its delegate for credentials. - * Communicates with a ResourceDelegate to asynchronously return responses and errors. - * Exposes simple get/post/put/delete methods. - */ -@SuppressWarnings("deprecation") -public class BaseResource implements Resource { - private static final String ANDROID_LOOPBACK_IP = "10.0.2.2"; - - private static final int MAX_TOTAL_CONNECTIONS = 20; - private static final int MAX_CONNECTIONS_PER_ROUTE = 10; - - private boolean retryOnFailedRequest = true; - - public static boolean rewriteLocalhost = true; - - private static final String LOG_TAG = "BaseResource"; - - protected final URI uri; - protected BasicHttpContext context; - protected DefaultHttpClient client; - public ResourceDelegate delegate; - protected HttpRequestBase request; - public final String charset = "utf-8"; - - private boolean shouldGzipCompress = false; - // A hint whether uploaded payloads are chunked. Default true to use GzipCompressingEntity, which is built-in functionality. - private boolean shouldChunkUploadsHint = true; - - /** - * We have very few writes (observers tend to be installed around sync - * sessions) and many iterations (every HTTP request iterates observers), so - * CopyOnWriteArrayList is a reasonable choice. - */ - protected static final CopyOnWriteArrayList<WeakReference<HttpResponseObserver>> - httpResponseObservers = new CopyOnWriteArrayList<>(); - - public BaseResource(String uri) throws URISyntaxException { - this(uri, rewriteLocalhost); - } - - public BaseResource(URI uri) { - this(uri, rewriteLocalhost); - } - - public BaseResource(String uri, boolean rewrite) throws URISyntaxException { - this(new URI(uri), rewrite); - } - - public BaseResource(URI uri, boolean rewrite) { - if (uri == null) { - throw new IllegalArgumentException("uri must not be null"); - } - if (rewrite && "localhost".equals(uri.getHost())) { - // Rewrite localhost URIs to refer to the special Android emulator loopback passthrough interface. - Logger.debug(LOG_TAG, "Rewriting " + uri + " to point to " + ANDROID_LOOPBACK_IP + "."); - try { - this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); - } catch (URISyntaxException e) { - Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e); - throw new IllegalArgumentException("Invalid URI", e); - } - } else { - this.uri = uri; - } - } - - public static void addHttpResponseObserver(HttpResponseObserver newHttpResponseObserver) { - if (newHttpResponseObserver == null) { - return; - } - httpResponseObservers.add(new WeakReference<HttpResponseObserver>(newHttpResponseObserver)); - } - - public static boolean isHttpResponseObserver(HttpResponseObserver httpResponseObserver) { - for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) { - HttpResponseObserver innerHttpResponseObserver = weakReference.get(); - if (innerHttpResponseObserver == httpResponseObserver) { - return true; - } - } - return false; - } - - public static boolean removeHttpResponseObserver(HttpResponseObserver httpResponseObserver) { - for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) { - HttpResponseObserver innerHttpResponseObserver = weakReference.get(); - if (innerHttpResponseObserver == httpResponseObserver) { - // It's safe to mutate the observers while iterating. - httpResponseObservers.remove(weakReference); - return true; - } - } - return false; - } - - @Override - public URI getURI() { - return this.uri; - } - - @Override - public String getURIString() { - return this.uri.toString(); - } - - @Override - public String getHostname() { - return this.getURI().getHost(); - } - - /** - * Causes the Resource to compress the uploaded entity payload in requests with payloads (e.g. post, put) - * @param shouldCompress true if the entity should be compressed, false otherwise - */ - public void setShouldCompressUploadedEntity(final boolean shouldCompress) { - shouldGzipCompress = shouldCompress; - } - - /** - * Causes the Resource to chunk the uploaded entity payload in requests with payloads (e.g. post, put). - * Note: this flag is only a hint - chunking is not guaranteed. - * - * Chunking is currently supported with gzip compression. - * - * @param shouldChunk true if the transfer should be chunked, false otherwise - */ - public void setShouldChunkUploadsHint(final boolean shouldChunk) { - shouldChunkUploadsHint = shouldChunk; - } - - private HttpEntity getMaybeCompressedEntity(final HttpEntity entity) { - if (!shouldGzipCompress) { - return entity; - } - - return shouldChunkUploadsHint ? new GzipCompressingEntity(entity) : new GzipNonChunkedCompressingEntity(entity); - } - - /** - * This shuts up HttpClient, which will otherwise debug log about there - * being no auth cache in the context. - */ - private static void addAuthCacheToContext(HttpUriRequest request, HttpContext context) { - AuthCache authCache = new BasicAuthCache(); // Not thread safe. - context.setAttribute(ClientContext.AUTH_CACHE, authCache); - } - - /** - * Invoke this after delegate and request have been set. - * @throws NoSuchAlgorithmException - * @throws KeyManagementException - */ - protected void prepareClient() throws KeyManagementException, NoSuchAlgorithmException, GeneralSecurityException { - context = new BasicHttpContext(); - - // We could reuse these client instances, except that we mess around - // with their parameters… so we'd need a pool of some kind. - client = new DefaultHttpClient(getConnectionManager()); - - // TODO: Eventually we should use Apache HttpAsyncClient. It's not out of alpha yet. - // Until then, we synchronously make the request, then invoke our delegate's callback. - AuthHeaderProvider authHeaderProvider = delegate.getAuthHeaderProvider(); - if (authHeaderProvider != null) { - Header authHeader = authHeaderProvider.getAuthHeader(request, context, client); - if (authHeader != null) { - request.addHeader(authHeader); - Logger.debug(LOG_TAG, "Added auth header."); - } - } - - addAuthCacheToContext(request, context); - - HttpParams params = client.getParams(); - HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout()); - HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout()); - HttpConnectionParams.setStaleCheckingEnabled(params, false); - HttpProtocolParams.setContentCharset(params, charset); - HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); - final String userAgent = delegate.getUserAgent(); - if (userAgent != null) { - HttpProtocolParams.setUserAgent(params, userAgent); - } - delegate.addHeaders(request, client); - } - - private static final Object connManagerMonitor = new Object(); - private static ClientConnectionManager connManager; - - // Call within a synchronized block on connManagerMonitor. - private static ClientConnectionManager enableTLSConnectionManager() throws KeyManagementException, NoSuchAlgorithmException { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, new SecureRandom()); - - Logger.debug(LOG_TAG, "Using protocols and cipher suites for Android API " + android.os.Build.VERSION.SDK_INT); - SSLSocketFactory sf = new SSLSocketFactory(sslContext, GlobalConstants.DEFAULT_PROTOCOLS, GlobalConstants.DEFAULT_CIPHER_SUITES, null); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("https", 443, sf)); - schemeRegistry.register(new Scheme("http", 80, new PlainSocketFactory())); - ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry); - - cm.setMaxTotal(MAX_TOTAL_CONNECTIONS); - cm.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE); - connManager = cm; - return cm; - } - - public static ClientConnectionManager getConnectionManager() throws KeyManagementException, NoSuchAlgorithmException - { - // TODO: shutdown. - synchronized (connManagerMonitor) { - if (connManager != null) { - return connManager; - } - return enableTLSConnectionManager(); - } - } - - /** - * Do some cleanup, so we don't need the stale connection check. - */ - public static void closeExpiredConnections() { - ClientConnectionManager connectionManager; - synchronized (connManagerMonitor) { - connectionManager = connManager; - } - if (connectionManager == null) { - return; - } - Logger.trace(LOG_TAG, "Closing expired connections."); - connectionManager.closeExpiredConnections(); - } - - public static void shutdownConnectionManager() { - ClientConnectionManager connectionManager; - synchronized (connManagerMonitor) { - connectionManager = connManager; - connManager = null; - } - if (connectionManager == null) { - return; - } - Logger.debug(LOG_TAG, "Shutting down connection manager."); - connectionManager.shutdown(); - } - - private void execute() { - HttpResponse response; - try { - response = client.execute(request, context); - Logger.debug(LOG_TAG, "Response: " + response.getStatusLine().toString()); - } catch (ClientProtocolException e) { - delegate.handleHttpProtocolException(e); - return; - } catch (IOException e) { - Logger.debug(LOG_TAG, "I/O exception returned from execute."); - if (!retryOnFailedRequest) { - delegate.handleHttpIOException(e); - } else { - retryRequest(); - } - return; - } catch (Exception e) { - // Bug 740731: Don't let an exception fall through. Wrapping isn't - // optimal, but often the exception is treated as an Exception anyway. - if (!retryOnFailedRequest) { - // Bug 769671: IOException(Throwable cause) was added only in API level 9. - final IOException ex = new IOException(); - ex.initCause(e); - delegate.handleHttpIOException(ex); - } else { - retryRequest(); - } - return; - } - - // Don't retry if the observer or delegate throws! - for (WeakReference<HttpResponseObserver> weakReference : httpResponseObservers) { - HttpResponseObserver observer = weakReference.get(); - if (observer != null) { - observer.observeHttpResponse(request, response); - } - } - delegate.handleHttpResponse(response); - } - - private void retryRequest() { - // Only retry once. - retryOnFailedRequest = false; - Logger.debug(LOG_TAG, "Retrying request..."); - this.execute(); - } - - private void go(HttpRequestBase request) { - if (delegate == null) { - throw new IllegalArgumentException("No delegate provided."); - } - this.request = request; - try { - this.prepareClient(); - } catch (KeyManagementException e) { - Logger.error(LOG_TAG, "Couldn't prepare client.", e); - delegate.handleTransportException(e); - return; - } catch (GeneralSecurityException e) { - Logger.error(LOG_TAG, "Couldn't prepare client.", e); - delegate.handleTransportException(e); - return; - } catch (Exception e) { - // Bug 740731: Don't let an exception fall through. Wrapping isn't - // optimal, but often the exception is treated as an Exception anyway. - delegate.handleTransportException(new GeneralSecurityException(e)); - return; - } - this.execute(); - } - - @Override - public void get() { - Logger.debug(LOG_TAG, "HTTP GET " + this.uri.toASCIIString()); - this.go(new HttpGet(this.uri)); - } - - /** - * Perform an HTTP GET as with {@link BaseResource#get()}, returning only - * after callbacks have been invoked. - */ - public void getBlocking() { - // Until we use the asynchronous Apache HttpClient, we can simply call - // through. - this.get(); - } - - @Override - public void delete() { - Logger.debug(LOG_TAG, "HTTP DELETE " + this.uri.toASCIIString()); - this.go(new HttpDelete(this.uri)); - } - - @Override - public void post(HttpEntity body) { - Logger.debug(LOG_TAG, "HTTP POST " + this.uri.toASCIIString()); - body = getMaybeCompressedEntity(body); - HttpPost request = new HttpPost(this.uri); - request.setEntity(body); - this.go(request); - } - - @Override - public void patch(HttpEntity body) { - Logger.debug(LOG_TAG, "HTTP PATCH " + this.uri.toASCIIString()); - body = getMaybeCompressedEntity(body); - HttpPatch request = new HttpPatch(this.uri); - request.setEntity(body); - this.go(request); - } - - @Override - public void put(HttpEntity body) { - Logger.debug(LOG_TAG, "HTTP PUT " + this.uri.toASCIIString()); - body = getMaybeCompressedEntity(body); - HttpPut request = new HttpPut(this.uri); - request.setEntity(body); - this.go(request); - } - - protected static StringEntity stringEntityWithContentTypeApplicationJSON(String s) { - StringEntity e = new StringEntity(s, "UTF-8"); - e.setContentType("application/json"); - return e; - } - - /** - * Helper for turning a JSON object into a payload. - * @throws UnsupportedEncodingException - */ - protected static StringEntity jsonEntity(JSONObject body) { - return stringEntityWithContentTypeApplicationJSON(body.toJSONString()); - } - - /** - * Helper for turning an extended JSON object into a payload. - * @throws UnsupportedEncodingException - */ - protected static StringEntity jsonEntity(ExtendedJSONObject body) { - return stringEntityWithContentTypeApplicationJSON(body.toJSONString()); - } - - /** - * Helper for turning a JSON array into a payload. - * @throws UnsupportedEncodingException - */ - protected static HttpEntity jsonEntity(JSONArray toPOST) throws UnsupportedEncodingException { - return stringEntityWithContentTypeApplicationJSON(toPOST.toJSONString()); - } - - /** - * Best-effort attempt to ensure that the entity has been fully consumed and - * that the underlying stream has been closed. - * - * This releases the connection back to the connection pool. - * - * @param entity The HttpEntity to be consumed. - */ - public static void consumeEntity(HttpEntity entity) { - try { - EntityUtils.consume(entity); - } catch (IOException e) { - // Doesn't matter. - } - } - - /** - * Best-effort attempt to ensure that the entity corresponding to the given - * HTTP response has been fully consumed and that the underlying stream has - * been closed. - * - * This releases the connection back to the connection pool. - * - * @param response - * The HttpResponse to be consumed. - */ - public static void consumeEntity(HttpResponse response) { - if (response == null) { - return; - } - try { - EntityUtils.consume(response.getEntity()); - } catch (IOException e) { - } - } - - /** - * Best-effort attempt to ensure that the entity corresponding to the given - * Sync storage response has been fully consumed and that the underlying - * stream has been closed. - * - * This releases the connection back to the connection pool. - * - * @param response - * The SyncStorageResponse to be consumed. - */ - public static void consumeEntity(SyncStorageResponse response) { - if (response.httpResponse() == null) { - return; - } - consumeEntity(response.httpResponse()); - } - - /** - * Best-effort attempt to ensure that the reader has been fully consumed, so - * that the underlying stream will be closed. - * - * This should allow the connection to be released back to the connection pool. - * - * @param reader The BufferedReader to be consumed. - */ - public static void consumeReader(BufferedReader reader) { - try { - reader.close(); - } catch (IOException e) { - // Do nothing. - } - } - - public void post(JSONArray jsonArray) throws UnsupportedEncodingException { - post(jsonEntity(jsonArray)); - } - - public void put(JSONObject jsonObject) throws UnsupportedEncodingException { - put(jsonEntity(jsonObject)); - } - - public void put(ExtendedJSONObject o) { - put(jsonEntity(o)); - } - - public void post(ExtendedJSONObject o) { - post(jsonEntity(o)); - } - - /** - * Perform an HTTP POST as with {@link BaseResource#post(ExtendedJSONObject)}, returning only - * after callbacks have been invoked. - */ - public void postBlocking(final ExtendedJSONObject o) { - // Until we use the asynchronous Apache HttpClient, we can simply call - // through. - post(jsonEntity(o)); - } - - public void post(JSONObject jsonObject) throws UnsupportedEncodingException { - post(jsonEntity(jsonObject)); - } - - public void patch(JSONArray jsonArray) throws UnsupportedEncodingException { - patch(jsonEntity(jsonArray)); - } - - public void patch(ExtendedJSONObject o) { - patch(jsonEntity(o)); - } - - public void patch(JSONObject jsonObject) throws UnsupportedEncodingException { - patch(jsonEntity(jsonObject)); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java deleted file mode 100644 index 84ae7a3d5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BaseResourceDelegate.java +++ /dev/null @@ -1,44 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -/** - * Shared abstract class for resource delegate that use the same timeouts - * and no credentials. - * - * @author rnewman - * - */ -public abstract class BaseResourceDelegate implements ResourceDelegate { - public static int connectionTimeoutInMillis = 1000 * 30; // Wait 30s for a connection to open. - public static int socketTimeoutInMillis = 1000 * 2 * 60; // Wait 2 minutes for data. - - protected Resource resource; - public BaseResourceDelegate(Resource resource) { - this.resource = resource; - } - - @Override - public int connectionTimeout() { - return connectionTimeoutInMillis; - } - - @Override - public int socketTimeout() { - return socketTimeoutInMillis; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return null; - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java deleted file mode 100644 index d8a371ddc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BasicAuthHeaderProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.auth.Credentials; -import ch.boye.httpclientandroidlib.auth.UsernamePasswordCredentials; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.auth.BasicScheme; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; - -/** - * An <code>AuthHeaderProvider</code> that returns an HTTP Basic auth header. - */ -public class BasicAuthHeaderProvider implements AuthHeaderProvider { - protected final String credentials; - - /** - * Constructor. - * - * @param credentials string in form "user:pass". - */ - public BasicAuthHeaderProvider(String credentials) { - this.credentials = credentials; - } - - /** - * Constructor. - * - * @param user username. - * @param pass password. - */ - public BasicAuthHeaderProvider(String user, String pass) { - this(user + ":" + pass); - } - - /** - * Return a Header object representing an Authentication header for HTTP - * Basic. - */ - @Override - public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) { - Credentials creds = new UsernamePasswordCredentials(credentials); - - // This must be UTF-8 to generate the same Basic Auth headers as desktop for non-ASCII passwords. - return BasicScheme.authenticate(creds, "UTF-8", false); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java deleted file mode 100644 index d142d50d9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BearerAuthHeaderProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -/** - * An <code>AuthHeaderProvider</code> that returns an Authorization header for - * Bearer tokens in the format expected by a Mozilla Firefox Accounts Profile Server. - * <p> - * See <a href="https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/master/docs/API.md</a>. - */ -public class BearerAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider { - public BearerAuthHeaderProvider(String token) { - super(token); - } - - @Override - protected String getPrefix() { - return "Bearer"; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java deleted file mode 100644 index 5004673b3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/BrowserIDAuthHeaderProvider.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -/** - * An <code>AuthHeaderProvider</code> that returns an Authorization header for - * BrowserID assertions in the format expected by a Mozilla Services Token - * Server. - * <p> - * See <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>. - */ -public class BrowserIDAuthHeaderProvider extends AbstractBearerTokenAuthHeaderProvider { - public BrowserIDAuthHeaderProvider(String assertion) { - super(assertion); - } - - @Override - protected String getPrefix() { - return "BrowserID"; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java deleted file mode 100644 index 1a2011771..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ConnectionMonitorThread.java +++ /dev/null @@ -1,44 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import org.mozilla.gecko.background.common.log.Logger; - -/** - * Every <code>REAP_INTERVAL</code> milliseconds, wake up - * and expire any connections that need cleaning up. - * - * When we're told to shut down, take the connection manager - * with us. - */ -public class ConnectionMonitorThread extends Thread { - private static final long REAP_INTERVAL = 5000; // 5 seconds. - private static final String LOG_TAG = "ConnectionMonitorThread"; - - private volatile boolean stopping; - - @Override - public void run() { - try { - while (!stopping) { - synchronized (this) { - wait(REAP_INTERVAL); - BaseResource.closeExpiredConnections(); - } - } - } catch (InterruptedException e) { - Logger.trace(LOG_TAG, "Interrupted."); - } - BaseResource.shutdownConnectionManager(); - } - - public void shutdown() { - Logger.debug(LOG_TAG, "ConnectionMonitorThread told to shut down."); - stopping = true; - synchronized (this) { - notifyAll(); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java deleted file mode 100644 index 1e238c022..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java +++ /dev/null @@ -1,92 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.client.entity.GzipCompressingEntity; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Wrapping entity that compresses content when {@link #writeTo writing}. - * - * This differs from {@link GzipCompressingEntity} in that it does not chunk - * the sent data, therefore replacing the "Transfer-Encoding" HTTP header with - * the "Content-Length" header required by some servers. - * - * However, to measure the content length, the gzipped content will be temporarily - * stored in memory so be careful what content you send! - */ -public class GzipNonChunkedCompressingEntity extends GzipCompressingEntity { - final int MAX_BUFFER_SIZE_BYTES = 10 * 1000 * 1000; // 10 MB. - - private byte[] gzippedContent; - - public GzipNonChunkedCompressingEntity(final HttpEntity entity) { - super(entity); - } - - /** - * @return content length for gzipped content or -1 if there is an error - */ - @Override - public long getContentLength() { - try { - initBuffer(); - } catch (final IOException e) { - // GzipCompressingEntity always returns -1 in which case a 'Content-Length' header is omitted. - // Presumably, without it the request will fail (either client-side or server-side). - return -1; - } - return gzippedContent.length; - } - - @Override - public boolean isChunked() { - // "Content-Length" & chunked encoding are mutually exclusive: - // https://en.wikipedia.org/wiki/Chunked_transfer_encoding - return false; - } - - @Override - public InputStream getContent() throws IOException { - initBuffer(); - return new ByteArrayInputStream(gzippedContent); - } - - @Override - public void writeTo(final OutputStream outstream) throws IOException { - initBuffer(); - outstream.write(gzippedContent); - } - - private void initBuffer() throws IOException { - if (gzippedContent != null) { - return; - } - - final long unzippedContentLength = wrappedEntity.getContentLength(); - if (unzippedContentLength > MAX_BUFFER_SIZE_BYTES) { - throw new IOException( - "Wrapped entity content length, " + unzippedContentLength + " bytes, exceeds max: " + MAX_BUFFER_SIZE_BYTES); - } - - // The buffer size needed by the gzipped content should be smaller than this, - // but it's more efficient just to allocate one larger buffer than allocate - // twice if the gzipped content is too large for the default buffer. - final ByteArrayOutputStream s = new ByteArrayOutputStream((int) unzippedContentLength); - try { - super.writeTo(s); - } finally { - s.close(); - } - - gzippedContent = s.toByteArray(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java deleted file mode 100644 index 5314d345b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HMACAuthHeaderProvider.java +++ /dev/null @@ -1,257 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Utils; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.message.BasicHeader; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; - -/** - * An <code>AuthHeaderProvider</code> that returns an Authorization header for - * HMAC-SHA1-signed requests in the format expected by Mozilla Services - * identity-attached services and specified by the MAC Authentication spec, available at - * <a href="https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac">https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac</a>. - * <p> - * See <a href="https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access">https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access</a>. - */ -public class HMACAuthHeaderProvider implements AuthHeaderProvider { - public static final String LOG_TAG = "HMACAuthHeaderProvider"; - - public static final int NONCE_LENGTH_IN_BYTES = 8; - - public static final String HMAC_SHA1_ALGORITHM = "hmacSHA1"; - - public final String identifier; - public final String key; - - public HMACAuthHeaderProvider(String identifier, String key) { - // Validate identifier string. From the MAC Authentication spec: - // id = "id" "=" string-value - // string-value = ( <"> plain-string <"> ) / plain-string - // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E ) - // We add quotes around the id string, so input identifier must be a plain-string. - if (identifier == null) { - throw new IllegalArgumentException("identifier must not be null."); - } - if (!isPlainString(identifier)) { - throw new IllegalArgumentException("identifier must be a plain-string."); - } - - if (key == null) { - throw new IllegalArgumentException("key must not be null."); - } - - this.identifier = identifier; - this.key = key; - } - - @Override - public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { - long timestamp = System.currentTimeMillis() / 1000; - String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES)); - String extra = ""; - - try { - return getAuthHeader(request, context, client, timestamp, nonce, extra); - } catch (InvalidKeyException | NoSuchAlgorithmException | UnsupportedEncodingException e) { - // We lie a little and make every exception a GeneralSecurityException. - throw new GeneralSecurityException(e); - } - } - - /** - * Test if input is a <code>plain-string</code>. - * <p> - * A plain-string is defined by the MAC Authentication spec as - * <code>plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )</code>. - * - * @param input - * as a String of "US-ASCII" bytes. - * @return true if input is a <code>plain-string</code>; false otherwise. - * @throws UnsupportedEncodingException - */ - protected static boolean isPlainString(String input) { - if (input == null || input.length() == 0) { - return false; - } - - byte[] bytes; - try { - bytes = input.getBytes("US-ASCII"); - } catch (UnsupportedEncodingException e) { - // Should never happen. - Logger.warn(LOG_TAG, "Got exception in isPlainString; returning false.", e); - return false; - } - - for (byte b : bytes) { - if ((0x20 <= b && b <= 0x21) || (0x23 <= b && b <= 0x5B) || (0x5D <= b && b <= 0x7E)) { - continue; - } - return false; - } - - return true; - } - - /** - * Helper function that generates an HTTP Authorization header given - * additional MAC Authentication specific data. - * - * @throws UnsupportedEncodingException - * @throws NoSuchAlgorithmException - * @throws InvalidKeyException - */ - protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client, - long timestamp, String nonce, String extra) - throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException { - // Validate timestamp. From the MAC Authentication spec: - // timestamp = 1*DIGIT - // This is equivalent to timestamp >= 0. - if (timestamp < 0) { - throw new IllegalArgumentException("timestamp must contain only [0-9]."); - } - - // Validate nonce string. From the MAC Authentication spec: - // nonce = "nonce" "=" string-value - // string-value = ( <"> plain-string <"> ) / plain-string - // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E ) - // We add quotes around the nonce string, so input nonce must be a plain-string. - if (nonce == null) { - throw new IllegalArgumentException("nonce must not be null."); - } - if (nonce.length() == 0) { - throw new IllegalArgumentException("nonce must not be empty."); - } - if (!isPlainString(nonce)) { - throw new IllegalArgumentException("nonce must be a plain-string."); - } - - // Validate extra string. From the MAC Authentication spec: - // ext = "ext" "=" string-value - // string-value = ( <"> plain-string <"> ) / plain-string - // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E ) - // We add quotes around the extra string, so input extra must be a plain-string. - // We break the spec by allowing ext to be an empty string, i.e. to match 0*(...). - if (extra == null) { - throw new IllegalArgumentException("extra must not be null."); - } - if (extra.length() > 0 && !isPlainString(extra)) { - throw new IllegalArgumentException("extra must be a plain-string."); - } - - String requestString = getRequestString(request, timestamp, nonce, extra); - String macString = getSignature(requestString, this.key); - - String h = "MAC id=\"" + this.identifier + "\", " + - "ts=\"" + timestamp + "\", " + - "nonce=\"" + nonce + "\", " + - "mac=\"" + macString + "\""; - - if (extra != null) { - h += ", ext=\"" + extra + "\""; - } - - Header header = new BasicHeader("Authorization", h); - - return header; - } - - protected static byte[] sha1(byte[] message, byte[] key) - throws NoSuchAlgorithmException, InvalidKeyException { - - SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM); - - Mac hasher = Mac.getInstance(HMAC_SHA1_ALGORITHM); - hasher.init(keySpec); - hasher.update(message); - - byte[] hmac = hasher.doFinal(); - - return hmac; - } - - /** - * Sign an HMAC request string. - * - * @param requestString to sign. - * @param key as <code>String</code>. - * @return signature as base-64 encoded string. - * @throws InvalidKeyException - * @throws NoSuchAlgorithmException - * @throws UnsupportedEncodingException - */ - protected static String getSignature(String requestString, String key) - throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { - String macString = Base64.encodeBase64String(sha1(requestString.getBytes("UTF-8"), key.getBytes("UTF-8"))); - - return macString; - } - - /** - * Generate an HMAC request string. - * <p> - * This method trusts its inputs to be valid as per the MAC Authentication spec. - * - * @param request HTTP request. - * @param timestamp to use. - * @param nonce to use. - * @param extra to use. - * @return request string. - */ - protected static String getRequestString(HttpUriRequest request, long timestamp, String nonce, String extra) { - String method = request.getMethod().toUpperCase(); - - URI uri = request.getURI(); - String host = uri.getHost(); - - String path = uri.getRawPath(); - if (uri.getRawQuery() != null) { - path += "?"; - path += uri.getRawQuery(); - } - if (uri.getRawFragment() != null) { - path += "#"; - path += uri.getRawFragment(); - } - - int port = uri.getPort(); - String scheme = uri.getScheme(); - if (port != -1) { - } else if ("http".equalsIgnoreCase(scheme)) { - port = 80; - } else if ("https".equalsIgnoreCase(scheme)) { - port = 443; - } else { - throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + "."); - } - - String requestString = timestamp + "\n" + - nonce + "\n" + - method + "\n" + - path + "\n" + - host + "\n" + - port + "\n" + - extra + "\n"; - - return requestString; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java deleted file mode 100644 index 27ec74b66..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HandleProgressException.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import org.mozilla.gecko.sync.SyncException; - -public class HandleProgressException extends SyncException { - private static final long serialVersionUID = -4444933937013161059L; - - public HandleProgressException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java deleted file mode 100644 index 2bdd5604a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HawkAuthHeaderProvider.java +++ /dev/null @@ -1,403 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Locale; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.mozilla.apache.commons.codec.binary.Base64; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Utils; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.message.BasicHeader; -import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; - -/** - * An <code>AuthHeaderProvider</code> that returns an Authorization header for - * Hawk: <a href="https://github.com/hueniverse/hawk">https://github.com/hueniverse/hawk</a>. - * - * Hawk is an HTTP authentication scheme using a message authentication code - * (MAC) algorithm to provide partial HTTP request cryptographic verification. - * Hawk is the successor to the HMAC authentication scheme. - */ -public class HawkAuthHeaderProvider implements AuthHeaderProvider { - public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName(); - - public static final int HAWK_HEADER_VERSION = 1; - - protected static final int NONCE_LENGTH_IN_BYTES = 8; - protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256"; - - protected final String id; - protected final byte[] key; - protected final boolean includePayloadHash; - protected final long skewSeconds; - - /** - * Create a Hawk Authorization header provider. - * <p> - * Hawk specifies no mechanism by which a client receives an - * identifier-and-key pair from the server. - * <p> - * Hawk requests can include a payload verification hash with requests that - * enclose an entity (PATCH, POST, and PUT requests). <b>You should default - * to including the payload verification hash<b> unless you have a good reason - * not to -- the server can always ignore payload verification hashes provided - * by the client. - * - * @param id - * to name requests with. - * @param key - * to sign request with. - * - * @param includePayloadHash - * true if payload verification hash should be included in signed - * request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>. - * - * @param skewSeconds - * a number of seconds by which to skew the current time when - * computing a header. - */ - public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) { - if (id == null) { - throw new IllegalArgumentException("id must not be null"); - } - if (key == null) { - throw new IllegalArgumentException("key must not be null"); - } - this.id = id; - this.key = key; - this.includePayloadHash = includePayloadHash; - this.skewSeconds = skewSeconds; - } - - /** - * @return the current time in milliseconds. - */ - @SuppressWarnings("static-method") - protected long now() { - return System.currentTimeMillis(); - } - - /** - * @return the current time in seconds, adjusted for skew. This should - * approximate the server's timestamp. - */ - protected long getTimestampSeconds() { - return (now() / 1000) + skewSeconds; - } - - @Override - public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { - long timestamp = getTimestampSeconds(); - String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES)); - String extra = ""; - - try { - return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash); - } catch (Exception e) { - // We lie a little and make every exception a GeneralSecurityException. - throw new GeneralSecurityException(e); - } - } - - /** - * Helper function that generates an HTTP Authorization: Hawk header given - * additional Hawk specific data. - * - * @throws NoSuchAlgorithmException - * @throws InvalidKeyException - * @throws IOException - */ - protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client, - long timestamp, String nonce, String extra, boolean includePayloadHash) - throws InvalidKeyException, NoSuchAlgorithmException, IOException { - if (timestamp < 0) { - throw new IllegalArgumentException("timestamp must contain only [0-9]."); - } - - if (nonce == null) { - throw new IllegalArgumentException("nonce must not be null."); - } - if (nonce.length() == 0) { - throw new IllegalArgumentException("nonce must not be empty."); - } - - String payloadHash = null; - if (includePayloadHash) { - payloadHash = getPayloadHashString(request); - } else { - Logger.debug(LOG_TAG, "Configured to not include payload hash for this request."); - } - - String app = null; - String dlg = null; - String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg); - String macString = getSignature(requestString.getBytes("UTF-8"), this.key); - - StringBuilder sb = new StringBuilder(); - sb.append("Hawk id=\""); - sb.append(this.id); - sb.append("\", "); - sb.append("ts=\""); - sb.append(timestamp); - sb.append("\", "); - sb.append("nonce=\""); - sb.append(nonce); - sb.append("\", "); - if (payloadHash != null) { - sb.append("hash=\""); - sb.append(payloadHash); - sb.append("\", "); - } - if (extra != null && extra.length() > 0) { - sb.append("ext=\""); - sb.append(escapeExtraHeaderAttribute(extra)); - sb.append("\", "); - } - sb.append("mac=\""); - sb.append(macString); - sb.append("\""); - - return new BasicHeader("Authorization", sb.toString()); - } - - /** - * Get the payload verification hash for the given request, if possible. - * <p> - * Returns null if the request does not enclose an entity (is not an HTTP - * PATCH, POST, or PUT). Throws if the payload verification hash cannot be - * computed. - * - * @param request - * to compute hash for. - * @return verification hash, or null if the request does not enclose an entity. - * @throws IllegalArgumentException if the request does not enclose a valid non-null entity. - * @throws UnsupportedEncodingException - * @throws NoSuchAlgorithmException - * @throws IOException - */ - protected static String getPayloadHashString(HttpRequestBase request) - throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException { - final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest; - if (!shouldComputePayloadHash) { - Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request."); - return null; - } - if (!(request instanceof HttpEntityEnclosingRequest)) { - throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity"); - } - final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); - if (entity == null) { - throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity"); - } - return Base64.encodeBase64String(getPayloadHash(entity)); - } - - /** - * Escape the user-provided extra string for the ext="" header attribute. - * <p> - * Hawk escapes the header ext="" attribute differently than it does the extra - * line in the normalized request string. - * <p> - * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385</a>. - * - * @param extra to escape. - * @return extra escaped for the ext="" header attribute. - */ - protected static String escapeExtraHeaderAttribute(String extra) { - return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\""); - } - - /** - * Escape the user-provided extra string for inserting into the normalized - * request string. - * <p> - * Hawk escapes the header ext="" attribute differently than it does the extra - * line in the normalized request string. - * <p> - * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67</a>. - * - * @param extra to escape. - * @return extra escaped for the normalized request string. - */ - protected static String escapeExtraString(String extra) { - return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n"); - } - - /** - * Return the content type with no parameters (pieces following ;). - * - * @param contentTypeHeader to interrogate. - * @return base content type. - */ - protected static String getBaseContentType(Header contentTypeHeader) { - if (contentTypeHeader == null) { - throw new IllegalArgumentException("contentTypeHeader must not be null."); - } - String contentType = contentTypeHeader.getValue(); - if (contentType == null) { - throw new IllegalArgumentException("contentTypeHeader value must not be null."); - } - int index = contentType.indexOf(";"); - if (index < 0) { - return contentType.trim(); - } - return contentType.substring(0, index).trim(); - } - - /** - * Generate the SHA-256 hash of a normalized Hawk payload generated from an - * HTTP entity. - * <p> - * <b>Warning:</b> the entity <b>must</b> be repeatable. If it is not, this - * code throws an <code>IllegalArgumentException</code>. - * <p> - * This is under-specified; the code here was reverse engineered from the code - * at - * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81</a>. - * @param entity to normalize and hash. - * @return hash. - * @throws IllegalArgumentException if entity is not repeatable. - */ - protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException { - if (!entity.isRepeatable()) { - throw new IllegalArgumentException("entity must be repeatable"); - } - final MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8")); - digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8")); - digest.update("\n".getBytes("UTF-8")); - InputStream stream = entity.getContent(); - try { - int numRead; - byte[] buffer = new byte[4096]; - while (-1 != (numRead = stream.read(buffer))) { - if (numRead > 0) { - digest.update(buffer, 0, numRead); - } - } - digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk. - return digest.digest(); - } finally { - stream.close(); - } - } - - /** - * Generate a normalized Hawk request string. This is under-specified; the - * code here was reverse engineered from the code at - * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55</a>. - * <p> - * This method trusts its inputs to be valid. - */ - protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) { - String method = request.getMethod().toUpperCase(Locale.US); - - URI uri = request.getURI(); - String host = uri.getHost(); - - String path = uri.getRawPath(); - if (uri.getRawQuery() != null) { - path += "?"; - path += uri.getRawQuery(); - } - if (uri.getRawFragment() != null) { - path += "#"; - path += uri.getRawFragment(); - } - - int port = uri.getPort(); - String scheme = uri.getScheme(); - if (port != -1) { - } else if ("http".equalsIgnoreCase(scheme)) { - port = 80; - } else if ("https".equalsIgnoreCase(scheme)) { - port = 443; - } else { - throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + "."); - } - - StringBuilder sb = new StringBuilder(); - sb.append("hawk."); - sb.append(HAWK_HEADER_VERSION); - sb.append('.'); - sb.append(type); - sb.append('\n'); - sb.append(timestamp); - sb.append('\n'); - sb.append(nonce); - sb.append('\n'); - sb.append(method); - sb.append('\n'); - sb.append(path); - sb.append('\n'); - sb.append(host); - sb.append('\n'); - sb.append(port); - sb.append('\n'); - if (hash != null) { - sb.append(hash); - } - sb.append("\n"); - if (extra != null && extra.length() > 0) { - sb.append(escapeExtraString(extra)); - } - sb.append("\n"); - if (app != null) { - sb.append(app); - sb.append("\n"); - if (dlg != null) { - sb.append(dlg); - } - sb.append("\n"); - } - - return sb.toString(); - } - - protected static byte[] hmacSha256(byte[] message, byte[] key) - throws NoSuchAlgorithmException, InvalidKeyException { - - SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM); - - Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM); - hasher.init(keySpec); - hasher.update(message); - - return hasher.doFinal(); - } - - /** - * Sign a Hawk request string. - * - * @param requestString to sign. - * @param key as <code>String</code>. - * @return signature as base-64 encoded string. - * @throws InvalidKeyException - * @throws NoSuchAlgorithmException - * @throws UnsupportedEncodingException - */ - protected static String getSignature(byte[] requestString, byte[] key) - throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { - return Base64.encodeBase64String(hmacSha256(requestString, key)); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java deleted file mode 100644 index 24b37a0e6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/HttpResponseObserver.java +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; - -public interface HttpResponseObserver { - /** - * Observe an HTTP response. - * @param request - * The <code>HttpUriRequest<code> that elicited the response. - * - * @param response - * The <code>HttpResponse</code> to observe. - */ - public void observeHttpResponse(HttpUriRequest request, HttpResponse response); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java deleted file mode 100644 index 3f76f929f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java +++ /dev/null @@ -1,225 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.Scanner; - -import org.json.simple.JSONArray; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.NonObjectJSONException; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.HttpStatus; -import ch.boye.httpclientandroidlib.impl.cookie.DateParseException; -import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; - -public class MozResponse { - private static final String LOG_TAG = "MozResponse"; - - private static final String HEADER_RETRY_AFTER = "retry-after"; - - protected HttpResponse response; - private String body = null; - - public HttpResponse httpResponse() { - return this.response; - } - - public int getStatusCode() { - return this.response.getStatusLine().getStatusCode(); - } - - public boolean wasSuccessful() { - return this.getStatusCode() == 200; - } - - public boolean isInvalidAuthentication() { - return this.getStatusCode() == HttpStatus.SC_UNAUTHORIZED; - } - - /** - * Fetch the content type of the HTTP response body. - * - * @return a <code>Header</code> instance, or <code>null</code> if there was - * no body or no valid Content-Type. - */ - public Header getContentType() { - HttpEntity entity = this.response.getEntity(); - if (entity == null) { - return null; - } - return entity.getContentType(); - } - - private static boolean missingHeader(String value) { - return value == null || - value.trim().length() == 0; - } - - public String body() throws IllegalStateException, IOException { - if (body != null) { - return body; - } - final HttpEntity entity = this.response.getEntity(); - if (entity == null) { - body = null; - return null; - } - - InputStreamReader is = new InputStreamReader(entity.getContent()); - // Oh, Java, you are so evil. - body = new Scanner(is).useDelimiter("\\A").next(); - return body; - } - - /** - * Return the body as a <b>non-null</b> <code>ExtendedJSONObject</code>. - * - * @return A non-null <code>ExtendedJSONObject</code>. - * - * @throws IllegalStateException - * @throws IOException - * @throws NonObjectJSONException - */ - public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, NonObjectJSONException { - if (body != null) { - // Do it from the cached String. - return new ExtendedJSONObject(body); - } - - HttpEntity entity = this.response.getEntity(); - if (entity == null) { - throw new IOException("no entity"); - } - - InputStream content = entity.getContent(); - try { - Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8")); - return new ExtendedJSONObject(in); - } finally { - content.close(); - } - } - - public JSONArray jsonArrayBody() throws NonArrayJSONException, IOException { - final JSONParser parser = new JSONParser(); - try { - if (body != null) { - // Do it from the cached String. - return (JSONArray) parser.parse(body); - } - - final HttpEntity entity = this.response.getEntity(); - if (entity == null) { - throw new IOException("no entity"); - } - - final InputStream content = entity.getContent(); - final Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8")); - try { - return (JSONArray) parser.parse(in); - } finally { - in.close(); - } - } catch (ClassCastException | ParseException e) { - NonArrayJSONException exception = new NonArrayJSONException("value must be a json array"); - exception.initCause(e); - throw exception; - } - } - - protected boolean hasHeader(String h) { - return this.response.containsHeader(h); - } - - public MozResponse(HttpResponse res) { - response = res; - } - - protected String getNonMissingHeader(String h) { - if (!this.hasHeader(h)) { - return null; - } - - final Header header = this.response.getFirstHeader(h); - final String value = header.getValue(); - if (missingHeader(value)) { - Logger.warn(LOG_TAG, h + " header present but empty."); - return null; - } - return value; - } - - protected long getLongHeader(String h) throws NumberFormatException { - final String value = getNonMissingHeader(h); - if (value == null) { - return -1L; - } - return Long.parseLong(value, 10); - } - - protected int getIntegerHeader(String h) throws NumberFormatException { - final String value = getNonMissingHeader(h); - if (value == null) { - return -1; - } - return Integer.parseInt(value, 10); - } - - /** - * @return A number of seconds, or -1 if the 'Retry-After' header was not present. - */ - public int retryAfterInSeconds() throws NumberFormatException { - final String retryAfter = getNonMissingHeader(HEADER_RETRY_AFTER); - if (retryAfter == null) { - return -1; - } - - try { - return Integer.parseInt(retryAfter, 10); - } catch (NumberFormatException e) { - // Fall through to try date format. - } - - try { - final long then = DateUtils.parseDate(retryAfter).getTime(); - final long now = System.currentTimeMillis(); - return (int)((then - now) / 1000); // Convert milliseconds to seconds. - } catch (DateParseException e) { - Logger.warn(LOG_TAG, "Retry-After header neither integer nor date: " + retryAfter); - return -1; - } - } - - /** - * @return A number of seconds, or -1 if the 'Backoff' header was not - * present. - */ - public int backoffInSeconds() throws NumberFormatException { - return this.getIntegerHeader("backoff"); - } - - public void logResponseBody(final String logTag) { - if (!Logger.LOG_PERSONAL_INFORMATION) { - return; - } - try { - Logger.pii(logTag, "Response body: " + body()); - } catch (Throwable e) { - Logger.debug(logTag, "No response body."); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java deleted file mode 100644 index ab7b98aff..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/Resource.java +++ /dev/null @@ -1,20 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.net.URI; - -import ch.boye.httpclientandroidlib.HttpEntity; - -public interface Resource { - public abstract URI getURI(); - public abstract String getURIString(); - public abstract String getHostname(); - public abstract void get(); - public abstract void delete(); - public abstract void post(HttpEntity body); - public abstract void patch(HttpEntity body); - public abstract void put(HttpEntity body); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java deleted file mode 100644 index 0dea9432b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/ResourceDelegate.java +++ /dev/null @@ -1,55 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.IOException; -import java.security.GeneralSecurityException; - -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -/** - * ResourceDelegate implementers must ensure that HTTP responses - * are fully consumed to ensure that connections are returned to - * the pool: - * - * EntityUtils.consume(entity); - * @author rnewman - * - */ -public interface ResourceDelegate { - // Request augmentation. - AuthHeaderProvider getAuthHeaderProvider(); - void addHeaders(HttpRequestBase request, DefaultHttpClient client); - - /** - * The value of the User-Agent header to include with the request. - * - * @return User-Agent header value; null means do not set User-Agent header. - */ - public String getUserAgent(); - - // Response handling. - - /** - * Override this to handle an HttpResponse. - * - * ResourceDelegate implementers <b>must</b> ensure that HTTP responses are - * fully consumed to ensure that connections are returned to the pool, for - * example by calling <code>EntityUtils.consume(response.getEntity())</code>. - */ - void handleHttpResponse(HttpResponse response); - void handleHttpProtocolException(ClientProtocolException e); - void handleHttpIOException(IOException e); - - // During preparation. - void handleTransportException(GeneralSecurityException e); - - // Connection parameters. - int connectionTimeout(); - int socketTimeout(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java deleted file mode 100644 index 5dfe660ef..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SRPConstants.java +++ /dev/null @@ -1,174 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.math.BigInteger; - -/** - * SRP Group Parameters from - * <a href="http://tools.ietf.org/html/rfc5054#appendix-A">Appendix A of RFC 5054</a>. - * - * The 1024-, 1536-, and 2048-bit groups are taken from software - * developed by Tom Wu and Eugene Jhong for the Stanford SRP - * distribution, and subsequently proven to be prime. The larger primes - * are taken from [MODP], but generators have been calculated that are - * primitive roots of N, unlike the generators in [MODP]. - * - * The 1024-bit and 1536-bit groups <b>MUST</b> be supported. - */ -public class SRPConstants { - public static class Parameters { - public final BigInteger N; - public final BigInteger g; - public final int bitLength; - public final int byteLength; - public final int hexLength; - - protected Parameters(String N, long g) { - if (N == null) { - throw new IllegalArgumentException("N must not be null"); - } - this.N = new BigInteger(N.replaceAll(" ", ""), 16); // Hex. - this.g = BigInteger.valueOf(g); - this.hexLength = this.N.toString(16).length(); - this.byteLength = hexLength / 2; - this.bitLength = this.byteLength * 8; - } - } - - public static final Parameters _1024 = new Parameters("" + - "EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C" + - "9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4" + - "8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29" + - "7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A" + - "FD5138FE 8376435B 9FC61D2F C0EB06E3", 2L); - - public static final Parameters _1536 = new Parameters("" + - "9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961" + - "4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843" + - "80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B" + - "E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5" + - "6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A" + - "F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E" + - "8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB", 2L); - - public static final Parameters _2048 = new Parameters("" + - "AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294" + - "3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D" + - "CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB" + - "D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74" + - "7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A" + - "436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D" + - "5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73" + - "03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6" + - "94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F" + - "9E4AFF73", 2L); - - public static final Parameters _3072 = new Parameters("" + - "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + - "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + - "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + - "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + - "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + - "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + - "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + - "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + - "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + - "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + - "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + - "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + - "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + - "E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF", 5L); - - public static final Parameters _4096 = new Parameters("" + - "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + - "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + - "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + - "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + - "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + - "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + - "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + - "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + - "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + - "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + - "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + - "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + - "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + - "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + - "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + - "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + - "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + - "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" + - "FFFFFFFF FFFFFFFF", 5L); - - public static final Parameters _6144 = new Parameters("" + - "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + - "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + - "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + - "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + - "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + - "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + - "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + - "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + - "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + - "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + - "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + - "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + - "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + - "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + - "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + - "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + - "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + - "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + - "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" + - "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" + - "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" + - "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" + - "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" + - "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + - "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" + - "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" + - "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" + - "6DCC4024 FFFFFFFF FFFFFFFF", 5L); - - public static final Parameters _8192 = new Parameters("" + - "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + - "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + - "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + - "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + - "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + - "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + - "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + - "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + - "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + - "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + - "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + - "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + - "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + - "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + - "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + - "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + - "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + - "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + - "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" + - "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" + - "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" + - "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" + - "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" + - "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + - "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" + - "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" + - "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" + - "6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA" + - "3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C" + - "5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" + - "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886" + - "2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6" + - "6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5" + - "0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268" + - "359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6" + - "FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" + - "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF", 19L); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java deleted file mode 100644 index 177d7aaba..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncResponse.java +++ /dev/null @@ -1,157 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import android.support.annotation.Nullable; - -import org.mozilla.gecko.sync.Utils; - -import ch.boye.httpclientandroidlib.HttpResponse; - -public class SyncResponse extends MozResponse { - public static final String X_WEAVE_BACKOFF = "x-weave-backoff"; - public static final String X_BACKOFF = "x-backoff"; - public static final String X_LAST_MODIFIED = "x-last-modified"; - public static final String X_WEAVE_TIMESTAMP = "x-weave-timestamp"; - public static final String X_WEAVE_RECORDS = "x-weave-records"; - public static final String X_WEAVE_QUOTA_REMAINING = "x-weave-quota-remaining"; - public static final String X_WEAVE_ALERT = "x-weave-alert"; - public static final String X_WEAVE_NEXT_OFFSET = "x-weave-next-offset"; - - public SyncResponse(HttpResponse res) { - super(res); - } - - /** - * @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not - * present. - */ - public int weaveBackoffInSeconds() throws NumberFormatException { - return this.getIntegerHeader(X_WEAVE_BACKOFF); - } - - /** - * @return A number of seconds, or -1 if the 'X-Backoff' header was not - * present. - */ - public int xBackoffInSeconds() throws NumberFormatException { - return this.getIntegerHeader(X_BACKOFF); - } - - /** - * Extract a number of seconds, or -1 if none of the specified headers were present. - * - * @param includeRetryAfter - * if <code>true</code>, the Retry-After header is excluded. This is - * useful for processing non-error responses where a Retry-After - * header would be unexpected. - * @return the maximum of the three possible backoff headers, in seconds. - */ - public int totalBackoffInSeconds(boolean includeRetryAfter) { - int retryAfterInSeconds = -1; - if (includeRetryAfter) { - try { - retryAfterInSeconds = retryAfterInSeconds(); - } catch (NumberFormatException e) { - } - } - - int weaveBackoffInSeconds = -1; - try { - weaveBackoffInSeconds = weaveBackoffInSeconds(); - } catch (NumberFormatException e) { - } - - int backoffInSeconds = -1; - try { - backoffInSeconds = xBackoffInSeconds(); - } catch (NumberFormatException e) { - } - - int totalBackoff = Math.max(retryAfterInSeconds, Math.max(backoffInSeconds, weaveBackoffInSeconds)); - if (totalBackoff < 0) { - return -1; - } else { - return totalBackoff; - } - } - - /** - * @return A number of milliseconds, or -1 if neither the 'Retry-After', - * 'X-Backoff', or 'X-Weave-Backoff' header were present. - */ - public long totalBackoffInMilliseconds() { - long totalBackoff = totalBackoffInSeconds(true); - if (totalBackoff < 0) { - return -1; - } else { - return 1000 * totalBackoff; - } - } - - public long normalizedWeaveTimestamp() { - return normalizedTimestampForHeader(X_WEAVE_TIMESTAMP); - } - - /** - * Timestamps returned from a Sync server are decimal numbers of seconds, - * e.g., 1323393518.04. - * - * We want milliseconds since epoch. - * - * @return milliseconds since the epoch, as a long, or -1 if the header - * was missing or invalid. - */ - public long normalizedTimestampForHeader(String header) { - if (!this.hasHeader(header)) { - return -1; - } - - return Utils.decimalSecondsToMilliseconds( - this.response.getFirstHeader(header).getValue() - ); - } - - public int weaveRecords() throws NumberFormatException { - return this.getIntegerHeader(X_WEAVE_RECORDS); - } - - public int weaveQuotaRemaining() throws NumberFormatException { - return this.getIntegerHeader(X_WEAVE_QUOTA_REMAINING); - } - - public String weaveAlert() { - return this.getNonMissingHeader(X_WEAVE_ALERT); - } - - /** - * This header may be sent back with multi-record responses where the request included a limit parameter. - * Its presence indicates that the number of available records exceeded the given limit. - * The value from this header can be passed back in the offset parameter to retrieve additional records. - * The value of this header will always be a string of characters from the urlsafe-base64 alphabet. - * The specific contents of the string are an implementation detail of the server, - * so clients should treat it as an opaque token. - * - * @return the offset header - */ - public String weaveOffset() { - return this.getNonMissingHeader(X_WEAVE_NEXT_OFFSET); - } - - /** - * This header gives the last-modified time of the target resource as seen during processing of the request, - * and will be included in all success responses (200, 201, 204). - * When given in response to a write request, this will be equal to the server’s current time and - * to the new last-modified time of any BSOs created or changed by the request. - * It is similar to the standard HTTP Last-Modified header, - * but the value is a decimal timestamp rather than a HTTP-format date. - * - * @return the last modified header - */ - @Nullable - public String lastModified() { - return this.getNonMissingHeader(X_LAST_MODIFIED); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java deleted file mode 100644 index 3ae672f21..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URI; - -import org.mozilla.gecko.background.common.log.Logger; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -/** - * A request class that handles line-by-line responses. Eventually this will - * handle real stream processing; for now, just parse the returned body - * line-by-line. - * - * @author rnewman - * - */ -public class SyncStorageCollectionRequest extends SyncStorageRequest { - private static final String LOG_TAG = "CollectionRequest"; - - public SyncStorageCollectionRequest(URI uri) { - super(uri); - } - - protected volatile boolean aborting = false; - - /** - * Instruct the request that it should process no more records, - * and decline to notify any more delegate callbacks. - */ - public void abort() { - aborting = true; - try { - this.resource.request.abort(); - } catch (Exception e) { - // Just in case. - Logger.warn(LOG_TAG, "Got exception in abort: " + e); - } - } - - @Override - protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) { - return new SyncCollectionResourceDelegate((SyncStorageCollectionRequest) request); - } - - // TODO: this is awful. - public class SyncCollectionResourceDelegate extends - SyncStorageResourceDelegate { - - private static final String CONTENT_TYPE_INCREMENTAL = "application/newlines"; - private static final int FETCH_BUFFER_SIZE = 16 * 1024; // 16K chars. - - SyncCollectionResourceDelegate(SyncStorageCollectionRequest request) { - super(request); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - super.addHeaders(request, client); - request.setHeader("Accept", CONTENT_TYPE_INCREMENTAL); - // Caller is responsible for setting full=1. - } - - @Override - public void handleHttpResponse(HttpResponse response) { - if (aborting) { - return; - } - - if (response.getStatusLine().getStatusCode() != 200) { - super.handleHttpResponse(response); - return; - } - - HttpEntity entity = response.getEntity(); - Header contentType = entity.getContentType(); - if (!contentType.getValue().startsWith(CONTENT_TYPE_INCREMENTAL)) { - // Not incremental! - super.handleHttpResponse(response); - return; - } - - // TODO: at this point we can access X-Weave-Timestamp, compare - // that to our local timestamp, and compute an estimate of clock - // skew. We can provide this to the incremental delegate, which - // will allow it to seamlessly correct timestamps on the records - // it processes. Bug 721887. - - // Line-by-line processing, then invoke success. - SyncStorageCollectionRequestDelegate delegate = (SyncStorageCollectionRequestDelegate) this.request.delegate; - InputStream content = null; - BufferedReader br = null; - try { - content = entity.getContent(); - br = new BufferedReader(new InputStreamReader(content), FETCH_BUFFER_SIZE); - String line; - - // This relies on connection timeouts at the HTTP layer. - while (!aborting && - null != (line = br.readLine())) { - try { - delegate.handleRequestProgress(line); - } catch (Exception ex) { - delegate.handleRequestError(new HandleProgressException(ex)); - BaseResource.consumeEntity(entity); - return; - } - } - if (aborting) { - // So we don't hit the success case below. - return; - } - } catch (IOException ex) { - if (!aborting) { - delegate.handleRequestError(ex); - } - BaseResource.consumeEntity(entity); - return; - } finally { - // Attempt to close the stream and reader. - if (br != null) { - try { - br.close(); - } catch (IOException e) { - // We don't care if this fails. - } - } - } - // We're done processing the entity. Don't let fetching the body succeed! - BaseResource.consumeEntity(entity); - delegate.handleRequestSuccess(new SyncStorageResponse(response)); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java deleted file mode 100644 index ddf52007b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageCollectionRequestDelegate.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -public abstract class SyncStorageCollectionRequestDelegate implements - SyncStorageRequestIncrementalDelegate, SyncStorageRequestDelegate { -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java deleted file mode 100644 index c18c4fe15..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.sync.CryptoRecord; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; - -/** - * Resource class that implements expected headers and processing for Sync. - * Accepts a simplified delegate. - * - * Includes: - * * Basic Auth headers (via Resource) - * * Error responses: - * * 401 - * * 503 - * * Headers: - * * Retry-After - * * X-Weave-Backoff - * * X-Backoff - * * X-Weave-Records? - * * ... - * * Timeouts - * * Network errors - * * application/newlines - * * JSON parsing - * * Content-Type and Content-Length validation. - */ -public class SyncStorageRecordRequest extends SyncStorageRequest { - - public class SyncStorageRecordResourceDelegate extends SyncStorageResourceDelegate { - SyncStorageRecordResourceDelegate(SyncStorageRequest request) { - super(request); - } - } - - public SyncStorageRecordRequest(URI uri) { - super(uri); - } - - public SyncStorageRecordRequest(String url) throws URISyntaxException { - this(new URI(url)); - } - - @Override - protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) { - return new SyncStorageRecordResourceDelegate(request); - } - - @SuppressWarnings("unchecked") - public void post(JSONObject body) { - // Let's do this the trivial way for now. - // Note that POSTs should be an array, so we wrap here. - final JSONArray toPOST = new JSONArray(); - toPOST.add(body); - try { - this.resource.post(toPOST); - } catch (UnsupportedEncodingException e) { - this.delegate.handleRequestError(e); - } - } - - public void post(JSONArray body) { - // Let's do this the trivial way for now. - try { - this.resource.post(body); - } catch (UnsupportedEncodingException e) { - this.delegate.handleRequestError(e); - } - } - - public void put(JSONObject body) { - // Let's do this the trivial way for now. - try { - this.resource.put(body); - } catch (UnsupportedEncodingException e) { - this.delegate.handleRequestError(e); - } - } - - public void post(CryptoRecord record) { - this.post(record.toJSONObject()); - } - - public void put(CryptoRecord record) { - this.put(record.toJSONObject()); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java deleted file mode 100644 index 3ede9cded..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequest.java +++ /dev/null @@ -1,204 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.HashMap; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.SyncConstants; - -import ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; - -public class SyncStorageRequest implements Resource { - public static HashMap<String, String> SERVER_ERROR_MESSAGES; - static { - HashMap<String, String> errors = new HashMap<String, String>(); - - // Sync protocol errors. - errors.put("1", "Illegal method/protocol"); - errors.put("2", "Incorrect/missing CAPTCHA"); - errors.put("3", "Invalid/missing username"); - errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)"); - errors.put("5", "User ID does not match account in path"); - errors.put("6", "JSON parse failure"); - errors.put("7", "Missing password field"); - errors.put("8", "Invalid Weave Basic Object"); - errors.put("9", "Requested password not strong enough"); - errors.put("10", "Invalid/missing password reset code"); - errors.put("11", "Unsupported function"); - errors.put("12", "No email address on file"); - errors.put("13", "Invalid collection"); - errors.put("14", "User over quota"); - errors.put("15", "The email does not match the username"); - errors.put("16", "Client upgrade required"); - errors.put("255", "An unexpected server error occurred: pool is empty."); - - // Infrastructure-generated errors. - errors.put("\"server issue: getVS failed\"", "server issue: getVS failed"); - errors.put("\"server issue: prefix not set\"", "server issue: prefix not set"); - errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client"); - errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed"); - errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy"); - errors.put("\"server issue: database not in pool\"", "server issue: database not in pool"); - errors.put("\"server issue: database marked as down\"", "server issue: database marked as down"); - SERVER_ERROR_MESSAGES = errors; - } - public static String getServerErrorMessage(String body) { - if (SERVER_ERROR_MESSAGES.containsKey(body)) { - return SERVER_ERROR_MESSAGES.get(body); - } - return body; - } - - /** - * @param uri - * @throws URISyntaxException - */ - public SyncStorageRequest(String uri) throws URISyntaxException { - this(new URI(uri)); - } - - /** - * @param uri - */ - public SyncStorageRequest(URI uri) { - this.resource = new BaseResource(uri); - this.resourceDelegate = this.makeResourceDelegate(this); - this.resource.delegate = this.resourceDelegate; - } - - @Override - public URI getURI() { - return this.resource.getURI(); - } - - @Override - public String getURIString() { - return this.resource.getURIString(); - } - - @Override - public String getHostname() { - return this.resource.getHostname(); - } - - /** - * A ResourceDelegate that mediates between Resource-level notifications and the SyncStorageRequest. - */ - public class SyncStorageResourceDelegate extends BaseResourceDelegate { - private static final String LOG_TAG = "SSResourceDelegate"; - protected SyncStorageRequest request; - - SyncStorageResourceDelegate(SyncStorageRequest request) { - super(request); - this.request = request; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return request.delegate.getAuthHeaderProvider(); - } - - @Override - public String getUserAgent() { - return SyncConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(HttpResponse response) { - Logger.debug(LOG_TAG, "SyncStorageResourceDelegate handling response: " + response.getStatusLine() + "."); - SyncStorageRequestDelegate d = this.request.delegate; - SyncStorageResponse res = new SyncStorageResponse(response); - // It is the responsibility of the delegate handlers to completely consume the response. - // In context of a Sync storage response, success is either a 200 OK or 202 Accepted. - // 202 is returned during uploads of data in a batching mode, indicating that more is expected. - if (res.getStatusCode() == 200 || res.getStatusCode() == 202) { - d.handleRequestSuccess(res); - } else { - Logger.warn(LOG_TAG, "HTTP request failed."); - try { - Logger.warn(LOG_TAG, "HTTP response body: " + res.getErrorMessage()); - } catch (Exception e) { - Logger.error(LOG_TAG, "Can't fetch HTTP response body.", e); - } - d.handleRequestFailure(res); - } - } - - @Override - public void handleHttpProtocolException(ClientProtocolException e) { - this.request.delegate.handleRequestError(e); - } - - @Override - public void handleHttpIOException(IOException e) { - this.request.delegate.handleRequestError(e); - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - this.request.delegate.handleRequestError(e); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - // Clients can use their delegate interface to specify X-If-Unmodified-Since. - String ifUnmodifiedSince = this.request.delegate.ifUnmodifiedSince(); - if (ifUnmodifiedSince != null) { - Logger.debug(LOG_TAG, "Making request with X-If-Unmodified-Since = " + ifUnmodifiedSince); - request.setHeader("x-if-unmodified-since", ifUnmodifiedSince); - } - if (request.getMethod().equalsIgnoreCase("DELETE")) { - request.addHeader("x-confirm-delete", "1"); - } - } - } - - protected BaseResourceDelegate resourceDelegate; - public SyncStorageRequestDelegate delegate; - protected BaseResource resource; - - public SyncStorageRequest() { - super(); - } - - // Default implementation. Override this. - protected BaseResourceDelegate makeResourceDelegate(SyncStorageRequest request) { - return new SyncStorageResourceDelegate(request); - } - - @Override - public void get() { - this.resource.get(); - } - - @Override - public void delete() { - this.resource.delete(); - } - - @Override - public void post(HttpEntity body) { - this.resource.post(body); - } - - @Override - public void patch(HttpEntity body) { - this.resource.patch(body); - } - - @Override - public void put(HttpEntity body) { - this.resource.put(body); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java deleted file mode 100644 index 29f42cc28..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestDelegate.java +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -public interface SyncStorageRequestDelegate { - public AuthHeaderProvider getAuthHeaderProvider(); - - String ifUnmodifiedSince(); - - // TODO: at this point we can access X-Weave-Timestamp, compare - // that to our local timestamp, and compute an estimate of clock - // skew. Bug 721887. - - /** - * Override this to handle a successful SyncStorageRequest. - * - * SyncStorageResourceDelegate implementers <b>must</b> ensure that the HTTP - * responses underlying SyncStorageResponses are fully consumed to ensure that - * connections are returned to the pool, for example by calling - * <code>BaseResource.consumeEntity(response)</code>. - */ - void handleRequestSuccess(SyncStorageResponse response); - - /** - * Override this to handle a failed SyncStorageRequest. - * - * - * SyncStorageResourceDelegate implementers <b>must</b> ensure that the HTTP - * responses underlying SyncStorageResponses are fully consumed to ensure that - * connections are returned to the pool, for example by calling - * <code>BaseResource.consumeEntity(response)</code>. - */ - void handleRequestFailure(SyncStorageResponse response); - - void handleRequestError(Exception ex); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java deleted file mode 100644 index aa5d735bf..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRequestIncrementalDelegate.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -public interface SyncStorageRequestIncrementalDelegate { - void handleRequestProgress(String progress); // For line-by-line. -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java deleted file mode 100644 index 644df314c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageResponse.java +++ /dev/null @@ -1,85 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.IOException; -import java.util.HashMap; - -import org.mozilla.gecko.background.common.log.Logger; - -import ch.boye.httpclientandroidlib.HttpResponse; - -public class SyncStorageResponse extends SyncResponse { - private static final String LOG_TAG = "SyncStorageResponse"; - - // Responses that are actionable get constant status codes. - public static final String RESPONSE_CLIENT_UPGRADE_REQUIRED = "16"; - - public static HashMap<String, String> SERVER_ERROR_MESSAGES; - static { - HashMap<String, String> errors = new HashMap<String, String>(); - - // Sync protocol errors. - errors.put("1", "Illegal method/protocol"); - errors.put("2", "Incorrect/missing CAPTCHA"); - errors.put("3", "Invalid/missing username"); - errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)"); - errors.put("5", "User ID does not match account in path"); - errors.put("6", "JSON parse failure"); - errors.put("7", "Missing password field"); - errors.put("8", "Invalid Weave Basic Object"); - errors.put("9", "Requested password not strong enough"); - errors.put("10", "Invalid/missing password reset code"); - errors.put("11", "Unsupported function"); - errors.put("12", "No email address on file"); - errors.put("13", "Invalid collection"); - errors.put("14", "User over quota"); - errors.put("15", "The email does not match the username"); - errors.put(RESPONSE_CLIENT_UPGRADE_REQUIRED, "Client upgrade required"); - errors.put("255", "An unexpected server error occurred: pool is empty."); - - // Infrastructure-generated errors. - errors.put("\"server issue: getVS failed\"", "server issue: getVS failed"); - errors.put("\"server issue: prefix not set\"", "server issue: prefix not set"); - errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client"); - errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed"); - errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy"); - errors.put("\"server issue: database not in pool\"", "server issue: database not in pool"); - errors.put("\"server issue: database marked as down\"", "server issue: database marked as down"); - SERVER_ERROR_MESSAGES = errors; - } - public static String getServerErrorMessage(String body) { - Logger.debug(LOG_TAG, "Looking up message for body \"" + body + "\""); - if (SERVER_ERROR_MESSAGES.containsKey(body)) { - return SERVER_ERROR_MESSAGES.get(body); - } - return body; - } - - - public SyncStorageResponse(HttpResponse res) { - super(res); - } - - public String getErrorMessage() throws IllegalStateException, IOException { - return SyncStorageResponse.getServerErrorMessage(this.body().trim()); - } - - /** - * This header gives the last-modified time of the target resource as seen during processing of - * the request, and will be included in all success responses (200, 201, 204). - * When given in response to a write request, this will be equal to the server’s current time and - * to the new last-modified time of any BSOs created or changed by the request. - */ - public String getLastModified() { - if (!response.containsHeader(X_LAST_MODIFIED)) { - return null; - } - return response.getFirstHeader(X_LAST_MODIFIED).getValue(); - } - - // TODO: Content-Type and Content-Length validation. - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java deleted file mode 100644 index dd68c0515..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/TLSSocketFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import java.io.IOException; -import java.net.Socket; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; - -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.background.common.log.Logger; - -import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory; -import ch.boye.httpclientandroidlib.params.HttpParams; - -public class TLSSocketFactory extends SSLSocketFactory { - private static final String LOG_TAG = "TLSSocketFactory"; - - // Guarded by `this`. - private static String[] cipherSuites = GlobalConstants.DEFAULT_CIPHER_SUITES; - - public TLSSocketFactory(SSLContext sslContext) { - super(sslContext); - } - - /** - * Attempt to specify the cipher suites to use for a connection. If - * setting fails (as it will on Android 2.2, because the wrong names - * are in use to specify ciphers), attempt to set the defaults. - * - * We store the list of cipher suites in `cipherSuites`, which - * avoids this fallback handling having to be executed more than once. - * - * This method is synchronized to ensure correct use of that member. - * - * See Bug 717691 for more details. - * - * @param socket - * The SSLSocket on which to operate. - */ - public static synchronized void setEnabledCipherSuites(SSLSocket socket) { - try { - socket.setEnabledCipherSuites(cipherSuites); - } catch (IllegalArgumentException e) { - cipherSuites = socket.getSupportedCipherSuites(); - Logger.warn(LOG_TAG, "Setting enabled cipher suites failed: " + e.getMessage()); - Logger.warn(LOG_TAG, "Using " + cipherSuites.length + " supported suites."); - socket.setEnabledCipherSuites(cipherSuites); - } - } - - @Override - public Socket createSocket(HttpParams params) throws IOException { - SSLSocket socket = (SSLSocket) super.createSocket(params); - socket.setEnabledProtocols(GlobalConstants.DEFAULT_PROTOCOLS); - setEnabledCipherSuites(socket); - return socket; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java deleted file mode 100644 index 2e26f041b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBOCollectionRequestDelegate.java +++ /dev/null @@ -1,35 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.KeyBundleProvider; - -/** - * Subclass this to handle collection fetches. - * @author rnewman - * - */ -public abstract class WBOCollectionRequestDelegate -extends SyncStorageCollectionRequestDelegate -implements KeyBundleProvider { - - @Override - public abstract KeyBundle keyBundle(); - public abstract void handleWBO(CryptoRecord record); - - @Override - public void handleRequestProgress(String progress) { - try { - CryptoRecord record = CryptoRecord.fromJSONRecord(progress); - record.keyBundle = this.keyBundle(); - this.handleWBO(record); - } catch (Exception e) { - this.handleRequestError(e); - // TODO: abort?! Allow exception to propagate to fail? - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java deleted file mode 100644 index 8a09e0c7f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/WBORequestDelegate.java +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.net; - -import org.mozilla.gecko.sync.KeyBundleProvider; -import org.mozilla.gecko.sync.crypto.KeyBundle; - -public abstract class WBORequestDelegate -implements SyncStorageRequestDelegate, KeyBundleProvider { - @Override - public abstract KeyBundle keyBundle(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java deleted file mode 100644 index 5fe3dc9fa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class BookmarkNeedsReparentingException extends SyncException { - - private static final long serialVersionUID = -7018336108709392800L; - - public BookmarkNeedsReparentingException(Exception ex) { - super(ex); - } - -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java deleted file mode 100644 index 289fc48ec..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -/** - * Shared interface for repositories that consume and produce - * bookmark records. - * - * @author rnewman - * - */ -public interface BookmarksRepository { - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java deleted file mode 100644 index a6dc3f6b8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; - -/** - * A kind of Server11Repository that supports explicit setting of total fetch limit, per-batch fetch limit, and a sort order. - * - * @author rnewman - * - */ -public class ConstrainedServer11Repository extends Server11Repository { - - private final String sort; - private final long batchLimit; - private final long totalLimit; - - public ConstrainedServer11Repository(String collection, String storageURL, - AuthHeaderProvider authHeaderProvider, - InfoCollections infoCollections, - InfoConfiguration infoConfiguration, - long batchLimit, long totalLimit, String sort) - throws URISyntaxException { - super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration); - this.batchLimit = batchLimit; - this.totalLimit = totalLimit; - this.sort = sort; - } - - @Override - public String getDefaultSort() { - return sort; - } - - @Override - public long getDefaultBatchLimit() { - return batchLimit; - } - - @Override - public long getDefaultTotalLimit() { - return totalLimit; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java deleted file mode 100644 index 8b29a37ba..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class FetchFailedException extends SyncException { - private static final long serialVersionUID = -7533105300182522946L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java deleted file mode 100644 index 3b6facc31..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java +++ /dev/null @@ -1,61 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.HashSet; -import java.util.Iterator; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class HashSetStoreTracker implements StoreTracker { - - // Guarded by `this`. - // Used to store GUIDs that were not locally modified but - // have been modified by a call to `store`, and thus - // should not be returned by a subsequent fetch. - private final HashSet<String> guids; - - public HashSetStoreTracker() { - guids = new HashSet<String>(); - } - - @Override - public String toString() { - return "#<Tracker: " + guids.size() + " guids tracked.>"; - } - - @Override - public synchronized boolean trackRecordForExclusion(String guid) { - return (guid != null) && guids.add(guid); - } - - @Override - public synchronized boolean isTrackedForExclusion(String guid) { - return (guid != null) && guids.contains(guid); - } - - @Override - public synchronized boolean untrackStoredForExclusion(String guid) { - return (guid != null) && guids.remove(guid); - } - - @Override - public RecordFilter getFilter() { - if (guids.size() == 0) { - return null; - } - return new RecordFilter() { - @Override - public boolean excludeRecord(Record r) { - return isTrackedForExclusion(r.guid); - } - }; - } - - @Override - public Iterator<String> recordsTrackedForExclusion() { - return this.guids.iterator(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java deleted file mode 100644 index eddc32102..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -/** - * Shared interface for repositories that consume and produce - * history records. - * - * @author rnewman - * - */ -public interface HistoryRepository { - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java deleted file mode 100644 index acedc66e2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class IdentityRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - return record; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java deleted file mode 100644 index 185f0d724..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InactiveSessionException extends SyncException { - - private static final long serialVersionUID = 537241160815940991L; - - public InactiveSessionException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java deleted file mode 100644 index 3597276a4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidBookmarkTypeException extends SyncException { - - private static final long serialVersionUID = -6098516814844387449L; - - public InvalidBookmarkTypeException(Exception e) { - super(e); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java deleted file mode 100644 index 3f761e540..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidRequestException extends SyncException { - - private static final long serialVersionUID = 4502951350743608243L; - - public InvalidRequestException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java deleted file mode 100644 index 0963892c9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidSessionTransitionException extends SyncException { - - private static final long serialVersionUID = 4157729859314427281L; - - public InvalidSessionTransitionException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java deleted file mode 100644 index 58cca4a49..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class MultipleRecordsForGuidException extends SyncException { - - private static final long serialVersionUID = 7426987323485324741L; - - public MultipleRecordsForGuidException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java deleted file mode 100644 index 85d119a5d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -import android.net.Uri; - -/** - * Raised when a Content Provider cannot be retrieved. - * - * @author rnewman - * - */ -public class NoContentProviderException extends SyncException { - private static final long serialVersionUID = 1L; - - public final Uri requestedProvider; - public NoContentProviderException(Uri requested) { - super(); - this.requestedProvider = requested; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java deleted file mode 100644 index 3681deffd..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NoGuidForIdException extends SyncException { - - private static final long serialVersionUID = -675614284405829041L; - - public NoGuidForIdException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java deleted file mode 100644 index 5747039aa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NoStoreDelegateException extends SyncException { - private static final long serialVersionUID = 6631689468978422074L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java deleted file mode 100644 index 4d9057992..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NullCursorException extends SyncException { - - private static final long serialVersionUID = 3146506225701104661L; - - public NullCursorException(Exception e) { - super(e); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java deleted file mode 100644 index 991fd7426..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class ParentNotFoundException extends SyncException { - - private static final long serialVersionUID = -2687003621705922982L; - - public ParentNotFoundException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java deleted file mode 100644 index 0f8075133..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class ProfileDatabaseException extends SyncException { - - private static final long serialVersionUID = -4916908502042261602L; - - public ProfileDatabaseException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java deleted file mode 100644 index 6a8d81a77..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -// Take a record retrieved from some middleware, producing -// some concrete record type for application to some local repository. -public abstract class RecordFactory { - public abstract Record createRecord(Record record); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java deleted file mode 100644 index 733448ded..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public interface RecordFilter { - public boolean excludeRecord(Record r); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java deleted file mode 100644 index 3dd3fd2c4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public abstract class Repository { - public abstract void createSession(RepositorySessionCreationDelegate delegate, Context context); - - public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) { - delegate.onCleaned(this); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java deleted file mode 100644 index 84fca1379..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java +++ /dev/null @@ -1,384 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -/** - * A <code>RepositorySession</code> is created and used thusly: - * - *<ul> - * <li>Construct, with a reference to its parent {@link Repository}, by calling - * {@link Repository#createSession(RepositorySessionCreationDelegate, android.content.Context)}.</li> - * <li>Populate with saved information by calling {@link #unbundle(RepositorySessionBundle)}.</li> - * <li>Begin a sync by calling {@link #begin(RepositorySessionBeginDelegate)}. <code>begin()</code> - * is an appropriate place to initialize expensive resources.</li> - * <li>Perform operations such as {@link #fetchSince(long, RepositorySessionFetchRecordsDelegate)} and - * {@link #store(Record)}.</li> - * <li>Finish by calling {@link #finish(RepositorySessionFinishDelegate)}, retrieving and storing - * the current bundle.</li> - *</ul> - * - * If <code>finish()</code> is not called, {@link #abort()} must be called. These calls must - * <em>always</em> be paired with <code>begin()</code>. - * - */ -public abstract class RepositorySession { - - public enum SessionStatus { - UNSTARTED, - ACTIVE, - ABORTED, - DONE - } - - private static final String LOG_TAG = "RepositorySession"; - - protected static void trace(String message) { - Logger.trace(LOG_TAG, message); - } - - private SessionStatus status = SessionStatus.UNSTARTED; - protected Repository repository; - protected RepositorySessionStoreDelegate delegate; - - /** - * A queue of Runnables which call out into delegates. - */ - protected ExecutorService delegateQueue = Executors.newSingleThreadExecutor(); - - /** - * A queue of Runnables which effect storing. - * This includes actual store work, and also the consequences of storeDone. - * This provides strict ordering. - */ - protected ExecutorService storeWorkQueue = Executors.newSingleThreadExecutor(); - - // The time that the last sync on this collection completed, in milliseconds since epoch. - private long lastSyncTimestamp = 0; - - public long getLastSyncTimestamp() { - return lastSyncTimestamp; - } - - public static long now() { - return System.currentTimeMillis(); - } - - public RepositorySession(Repository repository) { - this.repository = repository; - } - - public abstract void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate); - public abstract void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate delegate); - public abstract void fetch(String[] guids, RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException; - public abstract void fetchAll(RepositorySessionFetchRecordsDelegate delegate); - - /** - * Override this if you wish to short-circuit a sync when you know -- - * e.g., by inspecting the database or info/collections -- that no new - * data are available. - * - * @return true if a sync should proceed. - */ - public boolean dataAvailable() { - return true; - } - - /** - * @return true if we cannot safely sync from this <code>RepositorySession</code>. - */ - public boolean shouldSkip() { - return false; - } - - /* - * Store operations proceed thusly: - * - * * Set a delegate - * * Store an arbitrary number of records. At any time the delegate can be - * notified of an error. - * * Call storeDone to notify the session that no more items are forthcoming. - * * The store delegate will be notified of error or completion. - * - * This arrangement of calls allows for batching at the session level. - * - * Store success calls are not guaranteed. - */ - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - Logger.debug(LOG_TAG, "Setting store delegate to " + delegate); - this.delegate = delegate; - } - public abstract void store(Record record) throws NoStoreDelegateException; - - public void storeDone() { - // Our default behavior will be to assume that the Runnable is - // executed as soon as all the stores synchronously finish, so - // our end timestamp can just be… now. - storeDone(now()); - } - - public void storeDone(final long end) { - Logger.debug(LOG_TAG, "Scheduling onStoreCompleted for after storing is done: " + end); - Runnable command = new Runnable() { - @Override - public void run() { - delegate.onStoreCompleted(end); - } - }; - storeWorkQueue.execute(command); - } - - public abstract void wipe(RepositorySessionWipeDelegate delegate); - - /** - * Synchronously perform the shared work of beginning. Throws on failure. - * @throws InvalidSessionTransitionException - * - */ - protected void sharedBegin() throws InvalidSessionTransitionException { - Logger.debug(LOG_TAG, "Shared begin."); - if (delegateQueue.isShutdown()) { - throw new InvalidSessionTransitionException(null); - } - if (storeWorkQueue.isShutdown()) { - throw new InvalidSessionTransitionException(null); - } - this.transitionFrom(SessionStatus.UNSTARTED, SessionStatus.ACTIVE); - } - - /** - * Start the session. This is an appropriate place to initialize - * data access components such as database handles. - * - * @param delegate - * @throws InvalidSessionTransitionException - */ - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - sharedBegin(); - delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this); - } - - public void unbundle(RepositorySessionBundle bundle) { - this.lastSyncTimestamp = bundle == null ? 0 : bundle.getTimestamp(); - } - - /** - * Override this in your subclasses to return values to save between sessions. - * Note that RepositorySession automatically bumps the timestamp to the time - * the last sync began. If unbundled but not begun, this will be the same as the - * value in the input bundle. - * - * The Synchronizer most likely wants to bump the bundle timestamp to be a value - * return from a fetch call. - */ - protected RepositorySessionBundle getBundle() { - // Why don't we just persist the old bundle? - long timestamp = getLastSyncTimestamp(); - RepositorySessionBundle bundle = new RepositorySessionBundle(timestamp); - Logger.debug(LOG_TAG, "Setting bundle timestamp to " + timestamp + "."); - - return bundle; - } - - /** - * Just like finish(), but doesn't do any work that should only be performed - * at the end of a successful sync, and can be called any time. - */ - public void abort(RepositorySessionFinishDelegate delegate) { - this.abort(); - delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle()); - } - - /** - * Abnormally terminate the repository session, freeing or closing - * any resources that were opened during the lifetime of the session. - */ - public void abort() { - // TODO: do something here. - this.setStatus(SessionStatus.ABORTED); - try { - storeWorkQueue.shutdownNow(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Caught exception shutting down store work queue.", e); - } - try { - delegateQueue.shutdown(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Caught exception shutting down delegate queue.", e); - } - } - - /** - * End the repository session, freeing or closing any resources - * that were opened during the lifetime of the session. - * - * @param delegate notified of success or failure. - * @throws InactiveSessionException - */ - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - try { - this.transitionFrom(SessionStatus.ACTIVE, SessionStatus.DONE); - delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle()); - } catch (InvalidSessionTransitionException e) { - Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session"); - throw new InactiveSessionException(e); - } - - Logger.trace(LOG_TAG, "Shutting down work queues."); - storeWorkQueue.shutdown(); - delegateQueue.shutdown(); - } - - /** - * Run the provided command if we're active and our delegate queue - * is not shut down. - */ - protected synchronized void executeDelegateCommand(Runnable command) - throws InactiveSessionException { - if (!isActive() || delegateQueue.isShutdown()) { - throw new InactiveSessionException(null); - } - delegateQueue.execute(command); - } - - public synchronized void ensureActive() throws InactiveSessionException { - if (!isActive()) { - throw new InactiveSessionException(null); - } - } - - public synchronized boolean isActive() { - return status == SessionStatus.ACTIVE; - } - - public synchronized SessionStatus getStatus() { - return status; - } - - public synchronized void setStatus(SessionStatus status) { - this.status = status; - } - - public synchronized void transitionFrom(SessionStatus from, SessionStatus to) throws InvalidSessionTransitionException { - if (from == null || this.status == from) { - Logger.trace(LOG_TAG, "Successfully transitioning from " + this.status + " to " + to); - - this.status = to; - return; - } - Logger.warn(LOG_TAG, "Wanted to transition from " + from + " but in state " + this.status); - throw new InvalidSessionTransitionException(null); - } - - /** - * Produce a record that is some combination of the remote and local records - * provided. - * - * The returned record must be produced without mutating either remoteRecord - * or localRecord. It is acceptable to return either remoteRecord or localRecord - * if no modifications are to be propagated. - * - * The returned record *should* have the local androidID and the remote GUID, - * and some optional merge of data from the two records. - * - * This method can be called with records that are identical, or differ in - * any regard. - * - * This method will not be called if: - * - * * either record is marked as deleted, or - * * there is no local mapping for a new remote record. - * - * Otherwise, it will be called precisely once. - * - * Side-effects (e.g., for transactional storage) can be hooked in here. - * - * @param remoteRecord - * The record retrieved from upstream, already adjusted for clock skew. - * @param localRecord - * The record retrieved from local storage. - * @param lastRemoteRetrieval - * The timestamp of the last retrieved set of remote records, adjusted for - * clock skew. - * @param lastLocalRetrieval - * The timestamp of the last retrieved set of local records. - * @return - * A Record instance to apply, or null to apply nothing. - */ - protected Record reconcileRecords(final Record remoteRecord, - final Record localRecord, - final long lastRemoteRetrieval, - final long lastLocalRetrieval) { - Logger.debug(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid); - - if (localRecord.equalPayloads(remoteRecord)) { - if (remoteRecord.lastModified > localRecord.lastModified) { - Logger.debug(LOG_TAG, "Records are equal. No record application needed."); - return null; - } - - // Local wins. - return null; - } - - // TODO: Decide what to do based on: - // * Which of the two records is modified; - // * Whether they are equal or congruent; - // * The modified times of each record (interpreted through the lens of clock skew); - // * ... - boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified; - Logger.debug(LOG_TAG, "Local record is more recent? " + localIsMoreRecent); - Record donor = localIsMoreRecent ? localRecord : remoteRecord; - - // Modify the local record to match the remote record's GUID and values. - // Preserve the local Android ID, and merge data where possible. - // It sure would be nice if copyWithIDs didn't give a shit about androidID, mm? - Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID); - - // We don't want to upload the record if the remote record was - // applied without changes. - // This logic will become more complicated as reconciling becomes smarter. - if (!localIsMoreRecent) { - trackGUID(out.guid); - } - return out; - } - - /** - * Depending on the RepositorySession implementation, track - * that a record — most likely a brand-new record that has been - * applied unmodified — should be tracked so as to not be uploaded - * redundantly. - * - * The default implementations do nothing. - */ - protected void trackGUID(String guid) { - } - - protected synchronized void untrackGUIDs(Collection<String> guids) { - } - - protected void untrackGUID(String guid) { - } - - // Ah, Java. You wretched creature. - public Iterator<String> getTrackedRecordIDs() { - return new ArrayList<String>().iterator(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java deleted file mode 100644 index 7908ec797..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java +++ /dev/null @@ -1,55 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; - -import java.io.IOException; - -public class RepositorySessionBundle { - public static final String LOG_TAG = RepositorySessionBundle.class.getSimpleName(); - - protected static final String JSON_KEY_TIMESTAMP = "timestamp"; - - protected final ExtendedJSONObject object; - - public RepositorySessionBundle(String jsonString) throws IOException, NonObjectJSONException { - - object = new ExtendedJSONObject(jsonString); - } - - public RepositorySessionBundle(long lastSyncTimestamp) { - object = new ExtendedJSONObject(); - this.setTimestamp(lastSyncTimestamp); - } - - public long getTimestamp() { - if (object.containsKey(JSON_KEY_TIMESTAMP)) { - return object.getLong(JSON_KEY_TIMESTAMP); - } - - return -1; - } - - public void setTimestamp(long timestamp) { - Logger.debug(LOG_TAG, "Setting timestamp to " + timestamp + "."); - object.put(JSON_KEY_TIMESTAMP, timestamp); - } - - public void bumpTimestamp(long timestamp) { - long existing = this.getTimestamp(); - if (timestamp > existing) { - this.setTimestamp(timestamp); - } else { - Logger.debug(LOG_TAG, "Timestamp " + timestamp + " not greater than " + existing + "; not bumping."); - } - } - - public String toJSONString() { - return object.toJSONString(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java deleted file mode 100644 index 4404fda25..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java +++ /dev/null @@ -1,144 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; - -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** - * A Server11Repository implements fetching and storing against the Sync 1.1 API. - * It doesn't do crypto: that's the job of the middleware. - * - * @author rnewman - */ -public class Server11Repository extends Repository { - protected String collection; - protected URI collectionURI; - protected final AuthHeaderProvider authHeaderProvider; - protected final InfoCollections infoCollections; - - private final InfoConfiguration infoConfiguration; - - /** - * Construct a new repository that fetches and stores against the Sync 1.1. API. - * - * @param collection name. - * @param storageURL full URL to storage endpoint. - * @param authHeaderProvider to use in requests; may be null. - * @param infoCollections instance; must not be null. - * @throws URISyntaxException - */ - public Server11Repository(@NonNull String collection, @NonNull String storageURL, AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections, @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException { - if (collection == null) { - throw new IllegalArgumentException("collection must not be null"); - } - if (storageURL == null) { - throw new IllegalArgumentException("storageURL must not be null"); - } - if (infoCollections == null) { - throw new IllegalArgumentException("infoCollections must not be null"); - } - this.collection = collection; - this.collectionURI = new URI(storageURL + (storageURL.endsWith("/") ? collection : "/" + collection)); - this.authHeaderProvider = authHeaderProvider; - this.infoCollections = infoCollections; - this.infoConfiguration = infoConfiguration; - } - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - delegate.onSessionCreated(new Server11RepositorySession(this)); - } - - public URI collectionURI() { - return this.collectionURI; - } - - public URI collectionURI(boolean full, long newer, long limit, String sort, String ids, String offset) throws URISyntaxException { - ArrayList<String> params = new ArrayList<String>(); - if (full) { - params.add("full=1"); - } - if (newer >= 0) { - // Translate local millisecond timestamps into server decimal seconds. - String newerString = Utils.millisecondsToDecimalSecondsString(newer); - params.add("newer=" + newerString); - } - if (limit > 0) { - params.add("limit=" + limit); - } - if (sort != null) { - params.add("sort=" + sort); // We trust these values. - } - if (ids != null) { - params.add("ids=" + ids); // We trust these values. - } - if (offset != null) { - // Offset comes straight out of HTTP headers and it is the responsibility of the caller to URI-escape it. - params.add("offset=" + offset); - } - if (params.size() == 0) { - return this.collectionURI; - } - - StringBuilder out = new StringBuilder(); - char indicator = '?'; - for (String param : params) { - out.append(indicator); - indicator = '&'; - out.append(param); - } - String uri = this.collectionURI + out.toString(); - return new URI(uri); - } - - public URI wboURI(String id) throws URISyntaxException { - return new URI(this.collectionURI + "/" + id); - } - - // Override these. - @SuppressWarnings("static-method") - public long getDefaultBatchLimit() { - return -1; - } - - @SuppressWarnings("static-method") - public String getDefaultSort() { - return null; - } - - public long getDefaultTotalLimit() { - return -1; - } - - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - - public boolean updateNeeded(long lastSyncTimestamp) { - return infoCollections.updateNeeded(collection, lastSyncTimestamp); - } - - @Nullable - public Long getCollectionLastModified() { - return infoCollections.getTimestamp(collection); - } - - public InfoConfiguration getInfoConfiguration() { - return infoConfiguration; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java deleted file mode 100644 index 20c735a6b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java +++ /dev/null @@ -1,104 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; -import org.mozilla.gecko.sync.repositories.downloaders.BatchingDownloader; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader; - -public class Server11RepositorySession extends RepositorySession { - public static final String LOG_TAG = "Server11Session"; - - Server11Repository serverRepository; - private BatchingUploader uploader; - private final BatchingDownloader downloader; - - public Server11RepositorySession(Repository repository) { - super(repository); - serverRepository = (Server11Repository) repository; - this.downloader = new BatchingDownloader(serverRepository, this); - } - - public Server11Repository getServerRepository() { - return serverRepository; - } - - @Override - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - this.delegate = delegate; - - // Now that we have the delegate, we can initialize our uploader. - this.uploader = new BatchingUploader(this, storeWorkQueue, delegate); - } - - @Override - public void guidsSince(long timestamp, - RepositorySessionGuidsSinceDelegate delegate) { - // TODO Auto-generated method stub - - } - - @Override - public void fetchSince(long timestamp, - RepositorySessionFetchRecordsDelegate delegate) { - this.downloader.fetchSince(timestamp, delegate); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - this.fetchSince(-1, delegate); - } - - @Override - public void fetch(String[] guids, - RepositorySessionFetchRecordsDelegate delegate) { - this.downloader.fetch(guids, delegate); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - // TODO: implement wipe. - } - - @Override - public void store(Record record) throws NoStoreDelegateException { - if (delegate == null) { - throw new NoStoreDelegateException(); - } - - // If delegate was set, this shouldn't happen. - if (uploader == null) { - throw new IllegalStateException("Uploader haven't been initialized"); - } - - uploader.process(record); - } - - @Override - public void storeDone() { - Logger.debug(LOG_TAG, "storeDone()."); - - // If delegate was set, this shouldn't happen. - if (uploader == null) { - throw new IllegalStateException("Uploader haven't been initialized"); - } - - uploader.noMoreRecordsToUpload(); - } - - @Override - public boolean dataAvailable() { - return serverRepository.updateNeeded(getLastSyncTimestamp()); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java deleted file mode 100644 index fcb09e32e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class StoreFailedException extends SyncException { - private static final long serialVersionUID = 6080340122855859752L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java deleted file mode 100644 index b6a3071a9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.Iterator; - -/** - * Our hacky version of transactional semantics. The goal is to prevent - * the following situation: - * - * * AAA is not modified locally. - * * A modified AAA is downloaded during the storing phase. Its local - * timestamp is advanced. - * * The direction of syncing changes, and AAA is now uploaded to the server. - * - * The following situation should still be supported: - * - * * AAA is not modified locally. - * * A modified AAA is downloaded and merged with the local AAA. - * * The merged AAA is uploaded to the server. - * - * As should: - * - * * AAA is modified locally. - * * A modified AAA is downloaded, and discarded or merged. - * * The current version of AAA is uploaded to the server. - * - * We achieve this by tracking GUIDs during the storing phase. If we - * apply a record such that the local copy is substantially the same - * as the record we just downloaded, we add it to a list of records - * to avoid uploading. The definition of "substantially the same" - * depends on the particular repository. The only consideration is "do we - * want to upload this record in this sync?". - * - * Note that items are removed from this list when a fetch that - * considers them for upload completes successfully. The entire list - * is discarded when the session is completed. - * - * This interface exposes methods to: - * - * * During a store, recording that a record has been stored, and should - * thus not be returned in subsequent fetches; - * * During a fetch, checking whether a record should be returned. - * - * In the future this might also grow self-persistence. - * - * See also RepositorySession.trackRecord. - * - * @author rnewman - * - */ -public interface StoreTracker { - - /** - * @param guid - * The GUID of the item to track. - * @return - * Whether the GUID was a newly tracked value. - */ - public boolean trackRecordForExclusion(String guid); - - /** - * @param guid - * The GUID of the item to check. - * @return - * true if the item is already tracked. - */ - public boolean isTrackedForExclusion(String guid); - - /** - * - * @param guid - * @return true if the specified GUID was removed from the tracked set. - */ - public boolean untrackStoredForExclusion(String guid); - - public RecordFilter getFilter(); - - public Iterator<String> recordsTrackedForExclusion(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java deleted file mode 100644 index 1a5c1e96a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java +++ /dev/null @@ -1,102 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.Collection; -import java.util.Iterator; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -public abstract class StoreTrackingRepositorySession extends RepositorySession { - private static final String LOG_TAG = "StoreTrackSession"; - protected StoreTracker storeTracker; - - protected static StoreTracker createStoreTracker() { - return new HashSetStoreTracker(); - } - - public StoreTrackingRepositorySession(Repository repository) { - super(repository); - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue); - try { - super.sharedBegin(); - } catch (InvalidSessionTransitionException e) { - deferredDelegate.onBeginFailed(e); - return; - } - // Or do this in your own subclass. - storeTracker = createStoreTracker(); - deferredDelegate.onBeginSucceeded(this); - } - - @Override - protected synchronized void trackGUID(String guid) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - this.storeTracker.trackRecordForExclusion(guid); - } - - @Override - protected synchronized void untrackGUID(String guid) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - this.storeTracker.untrackStoredForExclusion(guid); - } - - @Override - protected synchronized void untrackGUIDs(Collection<String> guids) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - if (guids == null) { - return; - } - for (String guid : guids) { - this.storeTracker.untrackStoredForExclusion(guid); - } - } - - protected void trackRecord(Record record) { - - Logger.debug(LOG_TAG, "Tracking record " + record.guid + - " (" + record.lastModified + ") to avoid re-upload."); - // Future: we care about the timestamp… - trackGUID(record.guid); - } - - protected void untrackRecord(Record record) { - Logger.debug(LOG_TAG, "Un-tracking record " + record.guid + "."); - untrackGUID(record.guid); - } - - @Override - public Iterator<String> getTrackedRecordIDs() { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - return this.storeTracker.recordsTrackedForExclusion(); - } - - @Override - public void abort(RepositorySessionFinishDelegate delegate) { - this.storeTracker = null; - super.abort(delegate); - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - super.finish(delegate); - this.storeTracker = null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java deleted file mode 100644 index fd3c35da0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java +++ /dev/null @@ -1,326 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor { - - private static final String LOG_TAG = "BookmarksDataAccessor"; - - /* - * Fragments of SQL to make our lives easier. - */ - private static final String BOOKMARK_IS_FOLDER = BrowserContract.Bookmarks.TYPE + " = " + - BrowserContract.Bookmarks.TYPE_FOLDER; - - // SQL fragment to retrieve GUIDs whose ID mappings should be tracked by this session. - // Exclude folders we don't want to sync. - private static final String GUID_SHOULD_TRACK = BrowserContract.SyncColumns.GUID + " NOT IN ('" + - BrowserContract.Bookmarks.TAGS_FOLDER_GUID + "', '" + - BrowserContract.Bookmarks.PLACES_FOLDER_GUID + "', '" + - BrowserContract.Bookmarks.PINNED_FOLDER_GUID + "')"; - - private static final String EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE; - static { - if (AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length > 0) { - StringBuilder b = new StringBuilder(BrowserContract.SyncColumns.GUID + " NOT IN ("); - - int remaining = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length - 1; - for (String specialGuid : AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS) { - b.append('"'); - b.append(specialGuid); - b.append('"'); - if (remaining-- > 0) { - b.append(", "); - } - } - b.append(')'); - EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = b.toString(); - } else { - EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = null; // null is a valid WHERE clause. - } - } - - public static final String TYPE_FOLDER = "folder"; - public static final String TYPE_BOOKMARK = "bookmark"; - - private final RepoUtils.QueryHelper queryHelper; - - public AndroidBrowserBookmarksDataAccessor(Context context) { - super(context); - this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG); - } - - @Override - protected Uri getUri() { - return BrowserContractHelpers.BOOKMARKS_CONTENT_URI; - } - - protected static Uri getPositionsUri() { - return BrowserContractHelpers.BOOKMARKS_POSITIONS_CONTENT_URI; - } - - @Override - public void wipe() { - Uri uri = getUri(); - Logger.info(LOG_TAG, "wiping (except for special guids): " + uri); - context.getContentResolver().delete(uri, EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE, null); - } - - private final String[] GUID_AND_ID = new String[] { BrowserContract.Bookmarks.GUID, - BrowserContract.Bookmarks._ID }; - - protected Cursor getGuidsIDsForFolders() throws NullCursorException { - // Exclude items that we don't want to sync (pinned items, reading list, - // tags, the places root), in case they've ended up in the DB. - String where = BOOKMARK_IS_FOLDER + " AND " + GUID_SHOULD_TRACK; - return queryHelper.safeQuery(".getGuidsIDsForFolders", GUID_AND_ID, where, null, null); - } - - /** - * Issue a request to the Content Provider to update the positions of the - * records named by the provided GUIDs to the index of their GUID in the - * provided array. - * - * @param childArray - * A sequence of GUID strings. - */ - public int updatePositions(ArrayList<String> childArray) { - final int size = childArray.size(); - if (size == 0) { - return 0; - } - - Logger.debug(LOG_TAG, "Updating positions for " + size + " items."); - String[] args = childArray.toArray(new String[size]); - return context.getContentResolver().update(getPositionsUri(), new ContentValues(), null, args); - } - - public int bumpModifiedByGUID(Collection<String> ids, long modified) { - final int size = ids.size(); - if (size == 0) { - return 0; - } - - Logger.debug(LOG_TAG, "Bumping modified for " + size + " items to " + modified); - String where = RepoUtils.computeSQLInClause(size, BrowserContract.Bookmarks.GUID); - String[] selectionArgs = ids.toArray(new String[size]); - ContentValues values = new ContentValues(); - values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified); - - return context.getContentResolver().update(getUri(), values, where, selectionArgs); - } - - /** - * Bump the modified time of a record by ID. - */ - public int bumpModified(long id, long modified) { - Logger.debug(LOG_TAG, "Bumping modified for " + id + " to " + modified); - String where = BrowserContract.Bookmarks._ID + " = ?"; - String[] selectionArgs = new String[] { String.valueOf(id) }; - ContentValues values = new ContentValues(); - values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified); - - return context.getContentResolver().update(getUri(), values, where, selectionArgs); - } - - protected void updateParentAndPosition(String guid, long newParentId, long position) { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Bookmarks.PARENT, newParentId); - if (position >= 0) { - cv.put(BrowserContract.Bookmarks.POSITION, position); - } - updateByGuid(guid, cv); - } - - protected Map<String, Long> idsForGUIDs(String[] guids) throws NullCursorException { - final String where = RepoUtils.computeSQLInClause(guids.length, BrowserContract.Bookmarks.GUID); - Cursor c = queryHelper.safeQuery(".idsForGUIDs", GUID_AND_ID, where, guids, null); - try { - HashMap<String, Long> out = new HashMap<String, Long>(); - if (!c.moveToFirst()) { - return out; - } - final int guidIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks.GUID); - final int idIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID); - while (!c.isAfterLast()) { - out.put(c.getString(guidIndex), c.getLong(idIndex)); - c.moveToNext(); - } - return out; - } finally { - c.close(); - } - } - - /** - * Move the children of each source folder to the destination folder. - * Bump the modified time of each child. - * The caller should bump the modified time of the destination if desired. - * - * @param fromIDs the Android IDs of the source folders. - * @param to the Android ID of the destination folder. - * @return the number of updated rows. - */ - protected int moveChildren(String[] fromIDs, long to) { - long now = System.currentTimeMillis(); - long pos = -1; - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Bookmarks.PARENT, to); - cv.put(BrowserContract.Bookmarks.DATE_MODIFIED, now); - cv.put(BrowserContract.Bookmarks.POSITION, pos); - - final String where = RepoUtils.computeSQLInClause(fromIDs.length, BrowserContract.Bookmarks.PARENT); - return context.getContentResolver().update(getUri(), cv, where, fromIDs); - } - - /* - * Verify that all special GUIDs are present and that they aren't marked as deleted. - * Insert them if they aren't there. - */ - public void checkAndBuildSpecialGuids() throws NullCursorException { - final String[] specialGUIDs = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS; - Cursor cur = fetch(specialGUIDs); - long placesRoot = 0; - - // Map from GUID to whether deleted. Non-presence implies just that. - HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(specialGUIDs.length); - try { - if (cur.moveToFirst()) { - while (!cur.isAfterLast()) { - String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - if ("places".equals(guid)) { - placesRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID); - } - // Make sure none of these folders are marked as deleted. - boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; - statuses.put(guid, deleted); - cur.moveToNext(); - } - } - } finally { - cur.close(); - } - - // Insert or undelete them if missing. - for (String guid : specialGUIDs) { - if (statuses.containsKey(guid)) { - if (statuses.get(guid)) { - // Undelete. - Logger.info(LOG_TAG, "Undeleting special GUID " + guid); - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.SyncColumns.IS_DELETED, 0); - updateByGuid(guid, cv); - } - } else { - // Insert. - if (guid.equals("places")) { - // This is awkward. - Logger.info(LOG_TAG, "No places root. Inserting one."); - placesRoot = insertSpecialFolder("places", 0); - } else if (guid.equals("mobile")) { - Logger.info(LOG_TAG, "No mobile folder. Inserting one under the places root."); - insertSpecialFolder("mobile", placesRoot); - } else { - // unfiled, menu, toolbar. - Logger.info(LOG_TAG, "No " + guid + " root. Inserting one under places (" + placesRoot + ")."); - insertSpecialFolder(guid, placesRoot); - } - } - } - } - - private long insertSpecialFolder(String guid, long parentId) { - BookmarkRecord record = new BookmarkRecord(guid); - record.title = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.get(guid); - record.type = "folder"; - record.androidParentID = parentId; - return ContentUris.parseId(insert(record)); - } - - @Override - protected ContentValues getContentValues(Record record) { - BookmarkRecord rec = (BookmarkRecord) record; - - if (rec.deleted) { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.SyncColumns.GUID, rec.guid); - cv.put(BrowserContract.Bookmarks.IS_DELETED, 1); - return cv; - } - - final int recordType = BrowserContractHelpers.typeCodeForString(rec.type); - if (recordType == -1) { - throw new IllegalStateException("Unexpected record type " + rec.type); - } - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.SyncColumns.GUID, rec.guid); - cv.put(BrowserContract.Bookmarks.TYPE, recordType); - cv.put(BrowserContract.Bookmarks.TITLE, rec.title); - cv.put(BrowserContract.Bookmarks.URL, rec.bookmarkURI); - cv.put(BrowserContract.Bookmarks.DESCRIPTION, rec.description); - if (rec.tags == null) { - rec.tags = new JSONArray(); - } - cv.put(BrowserContract.Bookmarks.TAGS, rec.tags.toJSONString()); - cv.put(BrowserContract.Bookmarks.KEYWORD, rec.keyword); - cv.put(BrowserContract.Bookmarks.PARENT, rec.androidParentID); - cv.put(BrowserContract.Bookmarks.POSITION, rec.androidPosition); - - // Note that we don't set the modified timestamp: we allow the - // content provider to do that for us. - return cv; - } - - /** - * Returns a cursor over non-deleted records that list the given androidID as a parent. - */ - public Cursor getChildren(long androidID) throws NullCursorException { - return getChildren(androidID, false); - } - - /** - * Returns a cursor with any records that list the given androidID as a parent. - * Excludes 'places', and optionally any deleted records. - */ - public Cursor getChildren(long androidID, boolean includeDeleted) throws NullCursorException { - final String where = BrowserContract.Bookmarks.PARENT + " = ? AND " + - BrowserContract.SyncColumns.GUID + " <> ? " + - (!includeDeleted ? ("AND " + BrowserContract.SyncColumns.IS_DELETED + " = 0") : ""); - - final String[] args = new String[] { String.valueOf(androidID), "places" }; - - // Order by position, falling back on creation date and ID. - final String order = BrowserContract.Bookmarks.POSITION + ", " + - BrowserContract.SyncColumns.DATE_CREATED + ", " + - BrowserContract.Bookmarks._ID; - return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, order); - } - - - @Override - protected String[] getAllColumns() { - return BrowserContractHelpers.BookmarkColumns; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java deleted file mode 100644 index 38520fd7a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.BookmarksRepository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository { - - @Override - protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { - AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context); - final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate(); - deferredCreationDelegate.onSessionCreated(session); - } - - @Override - protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) { - return new AndroidBrowserBookmarksDataAccessor(context); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java deleted file mode 100644 index fb79901a1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java +++ /dev/null @@ -1,1107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.NoGuidForIdException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.ParentNotFoundException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession - implements BookmarksInsertionManager.BookmarkInserter { - - public static final int DEFAULT_DELETION_FLUSH_THRESHOLD = 50; - public static final int DEFAULT_INSERTION_FLUSH_THRESHOLD = 50; - - // TODO: synchronization for these. - private final HashMap<String, Long> parentGuidToIDMap = new HashMap<String, Long>(); - private final HashMap<Long, String> parentIDToGuidMap = new HashMap<Long, String>(); - - /** - * Some notes on reparenting/reordering. - * - * Fennec stores new items with a high-negative position, because it doesn't care. - * On the other hand, it also doesn't give us any help managing positions. - * - * We can process records and folders in any order, though we'll usually see folders - * first because their sortindex is larger. - * - * We can also see folders that refer to children we haven't seen, and children we - * won't see (perhaps due to a TTL, perhaps due to a limit on our fetch). - * - * And of course folders can refer to local children (including ones that might - * be reconciled into oblivion!), or local children in other folders. And the local - * version of a folder -- which might be a reconciling target, or might not -- can - * have local additions or removals. (That causes complications with on-the-fly - * reordering: we don't know in advance which records will even exist by the end - * of the sync.) - * - * We opt to leave records in a reasonable state as we go, applying reordering/ - * reparenting operations whenever possible. A final sequence is applied after all - * incoming records have been handled. - * - * As such, we need to track a bunch of stuff as we go: - * - * • For each downloaded folder, the array of children. These will be server GUIDs, - * but not necessarily identical to the remote list: if we download a record and - * it's been locally moved, it must be removed from this child array. - * - * This mapping can be discarded when final reordering has occurred, either on - * store completion or when every child has been seen within this session. - * - * • A list of orphans: records whose parent folder does not yet exist. This can be - * trimmed as orphans are reparented. - * - * • Mappings from folder GUIDs to folder IDs, so that we can parent items without - * having to look in the DB. Of course, this must be kept up-to-date as we - * reconcile. - * - * Reordering also needs to occur during fetch. That is, a folder might have been - * created locally, or modified locally without any remote changes. An order must - * be generated for the folder's children array, and it must be persisted into the - * database to act as a starting point for future changes. But of course we don't - * want to incur a database write if the children already have a satisfactory order. - * - * Do we also need a list of "adopters", parents that are still waiting for children? - * As items get picked out of the orphans list, we can do on-the-fly ordering, until - * we're left with lonely records at the end. - * - * As we modify local folders, perhaps by moving children out of their purview, we - * must bump their modification time so as to cause them to be uploaded on the next - * stage of syncing. The same applies to simple reordering. - */ - - // TODO: can we guarantee serial access to these? - private final HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>(); - private final HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>(); - private int needsReparenting = 0; - - private final AndroidBrowserBookmarksDataAccessor dataAccessor; - - protected BookmarksDeletionManager deletionManager; - protected BookmarksInsertionManager insertionManager; - - /** - * An array of known-special GUIDs. - */ - public static final String[] SPECIAL_GUIDS = new String[] { - // Mobile and desktop places roots have to come first. - "places", - "mobile", - "toolbar", - "menu", - "unfiled" - }; - - /** - * = A note about folder mapping = - * - * Note that _none_ of Places's folders actually have a special GUID. They're all - * randomly generated. Special folders are indicated by membership in the - * moz_bookmarks_roots table, and by having the parent `1`. - * - * Additionally, the mobile root is annotated. In Firefox Sync, PlacesUtils is - * used to find the IDs of these special folders. - * - * We need to consume records with these various GUIDs, producing a local - * representation which we are able to stably map upstream. - * - * Android Sync skips over the contents of some special GUIDs -- `places`, `tags`, - * etc. -- when finding IDs. - * Some of these special GUIDs are part of desktop structure (places, tags). Some - * are part of Fennec's custom data (readinglist, pinned). - * - * We don't want to upload or apply these records. - * - * That is: - * - * * We should not upload a `places`,`tags`, `readinglist`, or `pinned` record. - * * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set - * their parent ID as appropriate on upload. - * - * Fortunately, Fennec stores our representation of the data, not Places: that is, - * there's a "places" root, containing "mobile", "menu", "toolbar", etc. - * - * These are guaranteed to exist when the database is created. - * - * = Places folders = - * - * guid root_name folder_id parent - * ---------- ---------- ---------- ---------- - * ? places 1 0 - * ? menu 2 1 - * ? toolbar 3 1 - * ? tags 4 1 - * ? unfiled 5 1 - * - * ? mobile* 474 1 - * - * - * = Fennec folders = - * - * guid folder_id parent - * ---------- ---------- ---------- - * places 0 0 - * mobile 1 0 - * menu 2 0 - * etc. - * - */ - public static final Map<String, String> SPECIAL_GUID_PARENTS; - static { - HashMap<String, String> m = new HashMap<String, String>(); - m.put("places", null); - m.put("menu", "places"); - m.put("toolbar", "places"); - m.put("tags", "places"); - m.put("unfiled", "places"); - m.put("mobile", "places"); - SPECIAL_GUID_PARENTS = Collections.unmodifiableMap(m); - } - - - /** - * A map of guids to their localized name strings. - */ - // Oh, if only we could make this final and initialize it in the static initializer. - public static Map<String, String> SPECIAL_GUIDS_MAP; - - /** - * Return true if the provided record GUID should be skipped - * in child lists or fetch results. - * - * @param recordGUID the GUID of the record to check. - * @return true if the record should be skipped. - */ - public static boolean forbiddenGUID(final String recordGUID) { - return recordGUID == null || - BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(recordGUID) || - BrowserContract.Bookmarks.PLACES_FOLDER_GUID.equals(recordGUID) || - BrowserContract.Bookmarks.TAGS_FOLDER_GUID.equals(recordGUID); - } - - /** - * Return true if the provided parent GUID's children should - * be skipped in child lists or fetch results. - * This differs from {@link #forbiddenGUID(String)} in that we're skipping - * part of the hierarchy. - * - * @param parentGUID the GUID of parent of the record to check. - * @return true if the record should be skipped. - */ - public static boolean forbiddenParent(final String parentGUID) { - return parentGUID == null || - BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(parentGUID); - } - - public AndroidBrowserBookmarksRepositorySession(Repository repository, Context context) { - super(repository); - - if (SPECIAL_GUIDS_MAP == null) { - HashMap<String, String> m = new HashMap<String, String>(); - - // Note that we always use the literal name "mobile" for the Mobile Bookmarks - // folder, regardless of its actual name in the database or the Fennec UI. - // This is to match desktop (working around Bug 747699) and to avoid a similar - // issue locally. See Bug 748898. - m.put("mobile", "mobile"); - - // Other folders use their contextualized names, and we simply rely on - // these not changing, matching desktop, and such to avoid issues. - m.put("menu", context.getString(R.string.bookmarks_folder_menu)); - m.put("places", context.getString(R.string.bookmarks_folder_places)); - m.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar)); - m.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled)); - - SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m); - } - - dbHelper = new AndroidBrowserBookmarksDataAccessor(context); - dataAccessor = (AndroidBrowserBookmarksDataAccessor) dbHelper; - } - - private static int getTypeFromCursor(Cursor cur) { - return RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks.TYPE); - } - - private static boolean rowIsFolder(Cursor cur) { - return getTypeFromCursor(cur) == BrowserContract.Bookmarks.TYPE_FOLDER; - } - - private String getGUIDForID(long androidID) { - String guid = parentIDToGuidMap.get(androidID); - trace(" " + androidID + " => " + guid); - return guid; - } - - private long getIDForGUID(String guid) { - Long id = parentGuidToIDMap.get(guid); - if (id == null) { - Logger.warn(LOG_TAG, "Couldn't find local ID for GUID " + guid); - return -1; - } - return id; - } - - private String getGUID(Cursor cur) { - return RepoUtils.getStringFromCursor(cur, "guid"); - } - - private long getParentID(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT); - } - - // More efficient for bulk operations. - private long getPosition(Cursor cur, int positionIndex) { - return cur.getLong(positionIndex); - } - private long getPosition(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION); - } - - private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException { - if (parentGUID == null) { - return ""; - } - if (SPECIAL_GUIDS_MAP.containsKey(parentGUID)) { - return SPECIAL_GUIDS_MAP.get(parentGUID); - } - - // Get parent name from database. - String parentName = ""; - Cursor name = dataAccessor.fetch(new String[] { parentGUID }); - try { - name.moveToFirst(); - if (!name.isAfterLast()) { - parentName = RepoUtils.getStringFromCursor(name, BrowserContract.Bookmarks.TITLE); - } - else { - Logger.error(LOG_TAG, "Couldn't find record with guid '" + parentGUID + "' when looking for parent name."); - throw new ParentNotFoundException(null); - } - } finally { - name.close(); - } - return parentName; - } - - /** - * Retrieve the child array for a record, repositioning and updating the database as necessary. - * - * @param folderID - * The database ID of the folder. - * @param persist - * True if generated positions should be written to the database. The modified - * time of the parent folder is only bumped if this is true. - * @param childArray - * A new, empty JSONArray which will be populated with an array of GUIDs. - * @return - * True if the resulting array is "clean" (i.e., reflects the content of the database). - * @throws NullCursorException - */ - @SuppressWarnings("unchecked") - private boolean getChildrenArray(long folderID, boolean persist, JSONArray childArray) throws NullCursorException { - trace("Calling getChildren for androidID " + folderID); - Cursor children = dataAccessor.getChildren(folderID); - try { - if (!children.moveToFirst()) { - trace("No children: empty cursor."); - return true; - } - final int positionIndex = children.getColumnIndex(BrowserContract.Bookmarks.POSITION); - final int count = children.getCount(); - Logger.debug(LOG_TAG, "Expecting " + count + " children."); - - // Sorted by requested position. - TreeMap<Long, ArrayList<String>> guids = new TreeMap<Long, ArrayList<String>>(); - - while (!children.isAfterLast()) { - final String childGuid = getGUID(children); - final long childPosition = getPosition(children, positionIndex); - trace(" Child GUID: " + childGuid); - trace(" Child position: " + childPosition); - Utils.addToIndexBucketMap(guids, Math.abs(childPosition), childGuid); - children.moveToNext(); - } - - // This will suffice for taking a jumble of records and indices and - // producing a sorted sequence that preserves some kind of order -- - // from the abs of the position, falling back on cursor order (that - // is, creation time and ID). - // Note that this code is not intended to merge values from two sources! - boolean changed = false; - int i = 0; - for (Entry<Long, ArrayList<String>> entry : guids.entrySet()) { - long pos = entry.getKey(); - int atPos = entry.getValue().size(); - - // If every element has a different index, and the indices are - // in strict natural order, then changed will be false. - if (atPos > 1 || pos != i) { - changed = true; - } - - ++i; - - for (String guid : entry.getValue()) { - if (!forbiddenGUID(guid)) { - childArray.add(guid); - } - } - } - - if (Logger.shouldLogVerbose(LOG_TAG)) { - // Don't JSON-encode unless we're logging. - Logger.trace(LOG_TAG, "Output child array: " + childArray.toJSONString()); - } - - if (!changed) { - Logger.debug(LOG_TAG, "Nothing moved! Database reflects child array."); - return true; - } - - if (!persist) { - Logger.debug(LOG_TAG, "Returned array does not match database, and not persisting."); - return false; - } - - Logger.debug(LOG_TAG, "Generating child array required moving records. Updating DB."); - final long time = now(); - if (0 < dataAccessor.updatePositions(childArray)) { - Logger.debug(LOG_TAG, "Bumping parent time to " + time + "."); - dataAccessor.bumpModified(folderID, time); - } - return true; - } finally { - children.close(); - } - } - - protected static boolean isDeleted(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) != 0; - } - - @Override - protected Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - // During storing of a retrieved record, we never care about the children - // array that's already present in the database -- we don't use it for - // reconciling. Skip all that effort for now. - return retrieveRecord(cur, false); - } - - @Override - protected Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - return retrieveRecord(cur, true); - } - - /** - * Build a record from a cursor, with a flag to dictate whether the - * children array should be computed and written back into the database. - */ - protected BookmarkRecord retrieveRecord(Cursor cur, boolean computeAndPersistChildren) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - String recordGUID = getGUID(cur); - Logger.trace(LOG_TAG, "Record from mirror cursor: " + recordGUID); - - if (forbiddenGUID(recordGUID)) { - Logger.debug(LOG_TAG, "Ignoring " + recordGUID + " record in recordFromMirrorCursor."); - return null; - } - - // Short-cut for deleted items. - if (isDeleted(cur)) { - return AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, null, null, null); - } - - long androidParentID = getParentID(cur); - - // Ensure special folders stay in the right place. - String androidParentGUID = SPECIAL_GUID_PARENTS.get(recordGUID); - if (androidParentGUID == null) { - androidParentGUID = getGUIDForID(androidParentID); - } - - boolean needsReparenting = false; - - if (androidParentGUID == null) { - Logger.debug(LOG_TAG, "No parent GUID for record " + recordGUID + " with parent " + androidParentID); - // If the parent has been stored and somehow has a null GUID, throw an error. - if (parentIDToGuidMap.containsKey(androidParentID)) { - Logger.error(LOG_TAG, "Have the parent android ID for the record but the parent's GUID wasn't found."); - throw new NoGuidForIdException(null); - } - - // We have a parent ID but it's wrong. If the record is deleted, - // we'll just say that it was in the Unsorted Bookmarks folder. - // If not, we'll move it into Mobile Bookmarks. - needsReparenting = true; - } - - // If record is a folder, and we want to see children at this time, then build out the children array. - final JSONArray childArray; - if (computeAndPersistChildren) { - childArray = getChildrenArrayForRecordCursor(cur, recordGUID, true); - } else { - childArray = null; - } - String parentName = getParentName(androidParentGUID); - BookmarkRecord bookmark = AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray); - - if (bookmark == null) { - Logger.warn(LOG_TAG, "Unable to extract bookmark from cursor. Record GUID " + recordGUID + - ", parent " + androidParentGUID + "/" + androidParentID); - return null; - } - - if (needsReparenting) { - Logger.warn(LOG_TAG, "Bookmark record " + recordGUID + " has a bad parent pointer. Reparenting now."); - - String destination = bookmark.deleted ? "unfiled" : "mobile"; - bookmark.androidParentID = getIDForGUID(destination); - bookmark.androidPosition = getPosition(cur); - bookmark.parentID = destination; - bookmark.parentName = getParentName(destination); - if (!bookmark.deleted) { - // Actually move it. - // TODO: compute position. Persist. - relocateBookmark(bookmark); - } - } - - return bookmark; - } - - /** - * Ensure that the local database row for the provided bookmark - * reflects this record's parent information. - * - * @param bookmark - */ - private void relocateBookmark(BookmarkRecord bookmark) { - dataAccessor.updateParentAndPosition(bookmark.guid, bookmark.androidParentID, bookmark.androidPosition); - } - - protected JSONArray getChildrenArrayForRecordCursor(Cursor cur, String recordGUID, boolean persist) throws NullCursorException { - boolean isFolder = rowIsFolder(cur); - if (!isFolder) { - return null; - } - - long androidID = parentGuidToIDMap.get(recordGUID); - JSONArray childArray = new JSONArray(); - getChildrenArray(androidID, persist, childArray); - - Logger.debug(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID); - return childArray; - } - - @Override - public boolean shouldIgnore(Record record) { - if (!(record instanceof BookmarkRecord)) { - return true; - } - if (record.deleted) { - return false; - } - - BookmarkRecord bmk = (BookmarkRecord) record; - - if (forbiddenGUID(bmk.guid)) { - Logger.debug(LOG_TAG, "Ignoring forbidden record with guid: " + bmk.guid); - return true; - } - - if (forbiddenParent(bmk.parentID)) { - Logger.debug(LOG_TAG, "Ignoring child " + bmk.guid + " of forbidden parent folder " + bmk.parentID); - return true; - } - - if (BrowserContractHelpers.isSupportedType(bmk.type)) { - return false; - } - - Logger.debug(LOG_TAG, "Ignoring record with guid: " + bmk.guid + " and type: " + bmk.type); - return true; - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - // Check for the existence of special folders - // and insert them if they don't exist. - Cursor cur; - try { - Logger.debug(LOG_TAG, "Check and build special GUIDs."); - dataAccessor.checkAndBuildSpecialGuids(); - cur = dataAccessor.getGuidsIDsForFolders(); - Logger.debug(LOG_TAG, "Got GUIDs for folders."); - } catch (android.database.sqlite.SQLiteConstraintException e) { - Logger.error(LOG_TAG, "Got sqlite constraint exception working with Fennec bookmark DB.", e); - delegate.onBeginFailed(e); - return; - } catch (Exception e) { - delegate.onBeginFailed(e); - return; - } - - // To deal with parent mapping of bookmarks we have to do some - // hairy stuff. Here's the setup for it. - - Logger.debug(LOG_TAG, "Preparing folder ID mappings."); - - // Fake our root. - Logger.debug(LOG_TAG, "Tracking places root as ID 0."); - parentIDToGuidMap.put(0L, "places"); - parentGuidToIDMap.put("places", 0L); - try { - cur.moveToFirst(); - while (!cur.isAfterLast()) { - String guid = getGUID(cur); - long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID); - parentGuidToIDMap.put(guid, id); - parentIDToGuidMap.put(id, guid); - Logger.debug(LOG_TAG, "GUID " + guid + " maps to " + id); - cur.moveToNext(); - } - } finally { - cur.close(); - } - deletionManager = new BookmarksDeletionManager(dataAccessor, DEFAULT_DELETION_FLUSH_THRESHOLD); - - // We just crawled the database enumerating all folders; we'll start the - // insertion manager with exactly these folders as the known parents (the - // collection is copied) in the manager constructor. - insertionManager = new BookmarksInsertionManager(DEFAULT_INSERTION_FLUSH_THRESHOLD, parentGuidToIDMap.keySet(), this); - - Logger.debug(LOG_TAG, "Done with initial setup of bookmarks session."); - super.begin(delegate); - } - - /** - * Implement method of BookmarksInsertionManager.BookmarkInserter. - */ - @Override - public boolean insertFolder(BookmarkRecord record) { - // A folder that is *not* deleted needs its androidID updated, so that - // updateBookkeeping can re-parent, etc. - Record toStore = prepareRecord(record); - try { - Uri recordURI = dbHelper.insert(toStore); - if (recordURI == null) { - delegate.onRecordStoreFailed(new RuntimeException("Got null URI inserting folder with guid " + toStore.guid + "."), record.guid); - return false; - } - toStore.androidID = ContentUris.parseId(recordURI); - Logger.debug(LOG_TAG, "Inserted folder with guid " + toStore.guid + " as androidID " + toStore.androidID); - - updateBookkeeping(toStore); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - return false; - } - trackRecord(toStore); - delegate.onRecordStoreSucceeded(record.guid); - return true; - } - - /** - * Implement method of BookmarksInsertionManager.BookmarkInserter. - */ - @Override - public void bulkInsertNonFolders(Collection<BookmarkRecord> records) { - // All of these records are *not* deleted and *not* folders, so we don't - // need to update androidID at all! - // TODO: persist records that fail to insert for later retry. - ArrayList<Record> toStores = new ArrayList<Record>(records.size()); - for (Record record : records) { - toStores.add(prepareRecord(record)); - } - - try { - int stored = dataAccessor.bulkInsert(toStores); - if (stored != toStores.size()) { - // Something failed; most pessimistic action is to declare that all insertions failed. - // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? - for (Record failed : toStores) { - delegate.onRecordStoreFailed(new RuntimeException("Possibly failed to bulkInsert non-folder with guid " + failed.guid + "."), failed.guid); - } - return; - } - } catch (NullCursorException e) { - for (Record failed : toStores) { - delegate.onRecordStoreFailed(e, failed.guid); - } - return; - } - - // Success For All! - for (Record succeeded : toStores) { - try { - updateBookkeeping(succeeded); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception updating bookkeeping of non-folder with guid " + succeeded.guid + ".", e); - } - trackRecord(succeeded); - delegate.onRecordStoreSucceeded(succeeded.guid); - } - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - // Allow these to be GCed. - deletionManager = null; - insertionManager = null; - - // Override finish to do this check; make sure all records - // needing re-parenting have been re-parented. - if (needsReparenting != 0) { - Logger.error(LOG_TAG, "Finish called but " + needsReparenting + - " bookmark(s) have been placed in unsorted bookmarks and not been reparented."); - - // TODO: handling of failed reparenting. - // E.g., delegate.onFinishFailed(new BookmarkNeedsReparentingException(null)); - } - super.finish(delegate); - }; - - @Override - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - super.setStoreDelegate(delegate); - - if (deletionManager != null) { - deletionManager.setDelegate(delegate); - } - } - - @Override - protected Record reconcileRecords(Record remoteRecord, Record localRecord, - long lastRemoteRetrieval, - long lastLocalRetrieval) { - - BookmarkRecord reconciled = (BookmarkRecord) super.reconcileRecords(remoteRecord, localRecord, - lastRemoteRetrieval, - lastLocalRetrieval); - - // For now we *always* use the remote record's children array as a starting point. - // We won't write it into the database yet; we'll record it and process as we go. - reconciled.children = ((BookmarkRecord) remoteRecord).children; - - // *Always* track folders, though: if we decide we need to reposition items, we'll - // untrack later. - if (reconciled.isFolder()) { - trackRecord(reconciled); - } - return reconciled; - } - - /** - * Rename mobile folders to "mobile", both in and out. The other half of - * this logic lives in {@link #computeParentFields(BookmarkRecord, String, String)}, where - * the parent name of a record is set from {@link #SPECIAL_GUIDS_MAP} rather than - * from source data. - * - * Apply this approach generally for symmetry. - */ - @Override - protected void fixupRecord(Record record) { - final BookmarkRecord r = (BookmarkRecord) record; - final String parentName = SPECIAL_GUIDS_MAP.get(r.parentID); - if (parentName == null) { - return; - } - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Replacing parent name \"" + r.parentName + "\" with \"" + parentName + "\"."); - } - r.parentName = parentName; - } - - @Override - protected Record prepareRecord(Record record) { - if (record.deleted) { - Logger.debug(LOG_TAG, "No need to prepare deleted record " + record.guid); - return record; - } - - BookmarkRecord bmk = (BookmarkRecord) record; - - if (!isSpecialRecord(record)) { - // We never want to reparent special records. - handleParenting(bmk); - } - - if (Logger.LOG_PERSONAL_INFORMATION) { - if (bmk.isFolder()) { - Logger.pii(LOG_TAG, "Inserting folder " + bmk.guid + ", " + bmk.title + - " with parent " + bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.parentName + - ", " + bmk.androidPosition + ")"); - } else { - Logger.pii(LOG_TAG, "Inserting bookmark " + bmk.guid + ", " + bmk.title + ", " + - bmk.bookmarkURI + " with parent " + bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.parentName + - ", " + bmk.androidPosition + ")"); - } - } else { - if (bmk.isFolder()) { - Logger.debug(LOG_TAG, "Inserting folder " + bmk.guid + ", parent " + - bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.androidPosition + ")"); - } else { - Logger.debug(LOG_TAG, "Inserting bookmark " + bmk.guid + " with parent " + - bmk.androidParentID + - " (" + bmk.parentID + ", " + ", " + bmk.androidPosition + ")"); - } - } - return bmk; - } - - /** - * If the provided record doesn't have correct parent information, - * update appropriate bookkeeping to improve the situation. - * - * @param bmk - */ - private void handleParenting(BookmarkRecord bmk) { - if (parentGuidToIDMap.containsKey(bmk.parentID)) { - bmk.androidParentID = parentGuidToIDMap.get(bmk.parentID); - - // Might as well set a basic position from the downloaded children array. - JSONArray children = parentToChildArray.get(bmk.parentID); - if (children != null) { - int index = children.indexOf(bmk.guid); - if (index >= 0) { - bmk.androidPosition = index; - } - } - } - else { - bmk.androidParentID = parentGuidToIDMap.get("unfiled"); - ArrayList<String> children; - if (missingParentToChildren.containsKey(bmk.parentID)) { - children = missingParentToChildren.get(bmk.parentID); - } else { - children = new ArrayList<String>(); - } - children.add(bmk.guid); - needsReparenting++; - missingParentToChildren.put(bmk.parentID, children); - } - } - - private boolean isSpecialRecord(Record record) { - return SPECIAL_GUID_PARENTS.containsKey(record.guid); - } - - @Override - protected void updateBookkeeping(Record record) throws NoGuidForIdException, - NullCursorException, - ParentNotFoundException { - super.updateBookkeeping(record); - BookmarkRecord bmk = (BookmarkRecord) record; - - // If record is folder, update maps and re-parent children if necessary. - if (!bmk.isFolder()) { - Logger.debug(LOG_TAG, "Not a folder. No bookkeeping."); - return; - } - - Logger.debug(LOG_TAG, "Updating bookkeeping for folder " + record.guid); - - // Mappings between ID and GUID. - // TODO: update our persisted children arrays! - // TODO: if our Android ID just changed, replace parents for all of our children. - parentGuidToIDMap.put(bmk.guid, bmk.androidID); - parentIDToGuidMap.put(bmk.androidID, bmk.guid); - - JSONArray childArray = bmk.children; - - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, bmk.guid + " has children " + childArray.toJSONString()); - } - parentToChildArray.put(bmk.guid, childArray); - - // Re-parent. - if (missingParentToChildren.containsKey(bmk.guid)) { - for (String child : missingParentToChildren.get(bmk.guid)) { - // This might return -1; that's OK, the bookmark will - // be properly repositioned later. - long position = childArray.indexOf(child); - dataAccessor.updateParentAndPosition(child, bmk.androidID, position); - needsReparenting--; - } - missingParentToChildren.remove(bmk.guid); - } - } - - @Override - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - try { - insertionManager.enqueueRecord((BookmarkRecord) record); - } catch (Exception e) { - throw new NullCursorException(e); - } - } - - @Override - protected void storeRecordDeletion(final Record record, final Record existingRecord) { - if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) { - Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring."); - return; - } - final BookmarkRecord bookmarkRecord = (BookmarkRecord) record; - final BookmarkRecord existingBookmark = (BookmarkRecord) existingRecord; - final boolean isFolder = existingBookmark.isFolder(); - final String parentGUID = existingBookmark.parentID; - deletionManager.deleteRecord(bookmarkRecord.guid, isFolder, parentGUID); - } - - protected void flushQueues() { - long now = now(); - Logger.debug(LOG_TAG, "Applying remaining insertions."); - try { - insertionManager.finishUp(); - Logger.debug(LOG_TAG, "Done applying remaining insertions."); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Unable to apply remaining insertions.", e); - } - - Logger.debug(LOG_TAG, "Applying deletions."); - try { - untrackGUIDs(deletionManager.flushAll(getIDForGUID("unfiled"), now)); - Logger.debug(LOG_TAG, "Done applying deletions."); - } catch (Exception e) { - Logger.error(LOG_TAG, "Unable to apply deletions.", e); - } - } - - @SuppressWarnings("unchecked") - private void finishUp() { - try { - flushQueues(); - Logger.debug(LOG_TAG, "Have " + parentToChildArray.size() + " folders whose children might need repositioning."); - for (Entry<String, JSONArray> entry : parentToChildArray.entrySet()) { - String guid = entry.getKey(); - JSONArray onServer = entry.getValue(); - try { - final long folderID = getIDForGUID(guid); - final JSONArray inDB = new JSONArray(); - final boolean clean = getChildrenArray(folderID, false, inDB); - final boolean sameArrays = Utils.sameArrays(onServer, inDB); - - // If the local children and the remote children are already - // the same, then we don't need to bump the modified time of the - // parent: we wouldn't upload a different record, so avoid the cycle. - if (!sameArrays) { - int added = 0; - for (Object o : inDB) { - if (!onServer.contains(o)) { - onServer.add(o); - added++; - } - } - Logger.debug(LOG_TAG, "Added " + added + " items locally."); - Logger.debug(LOG_TAG, "Untracking and bumping " + guid + "(" + folderID + ")"); - dataAccessor.bumpModified(folderID, now()); - untrackGUID(guid); - } - - // If the arrays are different, or they're the same but not flushed to disk, - // write them out now. - if (!sameArrays || !clean) { - dataAccessor.updatePositions(new ArrayList<String>(onServer)); - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e); - } - } - } finally { - super.storeDone(); - } - } - - /** - * Hook into the deletion manager on wipe. - */ - class BookmarkWipeRunnable extends WipeRunnable { - public BookmarkWipeRunnable(RepositorySessionWipeDelegate delegate) { - super(delegate); - } - - @Override - public void run() { - try { - // Clear our queued deletions. - deletionManager.clear(); - insertionManager.clear(); - super.run(); - } catch (Exception ex) { - delegate.onWipeFailed(ex); - return; - } - } - } - - @Override - protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) { - return new BookmarkWipeRunnable(delegate); - } - - @Override - public void storeDone() { - Runnable command = new Runnable() { - @Override - public void run() { - finishUp(); - } - }; - storeWorkQueue.execute(command); - } - - @Override - protected String buildRecordString(Record record) { - BookmarkRecord bmk = (BookmarkRecord) record; - String parent = bmk.parentName + "/"; - if (bmk.isBookmark()) { - return "b" + parent + bmk.bookmarkURI + ":" + bmk.title; - } - if (bmk.isFolder()) { - return "f" + parent + bmk.title; - } - if (bmk.isSeparator()) { - return "s" + parent + bmk.androidPosition; - } - if (bmk.isQuery()) { - return "q" + parent + bmk.bookmarkURI; - } - return null; - } - - public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentGUID, String suggestedParentName) { - final String guid = rec.guid; - if (guid == null) { - // Oh dear. - Logger.error(LOG_TAG, "No guid in computeParentFields!"); - return null; - } - - String realParent = SPECIAL_GUID_PARENTS.get(guid); - if (realParent == null) { - // No magic parent. Use whatever the caller suggests. - realParent = suggestedParentGUID; - } else { - Logger.debug(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentGUID + - " for " + guid + "; using " + realParent); - } - - if (realParent == null) { - // Oh dear. - Logger.error(LOG_TAG, "No parent for record " + guid); - return null; - } - - // Always set the parent name for special folders back to default. - String parentName = SPECIAL_GUIDS_MAP.get(realParent); - if (parentName == null) { - parentName = suggestedParentName; - } - - rec.parentID = realParent; - rec.parentName = parentName; - return rec; - } - - private static BookmarkRecord logBookmark(BookmarkRecord rec) { - try { - Logger.debug(LOG_TAG, "Returning " + (rec.deleted ? "deleted " : "") + - "bookmark record " + rec.guid + " (" + rec.androidID + - ", parent " + rec.parentID + ")"); - if (!rec.deleted && Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "> Parent name: " + rec.parentName); - Logger.pii(LOG_TAG, "> Title: " + rec.title); - Logger.pii(LOG_TAG, "> Type: " + rec.type); - Logger.pii(LOG_TAG, "> URI: " + rec.bookmarkURI); - Logger.pii(LOG_TAG, "> Position: " + rec.androidPosition); - if (rec.isFolder()) { - Logger.pii(LOG_TAG, "FOLDER: Children are " + - (rec.children == null ? - "null" : - rec.children.toJSONString())); - } - } - } catch (Exception e) { - Logger.debug(LOG_TAG, "Exception logging bookmark record " + rec, e); - } - return rec; - } - - // Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark. - public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentGUID, String parentName, JSONArray children) { - final String collection = "bookmarks"; - final String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - final long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); - final boolean deleted = isDeleted(cur); - BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted); - - // No point in populating it. - if (deleted) { - return logBookmark(rec); - } - - int rowType = getTypeFromCursor(cur); - String typeString = BrowserContractHelpers.typeStringForCode(rowType); - - if (typeString == null) { - Logger.warn(LOG_TAG, "Unsupported type code " + rowType); - return null; - } - - Logger.trace(LOG_TAG, "Record " + guid + " has type " + typeString); - - rec.type = typeString; - rec.title = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE); - rec.bookmarkURI = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.URL); - rec.description = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION); - rec.tags = RepoUtils.getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS); - rec.keyword = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD); - - rec.androidID = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID); - rec.androidPosition = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION); - rec.children = children; - - // Need to restore the parentId since it isn't stored in content provider. - // We also take this opportunity to fix up parents for special folders, - // allowing us to map between the hierarchies used by Fennec and Places. - BookmarkRecord withParentFields = computeParentFields(rec, parentGUID, parentName); - if (withParentFields == null) { - // Oh dear. Something went wrong. - return null; - } - return logBookmark(withParentFields); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java deleted file mode 100644 index c09d64708..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java +++ /dev/null @@ -1,188 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -public class AndroidBrowserHistoryDataAccessor extends - AndroidBrowserRepositoryDataAccessor { - - public AndroidBrowserHistoryDataAccessor(Context context) { - super(context); - } - - @Override - protected Uri getUri() { - return BrowserContractHelpers.HISTORY_CONTENT_URI; - } - - @Override - protected ContentValues getContentValues(Record record) { - ContentValues cv = new ContentValues(); - HistoryRecord rec = (HistoryRecord) record; - cv.put(BrowserContract.History.GUID, rec.guid); - cv.put(BrowserContract.History.TITLE, rec.title); - cv.put(BrowserContract.History.URL, rec.histURI); - if (rec.visits != null) { - JSONArray visits = rec.visits; - long mostRecent = getLastVisited(visits); - - // Fennec stores history timestamps in milliseconds, and visit timestamps in microseconds. - // The rest of Sync works in microseconds. This is the conversion point for records coming form Sync. - cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent / 1000); - cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, mostRecent / 1000); - cv.put(BrowserContract.History.VISITS, Long.toString(visits.size())); - } - return cv; - } - - @Override - protected String[] getAllColumns() { - return BrowserContractHelpers.HistoryColumns; - } - - @Override - public Uri insert(Record record) { - HistoryRecord rec = (HistoryRecord) record; - - Logger.debug(LOG_TAG, "Storing record " + record.guid); - Uri newRecordUri = super.insert(record); - - Logger.debug(LOG_TAG, "Storing visits for " + record.guid); - context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(rec.guid, rec.visits) - ); - - return newRecordUri; - } - - /** - * Given oldGUID, first updates corresponding history record with new values (super operation), - * and then inserts visits from the new record. - * Existing visits from the old record are updated on database level to point to new GUID if necessary. - * - * @param oldGUID GUID of old <code>HistoryRecord</code> - * @param newRecord new <code>HistoryRecord</code> to replace old one with, and insert visits from - */ - @Override - public void update(String oldGUID, Record newRecord) { - // First, update existing history records with new values. This might involve changing history GUID, - // and thanks to ON UPDATE CASCADE clause on Visits.HISTORY_GUID foreign key, visits will be "ported over" - // to the new GUID. - super.update(oldGUID, newRecord); - - // Now we need to insert any visits from the new record - HistoryRecord rec = (HistoryRecord) newRecord; - String newGUID = newRecord.guid; - Logger.debug(LOG_TAG, "Storing visits for " + newGUID + ", replacing " + oldGUID); - - context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(newGUID, rec.visits) - ); - } - - /** - * Insert records. - * <p> - * This inserts all the records (using <code>ContentProvider.bulkInsert</code>), - * then inserts all the visit information (also using <code>ContentProvider.bulkInsert</code>). - * - * @param records - * the records to insert. - * @return - * the number of records actually inserted. - * @throws NullCursorException - */ - public int bulkInsert(ArrayList<HistoryRecord> records) throws NullCursorException { - if (records.isEmpty()) { - Logger.debug(LOG_TAG, "No records to insert, returning."); - } - - int size = records.size(); - ContentValues[] cvs = new ContentValues[size]; - int index = 0; - for (Record record : records) { - if (record.guid == null) { - throw new IllegalArgumentException("Record with null GUID passed in to bulkInsert."); - } - cvs[index] = getContentValues(record); - index += 1; - } - - // First update the history records. - int inserted = context.getContentResolver().bulkInsert(getUri(), cvs); - if (inserted == size) { - Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected."); - } else { - Logger.debug(LOG_TAG, "Inserted " + - inserted + " records but expected " + - size + " records; continuing to update visits."); - } - - final ContentValues remoteVisitAggregateValues = new ContentValues(); - final Uri historyIncrementRemoteAggregateUri = getUri().buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true") - .build(); - for (Record record : records) { - HistoryRecord rec = (HistoryRecord) record; - if (rec.visits != null && rec.visits.size() != 0) { - int remoteVisitsInserted = context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(rec.guid, rec.visits) - ); - - // If we just inserted any visits, update remote visit aggregate values. - // While inserting visits, we might not insert all of rec.visits - if we already have a local - // visit record with matching (guid,date), we will skip that visit. - // Remote visits aggregate value will be incremented by number of visits inserted. - // Note that we don't need to set REMOTE_DATE_LAST_VISITED, because it already gets set above. - if (remoteVisitsInserted > 0) { - // Note that REMOTE_VISITS must be set before calling cr.update(...) with a URI - // that has PARAM_INCREMENT_REMOTE_AGGREGATES=true. - remoteVisitAggregateValues.put(BrowserContract.History.REMOTE_VISITS, remoteVisitsInserted); - context.getContentResolver().update( - historyIncrementRemoteAggregateUri, - remoteVisitAggregateValues, - BrowserContract.History.GUID + " = ?", new String[] {rec.guid} - ); - } - } - } - - return inserted; - } - - /** - * Helper method used to find largest <code>VisitsHelper.SYNC_DATE_KEY</code> value in a provided JSONArray. - * - * @param visits Array of objects which will be searched. - * @return largest value of <code>VisitsHelper.SYNC_DATE_KEY</code>. - */ - private long getLastVisited(JSONArray visits) { - long mostRecent = 0; - for (int i = 0; i < visits.size(); i++) { - final JSONObject visit = (JSONObject) visits.get(i); - long visitDate = (Long) visit.get(VisitsHelper.SYNC_DATE_KEY); - if (visitDate > mostRecent) { - mostRecent = visitDate; - } - } - return mostRecent; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java deleted file mode 100644 index bd2b5d31f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.HistoryRepository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public class AndroidBrowserHistoryRepository extends AndroidBrowserRepository implements HistoryRepository { - - @Override - protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { - AndroidBrowserHistoryRepositorySession session = new AndroidBrowserHistoryRepositorySession(AndroidBrowserHistoryRepository.this, context); - delegate.onSessionCreated(session); - } - - @Override - protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) { - return new AndroidBrowserHistoryDataAccessor(context); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java deleted file mode 100644 index 7c462abc3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java +++ /dev/null @@ -1,208 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.NoGuidForIdException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.ParentNotFoundException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.os.RemoteException; - -public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserRepositorySession { - public static final String LOG_TAG = "ABHistoryRepoSess"; - - /** - * The number of records to queue for insertion before writing to databases. - */ - public static final int INSERT_RECORD_THRESHOLD = 50; - public static final int RECENT_VISITS_LIMIT = 20; - - public AndroidBrowserHistoryRepositorySession(Repository repository, Context context) { - super(repository); - dbHelper = new AndroidBrowserHistoryDataAccessor(context); - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - // HACK: Fennec creates history records without a GUID. Mercilessly drop - // them on the floor. See Bug 739514. - try { - dbHelper.delete(BrowserContract.History.GUID + " IS NULL", null); - } catch (Exception e) { - // Ignore. - } - super.begin(delegate); - } - - @Override - protected Record retrieveDuringStore(Cursor cur) { - return RepoUtils.historyFromMirrorCursor(cur); - } - - @Override - protected Record retrieveDuringFetch(Cursor cur) { - return RepoUtils.historyFromMirrorCursor(cur); - } - - @Override - protected String buildRecordString(Record record) { - HistoryRecord hist = (HistoryRecord) record; - return hist.histURI; - } - - @Override - public boolean shouldIgnore(Record record) { - if (super.shouldIgnore(record)) { - return true; - } - if (!(record instanceof HistoryRecord)) { - return true; - } - HistoryRecord r = (HistoryRecord) record; - return !RepoUtils.isValidHistoryURI(r.histURI); - } - - @Override - protected Record transformRecord(Record record) throws NullCursorException { - return addVisitsToRecord(record); - } - - private Record addVisitsToRecord(Record record) throws NullCursorException { - Logger.debug(LOG_TAG, "Adding visits for GUID " + record.guid); - - // Sync is an object store, so what we attach here will replace what's already present on the Sync servers. - // We upload just a recent subset of visits for each history record for space and bandwidth reasons. - // We chose 20 to be conservative. See Bug 1164660 for details. - ContentProviderClient visitsClient = dbHelper.context.getContentResolver().acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI); - if (visitsClient == null) { - throw new IllegalStateException("Could not obtain a ContentProviderClient for Visits URI"); - } - - try { - ((HistoryRecord) record).visits = VisitsHelper.getRecentHistoryVisitsForGUID( - visitsClient, record.guid, RECENT_VISITS_LIMIT); - } catch (RemoteException e) { - throw new IllegalStateException("Error while obtaining visits for a record", e); - } finally { - visitsClient.release(); - } - - return record; - } - - @Override - protected Record prepareRecord(Record record) { - return record; - } - - protected final Object recordsBufferMonitor = new Object(); - protected ArrayList<HistoryRecord> recordsBuffer = new ArrayList<HistoryRecord>(); - - /** - * Queue record for insertion, possibly flushing the queue. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! But this is only - * called from <code>store</code>, which is called on the queue thread. - * - * @param record - * A <code>Record</code> with a GUID that is not present locally. - */ - @Override - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - enqueueNewRecord((HistoryRecord) prepareRecord(record)); - } - - /** - * Batch incoming records until some reasonable threshold is hit or storeDone - * is received. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! - * - * @param record A <code>Record</code> with a GUID that is not present locally. - * @throws NullCursorException - */ - protected void enqueueNewRecord(HistoryRecord record) throws NullCursorException { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() >= INSERT_RECORD_THRESHOLD) { - flushNewRecords(); - } - Logger.debug(LOG_TAG, "Enqueuing new record with GUID " + record.guid); - recordsBuffer.add(record); - } - } - - /** - * Flush queue of incoming records to database. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! - * <p> - * Must be locked by recordsBufferMonitor! - * @throws NullCursorException - */ - protected void flushNewRecords() throws NullCursorException { - if (recordsBuffer.size() < 1) { - Logger.debug(LOG_TAG, "No records to flush, returning."); - return; - } - - final ArrayList<HistoryRecord> outgoing = recordsBuffer; - recordsBuffer = new ArrayList<HistoryRecord>(); - Logger.debug(LOG_TAG, "Flushing " + outgoing.size() + " records to database."); - // TODO: move bulkInsert to AndroidBrowserDataAccessor? - int inserted = ((AndroidBrowserHistoryDataAccessor) dbHelper).bulkInsert(outgoing); - if (inserted != outgoing.size()) { - // Something failed; most pessimistic action is to declare that all insertions failed. - // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? - for (HistoryRecord failed : outgoing) { - delegate.onRecordStoreFailed(new RuntimeException("Failed to insert history item with guid " + failed.guid + "."), failed.guid); - } - return; - } - - // All good, everybody succeeded. - for (HistoryRecord succeeded : outgoing) { - try { - // Does not use androidID -- just GUID -> String map. - updateBookkeeping(succeeded); - } catch (NoGuidForIdException | ParentNotFoundException e) { - // Should not happen. - throw new NullCursorException(e); - } catch (NullCursorException e) { - throw e; - } - trackRecord(succeeded); - delegate.onRecordStoreSucceeded(succeeded.guid); // At this point, we are really inserted. - } - } - - @Override - public void storeDone() { - storeWorkQueue.execute(new Runnable() { - @Override - public void run() { - synchronized (recordsBufferMonitor) { - try { - flushNewRecords(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error flushing records to database.", e); - } - } - storeDone(System.currentTimeMillis()); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java deleted file mode 100644 index 6c5c661ee..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java +++ /dev/null @@ -1,74 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public abstract class AndroidBrowserRepository extends Repository { - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, Context context) { - new CreateSessionThread(delegate, context).start(); - } - - @Override - public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) { - // Only clean deleted records if success - if (success) { - new CleanThread(delegate, context).start(); - } - } - - class CleanThread extends Thread { - private final RepositorySessionCleanDelegate delegate; - private final Context context; - - public CleanThread(RepositorySessionCleanDelegate delegate, Context context) { - if (context == null) { - throw new IllegalArgumentException("context is null"); - } - this.delegate = delegate; - this.context = context; - } - - @Override - public void run() { - try { - getDataAccessor(context).purgeDeleted(); - } catch (Exception e) { - delegate.onCleanFailed(AndroidBrowserRepository.this, e); - return; - } - delegate.onCleaned(AndroidBrowserRepository.this); - } - } - - protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context); - protected abstract void sessionCreator(RepositorySessionCreationDelegate delegate, Context context); - - class CreateSessionThread extends Thread { - private final RepositorySessionCreationDelegate delegate; - private final Context context; - - public CreateSessionThread(RepositorySessionCreationDelegate delegate, Context context) { - if (context == null) { - throw new IllegalArgumentException("context is null."); - } - this.delegate = delegate; - this.context = context; - } - - @Override - public void run() { - sessionCreator(delegate, context); - } - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java deleted file mode 100644 index 138d63d4c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java +++ /dev/null @@ -1,232 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.List; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.CursorDumper; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public abstract class AndroidBrowserRepositoryDataAccessor { - - private static final String[] GUID_COLUMNS = new String[] { BrowserContract.SyncColumns.GUID }; - protected Context context; - protected static String LOG_TAG = "BrowserDataAccessor"; - protected final RepoUtils.QueryHelper queryHelper; - - public AndroidBrowserRepositoryDataAccessor(Context context) { - this.context = context; - this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG); - } - - protected abstract String[] getAllColumns(); - - /** - * Produce a <code>ContentValues</code> instance that represents the provided <code>Record</code>. - * - * @param record The <code>Record</code> to be converted. - * @return The <code>ContentValues</code> corresponding to <code>record</code>. - */ - protected abstract ContentValues getContentValues(Record record); - - protected abstract Uri getUri(); - - /** - * Dump all the records in raw format. - */ - public void dumpDB() { - Cursor cur = null; - try { - cur = queryHelper.safeQuery(".dumpDB", null, null, null, null); - CursorDumper.dumpCursor(cur); - } catch (NullCursorException e) { - } finally { - if (cur != null) { - cur.close(); - } - } - } - - public String dateModifiedWhere(long timestamp) { - return BrowserContract.SyncColumns.DATE_MODIFIED + " >= " + Long.toString(timestamp); - } - - public void delete(String where, String[] args) { - Uri uri = getUri(); - context.getContentResolver().delete(uri, where, args); - } - - public void wipe() { - Logger.debug(LOG_TAG, "Wiping."); - delete(null, null); - } - - public void purgeDeleted() throws NullCursorException { - String where = BrowserContract.SyncColumns.IS_DELETED + "= 1"; - Uri uri = getUri(); - Logger.info(LOG_TAG, "Purging deleted from: " + uri); - context.getContentResolver().delete(uri, where, null); - } - - /** - * Remove matching records from the database entirely, i.e., do not set a - * deleted flag, delete entirely. - * - * @param guid - * The GUID of the record to be deleted. - * @return The number of records deleted. - */ - public int purgeGuid(String guid) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - - int deleted = context.getContentResolver().delete(getUri(), where, args); - if (deleted != 1) { - Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " records for guid " + guid); - } - return deleted; - } - - public void update(String guid, Record newRecord) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - ContentValues cv = getContentValues(newRecord); - int updated = context.getContentResolver().update(getUri(), cv, where, args); - if (updated != 1) { - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid); - } - } - - public Uri insert(Record record) { - ContentValues cv = getContentValues(record); - return context.getContentResolver().insert(getUri(), cv); - } - - /** - * Fetch all records. - * <p> - * The caller is responsible for closing the cursor. - * - * @return A cursor. You </b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetchAll() throws NullCursorException { - return queryHelper.safeQuery(".fetchAll", getAllColumns(), null, null, null); - } - - /** - * Fetch GUIDs for records modified since the provided timestamp. - * <p> - * The caller is responsible for closing the cursor. - * - * @param timestamp A timestamp in milliseconds. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor getGUIDsSince(long timestamp) throws NullCursorException { - return queryHelper.safeQuery(".getGUIDsSince", - GUID_COLUMNS, - dateModifiedWhere(timestamp), - null, null); - } - - /** - * Fetch records modified since the provided timestamp. - * <p> - * The caller is responsible for closing the cursor. - * - * @param timestamp A timestamp in milliseconds. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetchSince(long timestamp) throws NullCursorException { - return queryHelper.safeQuery(".fetchSince", - getAllColumns(), - dateModifiedWhere(timestamp), - null, null); - } - - /** - * Fetch records for the provided GUIDs. - * <p> - * The caller is responsible for closing the cursor. - * - * @param guids The GUIDs of the records to fetch. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetch(String guids[]) throws NullCursorException { - String where = RepoUtils.computeSQLInClause(guids.length, "guid"); - return queryHelper.safeQuery(".fetch", getAllColumns(), where, guids, null); - } - - public void updateByGuid(String guid, ContentValues cv) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - - int updated = context.getContentResolver().update(getUri(), cv, where, args); - if (updated == 1) { - return; - } - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid); - } - - /** - * Insert records. - * <p> - * This inserts all the records (using <code>ContentProvider.bulkInsert</code>), - * but does <b>not</b> update the <code>androidID</code> of each record. - * - * @param records - * the records to insert. - * @return - * the number of records actually inserted. - * @throws NullCursorException - */ - public int bulkInsert(List<Record> records) throws NullCursorException { - if (records.isEmpty()) { - Logger.debug(LOG_TAG, "No records to insert, returning."); - } - - int size = records.size(); - ContentValues[] cvs = new ContentValues[size]; - int index = 0; - for (Record record : records) { - try { - cvs[index] = getContentValues(record); - index += 1; - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception in getContentValues for record with guid " + record.guid, e); - } - } - - if (index != size) { - // bulkInsert treats null ContentValues as blank rows, which we don't want - // to insert into the database. - // We expect exceptions in getContentValues to be exceedingly rare, so we - // re-allocate in the (rare) error case and maintain a fast path for the - // success case. - size = index; - } - - int inserted = context.getContentResolver().bulkInsert(getUri(), cvs); - if (inserted == size) { - Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected."); - } else { - Logger.debug(LOG_TAG, "Inserted " + - inserted + " records but expected " + - size + " records."); - } - return inserted; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java deleted file mode 100644 index 4f0da0bcc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java +++ /dev/null @@ -1,792 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidRequestException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.MultipleRecordsForGuidException; -import org.mozilla.gecko.sync.repositories.NoGuidForIdException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.ParentNotFoundException; -import org.mozilla.gecko.sync.repositories.ProfileDatabaseException; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.database.Cursor; -import android.net.Uri; -import android.util.SparseArray; - -/** - * You'll notice that all delegate calls *either*: - * - * - request a deferred delegate with the appropriate work queue, then - * make the appropriate call, or - * - create a Runnable which makes the appropriate call, and pushes it - * directly into the appropriate work queue. - * - * This is to ensure that all delegate callbacks happen off the current - * thread. This provides lock safety (we don't enter another method that - * might try to take a lock already taken in our caller), and ensures - * that operations take place off the main thread. - * - * Don't do both -- the two approaches are equivalent -- and certainly - * don't do neither unless you know what you're doing! - * - * Similarly, all store calls go through the appropriate store queue. This - * ensures that store() and storeDone() consequences occur before-after. - * - * @author rnewman - * - */ -public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepositorySession { - public static final String LOG_TAG = "BrowserRepoSession"; - - protected AndroidBrowserRepositoryDataAccessor dbHelper; - - /** - * In order to reconcile the "same record" with two *different* GUIDs (for - * example, the same bookmark created by two different clients), we maintain a - * mapping for each local record from a "record string" to - * "local record GUID". - * <p> - * The "record string" above is a "record identifying unique key" produced by - * <code>buildRecordString</code>. - * <p> - * Since we hash each "record string", this map may produce a false positive. - * In this case, we search the database for a matching record explicitly using - * <code>findByRecordString</code>. - */ - protected SparseArray<String> recordToGuid; - - public AndroidBrowserRepositorySession(Repository repository) { - super(repository); - } - - /** - * Retrieve a record from a cursor. Act as if we don't know the final contents of - * the record: for example, a folder's child array might change. - * - * Return null if this record should not be processed. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected abstract Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException; - - /** - * Retrieve a record from a cursor. Ensure that the contents of the database are - * updated to match the record that we're constructing: for example, the children - * of a folder might be repositioned as we generate the folder's record. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected abstract Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException; - - /** - * Override this to allow records to be skipped during insertion. - * - * For example, a session subclass might skip records of an unsupported type. - */ - @SuppressWarnings("static-method") - public boolean shouldIgnore(Record record) { - return false; - } - - /** - * Perform any necessary transformation of a record prior to searching by - * any field other than GUID. - * - * Example: translating remote folder names into local names. - */ - @SuppressWarnings("static-method") - protected void fixupRecord(Record record) { - return; - } - - /** - * Override in subclass to implement record extension. - * - * Populate any fields of the record that are expensive to calculate, - * prior to reconciling. - * - * Example: computing children arrays. - * - * Return null if this record should not be processed. - * - * @param record - * The record to transform. Can be null. - * @return The transformed record. Can be null. - * @throws NullCursorException - */ - @SuppressWarnings("static-method") - protected Record transformRecord(Record record) throws NullCursorException { - return record; - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue); - super.sharedBegin(); - - try { - // We do this check here even though it results in one extra call to the DB - // because if we didn't, we have to do a check on every other call since there - // is no way of knowing which call would be hit first. - checkDatabase(); - } catch (ProfileDatabaseException e) { - Logger.error(LOG_TAG, "ProfileDatabaseException from begin. Fennec must be launched once until this error is fixed"); - deferredDelegate.onBeginFailed(e); - return; - } catch (Exception e) { - deferredDelegate.onBeginFailed(e); - return; - } - storeTracker = createStoreTracker(); - deferredDelegate.onBeginSucceeded(this); - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - dbHelper = null; - recordToGuid = null; - super.finish(delegate); - } - - /** - * Produce a "record string" (record identifying unique key). - * - * @param record - * the <code>Record</code> to identify. - * @return a <code>String</code> instance. - */ - protected abstract String buildRecordString(Record record); - - protected void checkDatabase() throws ProfileDatabaseException, NullCursorException { - Logger.debug(LOG_TAG, "BEGIN: checking database."); - try { - dbHelper.fetch(new String[] { "none" }).close(); - Logger.debug(LOG_TAG, "END: checking database."); - } catch (NullPointerException e) { - throw new ProfileDatabaseException(e); - } - } - - @Override - public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) { - GuidsSinceRunnable command = new GuidsSinceRunnable(timestamp, delegate); - delegateQueue.execute(command); - } - - class GuidsSinceRunnable implements Runnable { - - private final RepositorySessionGuidsSinceDelegate delegate; - private final long timestamp; - - public GuidsSinceRunnable(long timestamp, - RepositorySessionGuidsSinceDelegate delegate) { - this.timestamp = timestamp; - this.delegate = delegate; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - Cursor cur; - try { - cur = dbHelper.getGUIDsSince(timestamp); - } catch (Exception e) { - delegate.onGuidsSinceFailed(e); - return; - } - - ArrayList<String> guids; - try { - if (!cur.moveToFirst()) { - delegate.onGuidsSinceSucceeded(new String[] {}); - return; - } - guids = new ArrayList<String>(); - while (!cur.isAfterLast()) { - guids.add(RepoUtils.getStringFromCursor(cur, "guid")); - cur.moveToNext(); - } - } finally { - Logger.debug(LOG_TAG, "Closing cursor after guidsSince."); - cur.close(); - } - - String guidsArray[] = new String[guids.size()]; - guids.toArray(guidsArray); - delegate.onGuidsSinceSucceeded(guidsArray); - } - } - - @Override - public void fetch(String[] guids, - RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException { - FetchRunnable command = new FetchRunnable(guids, now(), null, delegate); - executeDelegateCommand(command); - } - - abstract class FetchingRunnable implements Runnable { - protected final RepositorySessionFetchRecordsDelegate delegate; - - public FetchingRunnable(RepositorySessionFetchRecordsDelegate delegate) { - this.delegate = delegate; - } - - protected void fetchFromCursor(Cursor cursor, RecordFilter filter, long end) { - Logger.debug(LOG_TAG, "Fetch from cursor:"); - try { - try { - if (!cursor.moveToFirst()) { - delegate.onFetchCompleted(end); - return; - } - while (!cursor.isAfterLast()) { - Record r = retrieveDuringFetch(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.trace(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(transformRecord(r)); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - cursor.moveToNext(); - } - delegate.onFetchCompleted(end); - } catch (NoGuidForIdException e) { - Logger.warn(LOG_TAG, "No GUID for ID.", e); - delegate.onFetchFailed(e, null); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception in fetchFromCursor.", e); - delegate.onFetchFailed(e, null); - return; - } - } finally { - Logger.trace(LOG_TAG, "Closing cursor after fetch."); - cursor.close(); - } - } - } - - public class FetchRunnable extends FetchingRunnable { - private final String[] guids; - private final long end; - private final RecordFilter filter; - - public FetchRunnable(String[] guids, - long end, - RecordFilter filter, - RepositorySessionFetchRecordsDelegate delegate) { - super(delegate); - this.guids = guids; - this.end = end; - this.filter = filter; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - if (guids == null || guids.length < 1) { - Logger.error(LOG_TAG, "No guids sent to fetch"); - delegate.onFetchFailed(new InvalidRequestException(null), null); - return; - } - - try { - Cursor cursor = dbHelper.fetch(guids); - this.fetchFromCursor(cursor, filter, end); - } catch (NullCursorException e) { - delegate.onFetchFailed(e, null); - } - } - } - - @Override - public void fetchSince(long timestamp, - RepositorySessionFetchRecordsDelegate delegate) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - - Logger.debug(LOG_TAG, "Running fetchSince(" + timestamp + ")."); - FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), this.storeTracker.getFilter(), delegate); - delegateQueue.execute(command); - } - - class FetchSinceRunnable extends FetchingRunnable { - private final long since; - private final long end; - private final RecordFilter filter; - - public FetchSinceRunnable(long since, - long end, - RecordFilter filter, - RepositorySessionFetchRecordsDelegate delegate) { - super(delegate); - this.since = since; - this.end = end; - this.filter = filter; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - try { - Cursor cursor = dbHelper.fetchSince(since); - this.fetchFromCursor(cursor, filter, end); - } catch (NullCursorException e) { - delegate.onFetchFailed(e, null); - return; - } - } - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - this.fetchSince(0, delegate); - } - - protected int storeCount = 0; - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to AndroidBrowserRepositorySession.store()."); - } - - storeCount += 1; - Logger.debug(LOG_TAG, "Storing record with GUID " + record.guid + " (stored " + storeCount + " records this session)."); - - // Store Runnables *must* complete synchronously. It's OK, they - // run on a background thread. - Runnable command = new Runnable() { - - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "AndroidBrowserRepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - // Check that the record is a valid type. - // Fennec only supports bookmarks and folders. All other types of records, - // including livemarks and queries, are simply ignored. - // See Bug 708149. This might be resolved by Fennec changing its database - // schema, or by Sync storing non-applied records in its own private database. - if (shouldIgnore(record)) { - Logger.debug(LOG_TAG, "Ignoring record " + record.guid); - - // Don't throw: we don't want to abort the entire sync when we get a livemark! - // delegate.onRecordStoreFailed(new InvalidBookmarkTypeException(null)); - return; - } - - - // TODO: lift these into the session. - // Temporary: this matches prior syncing semantics, in which only - // the relationship between the local and remote record is considered. - // In the future we'll track these two timestamps and use them to - // determine which records have changed, and thus process incoming - // records more efficiently. - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = record.lastModified > lastRemoteRetrieval; - - Record existingRecord; - try { - // GUID matching only: deleted records don't have a payload with which to search. - existingRecord = retrieveByGUIDDuringStore(record.guid); - if (record.deleted) { - if (existingRecord == null) { - // We're done. Don't bother with a callback. That can change later - // if we want it to. - trace("Incoming record " + record.guid + " is deleted, and no local version. Bye!"); - return; - } - - if (existingRecord.deleted) { - trace("Local record already deleted. Bye!"); - return; - } - - // Which one wins? - if (!remotelyModified) { - trace("Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - trace("Remote modified, local not. Deleting."); - storeRecordDeletion(record, existingRecord); - return; - } - - trace("Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - trace("Remote is newer, and deleted. Deleting local."); - storeRecordDeletion(record, existingRecord); - return; - } - - trace("Remote is older, local is not deleted. Ignoring."); - return; - } - // End deletion logic. - - // Now we're processing a non-deleted incoming record. - // Apply any changes we need in order to correctly find existing records. - fixupRecord(record); - - if (existingRecord == null) { - trace("Looking up match for record " + record.guid); - existingRecord = findExistingRecord(record); - } - - if (existingRecord == null) { - // The record is new. - trace("No match. Inserting."); - insert(record); - return; - } - - // We found a local dupe. - trace("Incoming record " + record.guid + " dupes to local record " + existingRecord.guid); - - // Populate more expensive fields prior to reconciling. - existingRecord = transformRecord(existingRecord); - Record toStore = reconcileRecords(record, existingRecord, lastRemoteRetrieval, lastLocalRetrieval); - - if (toStore == null) { - Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record."); - return; - } - - // TODO: pass in timestamps? - - // This section of code will only run if the incoming record is not - // marked as deleted, so we never want to just drop ours from the database: - // we need to upload it later. - // Allowing deleted items to propagate through `replace` allows normal - // logging and side-effects to occur, and is no more expensive than simply - // bumping the modified time. - Logger.debug(LOG_TAG, "Replacing existing " + existingRecord.guid + - (toStore.deleted ? " with deleted record " : " with record ") + - toStore.guid); - Record replaced = replace(toStore, existingRecord); - - // Note that we don't track records here; deciding that is the job - // of reconcileRecords. - Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid + - "(" + replaced.androidID + ")"); - delegate.onRecordStoreSucceeded(replaced.guid); - return; - - } catch (MultipleRecordsForGuidException e) { - Logger.error(LOG_TAG, "Multiple records returned for given guid: " + record.guid); - delegate.onRecordStoreFailed(e, record.guid); - return; - } catch (NoGuidForIdException e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } catch (Exception e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - }; - storeWorkQueue.execute(command); - } - - /** - * Process a request for deletion of a record. - * Neither argument will ever be null. - * - * @param record the incoming record. This will be mostly blank, given that it's a deletion. - * @param existingRecord the existing record. Use this to decide how to process the deletion. - */ - protected void storeRecordDeletion(final Record record, final Record existingRecord) { - // TODO: we ought to mark the record as deleted rather than purging it, - // in order to support syncing to multiple destinations. Bug 722607. - dbHelper.purgeGuid(record.guid); - delegate.onRecordStoreSucceeded(record.guid); - } - - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Record toStore = prepareRecord(record); - Uri recordURI = dbHelper.insert(toStore); - if (recordURI == null) { - throw new NullCursorException(new RuntimeException("Got null URI inserting record with guid " + record.guid)); - } - toStore.androidID = ContentUris.parseId(recordURI); - - updateBookkeeping(toStore); - trackRecord(toStore); - delegate.onRecordStoreSucceeded(toStore.guid); - - Logger.debug(LOG_TAG, "Inserted record with guid " + toStore.guid + " as androidID " + toStore.androidID); - } - - protected Record replace(Record newRecord, Record existingRecord) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Record toStore = prepareRecord(newRecord); - - // newRecord should already have suitable androidID and guid. - dbHelper.update(existingRecord.guid, toStore); - updateBookkeeping(toStore); - Logger.debug(LOG_TAG, "replace() returning record " + toStore.guid); - return toStore; - } - - /** - * Retrieve a record from the store by GUID, without writing unnecessarily to the - * database. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - * @throws MultipleRecordsForGuidException - */ - protected Record retrieveByGUIDDuringStore(String guid) throws - NoGuidForIdException, - NullCursorException, - ParentNotFoundException, - MultipleRecordsForGuidException { - Cursor cursor = dbHelper.fetch(new String[] { guid }); - try { - if (!cursor.moveToFirst()) { - return null; - } - - Record r = retrieveDuringStore(cursor); - - cursor.moveToNext(); - if (cursor.isAfterLast()) { - // Got one record! - return r; // Not transformed. - } - - // More than one. Oh dear. - throw (new MultipleRecordsForGuidException(null)); - } finally { - cursor.close(); - } - } - - /** - * Attempt to find an equivalent record through some means other than GUID. - * - * @param record - * The record for which to search. - * @return - * An equivalent Record object, or null if none is found. - * - * @throws MultipleRecordsForGuidException - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException, - NoGuidForIdException, NullCursorException, ParentNotFoundException { - - Logger.debug(LOG_TAG, "Finding existing record for incoming record with GUID " + record.guid); - String recordString = buildRecordString(record); - if (recordString == null) { - Logger.debug(LOG_TAG, "No record string for incoming record " + record.guid); - return null; - } - - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Searching with record string " + recordString); - } else { - Logger.debug(LOG_TAG, "Searching with record string."); - } - String guid = getGuidForString(recordString); - if (guid == null) { - Logger.debug(LOG_TAG, "Failed to find existing record for " + record.guid); - return null; - } - - // Our map contained a match, but it could be a false positive. Since - // computed record string is supposed to be a unique key, we can easily - // verify our positive. - Logger.debug(LOG_TAG, "Found one. Checking stored record."); - Record stored = retrieveByGUIDDuringStore(guid); - String storedRecordString = buildRecordString(record); - if (recordString.equals(storedRecordString)) { - Logger.debug(LOG_TAG, "Existing record matches incoming record. Returning existing record."); - return stored; - } - - // Oh no, we got a false positive! (This should be *very* rare -- - // essentially, we got a hash collision.) Search the DB for this record - // explicitly by hand. - Logger.debug(LOG_TAG, "Existing record does not match incoming record. Trying to find record by record string."); - return findByRecordString(recordString); - } - - protected String getGuidForString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - if (recordToGuid == null) { - createRecordToGuidMap(); - } - return recordToGuid.get(recordString.hashCode()); - } - - protected void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Logger.info(LOG_TAG, "BEGIN: creating record -> GUID map."); - recordToGuid = new SparseArray<String>(); - - // TODO: we should be able to do this entire thing with string concatenations within SQL. - // Also consider whether it's better to fetch and process every record in the DB into - // memory, or run a query per record to do the same thing. - Cursor cur = dbHelper.fetchAll(); - try { - if (!cur.moveToFirst()) { - return; - } - while (!cur.isAfterLast()) { - Record record = retrieveDuringStore(cur); - if (record != null) { - final String recordString = buildRecordString(record); - if (recordString != null) { - recordToGuid.put(recordString.hashCode(), record.guid); - } - } - cur.moveToNext(); - } - } finally { - cur.close(); - } - Logger.info(LOG_TAG, "END: creating record -> GUID map."); - } - - /** - * Search the local database for a record with the same "record string". - * <p> - * We expect to do this only in the unlikely event of a hash - * collision, so we iterate the database completely. Since we want - * to include information about the parents of bookmarks, it is - * difficult to do better purely using the - * <code>ContentProvider</code> interface. - * - * @param recordString - * the "record string" to search for; must be n - * @return a <code>Record</code> with the same "record string", or - * <code>null</code> if none is present. - * @throws ParentNotFoundException - * @throws NullCursorException - * @throws NoGuidForIdException - */ - protected Record findByRecordString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Cursor cur = dbHelper.fetchAll(); - try { - if (!cur.moveToFirst()) { - return null; - } - while (!cur.isAfterLast()) { - Record record = retrieveDuringStore(cur); - if (record != null) { - final String storedRecordString = buildRecordString(record); - if (recordString.equals(storedRecordString)) { - return record; - } - } - cur.moveToNext(); - } - return null; - } finally { - cur.close(); - } - } - - public void putRecordToGuidMap(String recordString, String guid) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - if (recordString == null) { - return; - } - - if (recordToGuid == null) { - createRecordToGuidMap(); - } - recordToGuid.put(recordString.hashCode(), guid); - } - - protected abstract Record prepareRecord(Record record); - - protected void updateBookkeeping(Record record) throws NoGuidForIdException, - NullCursorException, - ParentNotFoundException { - putRecordToGuidMap(buildRecordString(record), record.guid); - } - - protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) { - return new WipeRunnable(delegate); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - Runnable command = getWipeRunnable(delegate); - storeWorkQueue.execute(command); - } - - class WipeRunnable implements Runnable { - protected RepositorySessionWipeDelegate delegate; - - public WipeRunnable(RepositorySessionWipeDelegate delegate) { - this.delegate = delegate; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - dbHelper.wipe(); - delegate.onWipeSucceeded(); - } - } - - // For testing purposes. - public AndroidBrowserRepositoryDataAccessor getDBHelper() { - return dbHelper; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java deleted file mode 100644 index d8d8756f7..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java +++ /dev/null @@ -1,239 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; - -/** - * Queue up deletions. Process them at the end. - * - * Algorithm: - * - * * Collect GUIDs as we go. For convenience we partition these into - * folders and non-folders. - * - * * Non-folders can be deleted in batches as we go. - * - * * At the end of the sync: - * * Delete all that aren't folders. - * * Move the remaining children of any that are folders to an "Orphans" folder. - * - We do this even for children that are _marked_ as deleted -- we still want - * to upload them, and their parent is irrelevant. - * * Delete all the folders. - * - * * Any outstanding records -- the ones we moved to "Orphans" -- are true orphans. - * These should be reuploaded (because their parent has changed), as should their - * new parent (because its children array has changed). - * We achieve the former by moving them without tracking (but we don't make any - * special effort here -- warning! Lurking bug!). - * We achieve the latter by bumping its mtime. The caller should take care of untracking it. - * - * Note that we make no particular effort to handle repositioning or reparenting: - * batching deletes at the end should be handled seamlessly by existing code, - * because the deleted records could have arrived in a batch at the end regardless. - * - * Note that this class is not thread safe. This should be fine: call it only - * from within a store runnable. - * - */ -public class BookmarksDeletionManager { - private static final String LOG_TAG = "BookmarkDelete"; - - private final AndroidBrowserBookmarksDataAccessor dataAccessor; - private RepositorySessionStoreDelegate delegate; - - private final int flushThreshold; - - private final HashSet<String> folders = new HashSet<String>(); - private final HashSet<String> nonFolders = new HashSet<String>(); - private int nonFolderCount = 0; - - // Records that we need to touch once we've deleted the non-folders. - private HashSet<String> nonFolderParents = new HashSet<String>(); - private HashSet<String> folderParents = new HashSet<String>(); - - /** - * Create an instance to be used for tracking deletions in a bookmarks - * repository session. - * - * @param dataAccessor - * Used to effect database changes. - * - * @param flushThreshold - * When this many non-folder records have been stored for deletion, - * an incremental flush occurs. - */ - public BookmarksDeletionManager(AndroidBrowserBookmarksDataAccessor dataAccessor, int flushThreshold) { - this.dataAccessor = dataAccessor; - this.flushThreshold = flushThreshold; - } - - /** - * Set the delegate to use for callbacks. - * If not invoked, no callbacks will be submitted. - * - * @param delegate a delegate, which should already be a delayed delegate. - */ - public void setDelegate(RepositorySessionStoreDelegate delegate) { - this.delegate = delegate; - } - - public void deleteRecord(String guid, boolean isFolder, String parentGUID) { - if (guid == null) { - Logger.warn(LOG_TAG, "Cannot queue deletion of record with no GUID."); - return; - } - Logger.debug(LOG_TAG, "Queuing deletion of " + guid); - - if (isFolder) { - folders.add(guid); - if (!folders.contains(parentGUID)) { - // We're not going to delete its parent; will need to bump it. - folderParents.add(parentGUID); - } - - nonFolderParents.remove(guid); - folderParents.remove(guid); - return; - } - - if (!folders.contains(parentGUID)) { - // We're not going to delete its parent; will need to bump it. - nonFolderParents.add(parentGUID); - } - - if (nonFolders.add(guid)) { - if (++nonFolderCount >= flushThreshold) { - deleteNonFolders(); - } - } - } - - /** - * Flush deletions that can be easily taken care of right now. - */ - public void incrementalFlush() { - // Yes, this means we only bump when we finish, not during an incremental flush. - deleteNonFolders(); - } - - /** - * Apply all pending deletions and reset state for the next batch of stores. - * - * @param orphanDestination the ID of the folder to which orphaned children - * should be moved. - * - * @throws NullCursorException - * @return a set of IDs to untrack. Will not be null. - */ - public Set<String> flushAll(long orphanDestination, long now) throws NullCursorException { - Logger.debug(LOG_TAG, "Doing complete flush of deleted items. Moving orphans to " + orphanDestination); - deleteNonFolders(); - - // Find out which parents *won't* be deleted, and thus need to have their - // modified times bumped. - nonFolderParents.removeAll(folders); - - Logger.debug(LOG_TAG, "Bumping modified times for " + nonFolderParents.size() + - " parents of deleted non-folders."); - dataAccessor.bumpModifiedByGUID(nonFolderParents, now); - - if (folders.size() > 0) { - final String[] folderGUIDs = folders.toArray(new String[folders.size()]); - final String[] folderIDs = getIDs(folderGUIDs); // Throws if any don't exist. - int moved = dataAccessor.moveChildren(folderIDs, orphanDestination); - if (moved > 0) { - dataAccessor.bumpModified(orphanDestination, now); - } - - // We've deleted or moved anything that might be under these folders. - // Just delete them. - final String folderWhere = RepoUtils.computeSQLInClause(folders.size(), BrowserContract.Bookmarks.GUID); - dataAccessor.delete(folderWhere, folderGUIDs); - invokeCallbacks(delegate, folderGUIDs); - - folderParents.removeAll(folders); - Logger.debug(LOG_TAG, "Bumping modified times for " + folderParents.size() + - " parents of deleted folders."); - dataAccessor.bumpModifiedByGUID(folderParents, now); - - // Clean up. - folders.clear(); - } - - HashSet<String> ret = nonFolderParents; - ret.addAll(folderParents); - - nonFolderParents = new HashSet<String>(); - folderParents = new HashSet<String>(); - return ret; - } - - private String[] getIDs(String[] guids) throws NullCursorException { - // Convert GUIDs to numeric IDs. - String[] ids = new String[guids.length]; - Map<String, Long> guidsToIDs = dataAccessor.idsForGUIDs(guids); - for (int i = 0; i < guids.length; ++i) { - String guid = guids[i]; - Long id = guidsToIDs.get(guid); - if (id == null) { - throw new IllegalArgumentException("Can't get ID for unknown record " + guid); - } - ids[i] = id.toString(); - } - return ids; - } - - /** - * Flush non-folder deletions. This can be called at any time. - */ - private void deleteNonFolders() { - if (nonFolderCount == 0) { - Logger.debug(LOG_TAG, "No non-folders to delete."); - return; - } - - Logger.debug(LOG_TAG, "Applying deletion of " + nonFolderCount + " non-folders."); - final String[] nonFolderGUIDs = nonFolders.toArray(new String[nonFolderCount]); - final String nonFolderWhere = RepoUtils.computeSQLInClause(nonFolderCount, BrowserContract.Bookmarks.GUID); - dataAccessor.delete(nonFolderWhere, nonFolderGUIDs); - - invokeCallbacks(delegate, nonFolderGUIDs); - - // Discard these. - // Note that we maintain folderParents and nonFolderParents; we need them later. - nonFolders.clear(); - nonFolderCount = 0; - } - - private void invokeCallbacks(RepositorySessionStoreDelegate delegate, - String[] nonFolderGUIDs) { - if (delegate == null) { - return; - } - Logger.trace(LOG_TAG, "Invoking store callback for " + nonFolderGUIDs.length + " GUIDs."); - for (String guid : nonFolderGUIDs) { - delegate.onRecordStoreSucceeded(guid); - } - } - - /** - * Clear state in case of redundancy (e.g., wipe). - */ - public void clear() { - nonFolders.clear(); - nonFolderCount = 0; - folders.clear(); - nonFolderParents.clear(); - folderParents.clear(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java deleted file mode 100644 index 98670d39b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java +++ /dev/null @@ -1,298 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; - -/** - * Queue up insertions: - * <ul> - * <li>Folder inserts where the parent is known. Do these immediately, because - * they allow other records to be inserted. Requires bookkeeping updates. On - * insert, flush the next set.</li> - * <li>Regular inserts where the parent is known. These can happen whenever. - * Batch for speed.</li> - * <li>Records where the parent is not known. These can be flushed out when the - * parent is known, or entered as orphans. This can be a queue earlier in the - * process, so they don't get assigned to Unsorted. Feed into the main batch - * when the parent arrives.</li> - * </ul> - * <p> - * Deletions are always done at the end so that orphaning is minimized, and - * that's why we are batching folders and non-folders separately. - * <p> - * Updates are always applied as they arrive. - * <p> - * Note that this class is not thread safe. This should be fine: call it only - * from within a store runnable. - */ -public class BookmarksInsertionManager { - public static final String LOG_TAG = "BookmarkInsert"; - public static boolean DEBUG = false; - - protected final int flushThreshold; - protected final BookmarkInserter inserter; - - /** - * Folders that have been successfully inserted. - */ - private final Set<String> insertedFolders = new HashSet<String>(); - - /** - * Non-folders waiting for bulk insertion. - * <p> - * We write in insertion order to keep things easy to debug. - */ - private final Set<BookmarkRecord> nonFoldersToWrite = new LinkedHashSet<BookmarkRecord>(); - - /** - * Map from parent folder GUID to child records (folders and non-folders) - * waiting to be enqueued after parent folder is inserted. - */ - private final Map<String, Set<BookmarkRecord>> recordsWaitingForParent = new HashMap<String, Set<BookmarkRecord>>(); - - /** - * Create an instance to be used for tracking insertions in a bookmarks - * repository session. - * - * @param flushThreshold - * When this many non-folder records have been stored for insertion, - * an incremental flush occurs. - * @param insertedFolders - * The GUIDs of all the folders already inserted into the database. - * @param inserter - * The <code>BookmarkInsert</code> to use. - */ - public BookmarksInsertionManager(int flushThreshold, Collection<String> insertedFolders, BookmarkInserter inserter) { - this.flushThreshold = flushThreshold; - this.insertedFolders.addAll(insertedFolders); - this.inserter = inserter; - } - - protected void addRecordWithUnwrittenParent(BookmarkRecord record) { - Set<BookmarkRecord> destination = recordsWaitingForParent.get(record.parentID); - if (destination == null) { - destination = new LinkedHashSet<BookmarkRecord>(); - recordsWaitingForParent.put(record.parentID, destination); - } - destination.add(record); - } - - /** - * If <code>record</code> is a folder, insert it immediately; if it is a - * non-folder, enqueue it. Then do the same for any records waiting for this record. - * - * @param record - * the <code>BookmarkRecord</code> to enqueue. - */ - protected void recursivelyEnqueueRecordAndChildren(BookmarkRecord record) { - if (record.isFolder()) { - if (!inserter.insertFolder(record)) { - Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!"); - return; - } - Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders."); - insertedFolders.add(record.guid); - } else { - Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue."); - nonFoldersToWrite.add(record); - } - - // Now process record's children. - Set<BookmarkRecord> waiting = recordsWaitingForParent.remove(record.guid); - if (waiting == null) { - return; - } - for (BookmarkRecord waiter : waiting) { - recursivelyEnqueueRecordAndChildren(waiter); - } - } - - /** - * Enqueue a folder. - * - * @param record - * the folder to enqueue. - */ - protected void enqueueFolder(BookmarkRecord record) { - Logger.debug(LOG_TAG, "Inserting folder with guid " + record.guid); - - if (!insertedFolders.contains(record.parentID)) { - Logger.debug(LOG_TAG, "Folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent."); - addRecordWithUnwrittenParent(record); - return; - } - - // Parent is known; add as much of the tree as this roots. - recursivelyEnqueueRecordAndChildren(record); - flushNonFoldersIfNecessary(); - } - - /** - * Enqueue a non-folder. - * - * @param record - * the non-folder to enqueue. - */ - protected void enqueueNonFolder(BookmarkRecord record) { - Logger.debug(LOG_TAG, "Inserting non-folder with guid " + record.guid); - - if (!insertedFolders.contains(record.parentID)) { - Logger.debug(LOG_TAG, "Non-folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent."); - addRecordWithUnwrittenParent(record); - return; - } - - // Parent is known; add to insertion queue and maybe write. - Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue."); - nonFoldersToWrite.add(record); - flushNonFoldersIfNecessary(); - } - - /** - * Enqueue a bookmark record for eventual insertion. - * - * @param record - * the <code>BookmarkRecord</code> to enqueue. - */ - public void enqueueRecord(BookmarkRecord record) { - if (record.isFolder()) { - enqueueFolder(record); - } else { - enqueueNonFolder(record); - } - if (DEBUG) { - dumpState(); - } - } - - /** - * Flush non-folders; empties the insertion queue entirely. - */ - protected void flushNonFolders() { - inserter.bulkInsertNonFolders(nonFoldersToWrite); // All errors are handled in bulkInsertNonFolders. - nonFoldersToWrite.clear(); - } - - /** - * Flush non-folder insertions if there are many of them; empties the - * insertion queue entirely. - */ - protected void flushNonFoldersIfNecessary() { - int num = nonFoldersToWrite.size(); - if (num < flushThreshold) { - Logger.debug(LOG_TAG, "Incremental flush called with " + num + " < " + flushThreshold + " non-folders; not flushing."); - return; - } - Logger.debug(LOG_TAG, "Incremental flush called with " + num + " non-folders; flushing."); - flushNonFolders(); - } - - /** - * Insert all remaining folders followed by all remaining non-folders, - * regardless of whether parent records have been successfully inserted. - */ - public void finishUp() { - // Iterate through all waiting records, writing the folders and collecting - // the non-folders for bulk insertion. - int numFolders = 0; - int numNonFolders = 0; - for (Set<BookmarkRecord> records : recordsWaitingForParent.values()) { - for (BookmarkRecord record : records) { - if (!record.isFolder()) { - numNonFolders += 1; - nonFoldersToWrite.add(record); - continue; - } - - numFolders += 1; - if (!inserter.insertFolder(record)) { - Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!"); - continue; - } - - Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders."); - insertedFolders.add(record.guid); - } - } - recordsWaitingForParent.clear(); - flushNonFolders(); - - Logger.debug(LOG_TAG, "finishUp inserted " + - numFolders + " folders without known parents and " + - numNonFolders + " non-folders without known parents."); - if (DEBUG) { - dumpState(); - } - } - - public void clear() { - this.insertedFolders.clear(); - this.nonFoldersToWrite.clear(); - this.recordsWaitingForParent.clear(); - } - - // For debugging. - public boolean isClear() { - return nonFoldersToWrite.isEmpty() && recordsWaitingForParent.isEmpty(); - } - - // For debugging. - public void dumpState() { - ArrayList<String> readies = new ArrayList<String>(); - for (BookmarkRecord record : nonFoldersToWrite) { - readies.add(record.guid); - } - String ready = Utils.toCommaSeparatedString(new ArrayList<String>(readies)); - - ArrayList<String> waits = new ArrayList<String>(); - for (Set<BookmarkRecord> recs : recordsWaitingForParent.values()) { - for (BookmarkRecord rec : recs) { - waits.add(rec.guid); - } - } - String waiting = Utils.toCommaSeparatedString(waits); - String known = Utils.toCommaSeparatedString(insertedFolders); - - Logger.debug(LOG_TAG, "Q=(" + ready + "), W = (" + waiting + "), P=(" + known + ")"); - } - - public interface BookmarkInserter { - /** - * Insert a single folder. - * <p> - * All exceptions should be caught and all delegate callbacks invoked here. - * - * @param record - * the record to insert. - * @return - * <code>true</code> if the folder was inserted; <code>false</code> otherwise. - */ - public boolean insertFolder(BookmarkRecord record); - - /** - * Insert many non-folders. Each non-folder's parent was already present in - * the database before this <code>BookmarkInsertionsManager</code> was - * created, or had <code>insertFolder</code> called with it as argument (and - * possibly was not inserted). - * <p> - * All exceptions should be caught and all delegate callbacks invoked here. - * - * @param records - * the records to insert. - */ - public void bulkInsertNonFolders(Collection<BookmarkRecord> records); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java deleted file mode 100644 index e83aea087..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java +++ /dev/null @@ -1,154 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.setup.Constants; - -import android.net.Uri; - -public class BrowserContractHelpers extends BrowserContract { - - protected static Uri withSyncAndDeletedAndProfile(Uri u) { - return u.buildUpon() - .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) - .appendQueryParameter(PARAM_IS_SYNC, "true") - .appendQueryParameter(PARAM_SHOW_DELETED, "true") - .build(); - } - protected static Uri withSyncAndProfile(Uri u) { - return u.buildUpon() - .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) - .appendQueryParameter(PARAM_IS_SYNC, "true") - .build(); - } - - public static final Uri BOOKMARKS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.CONTENT_URI); - public static final Uri BOOKMARKS_PARENTS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.PARENTS_CONTENT_URI); - public static final Uri BOOKMARKS_POSITIONS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.POSITIONS_CONTENT_URI); - public static final Uri HISTORY_CONTENT_URI = withSyncAndDeletedAndProfile(History.CONTENT_URI); - public static final Uri VISITS_CONTENT_URI = withSyncAndDeletedAndProfile(Visits.CONTENT_URI); - public static final Uri SCHEMA_CONTENT_URI = withSyncAndDeletedAndProfile(Schema.CONTENT_URI); - public static final Uri PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(Passwords.CONTENT_URI); - public static final Uri DELETED_PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(DeletedPasswords.CONTENT_URI); - public static final Uri FORM_HISTORY_CONTENT_URI = withSyncAndProfile(FormHistory.CONTENT_URI); - public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSyncAndProfile(DeletedFormHistory.CONTENT_URI); - public static final Uri TABS_CONTENT_URI = withSyncAndProfile(Tabs.CONTENT_URI); - public static final Uri CLIENTS_CONTENT_URI = withSyncAndProfile(Clients.CONTENT_URI); - public static final Uri LOGINS_CONTENT_URI = withSyncAndProfile(Logins.CONTENT_URI); - - public static final String[] PasswordColumns = new String[] { - Passwords.ID, - Passwords.HOSTNAME, - Passwords.HTTP_REALM, - Passwords.FORM_SUBMIT_URL, - Passwords.USERNAME_FIELD, - Passwords.PASSWORD_FIELD, - Passwords.ENCRYPTED_USERNAME, - Passwords.ENCRYPTED_PASSWORD, - Passwords.ENC_TYPE, - Passwords.TIME_CREATED, - Passwords.TIME_LAST_USED, - Passwords.TIME_PASSWORD_CHANGED, - Passwords.TIMES_USED, - Passwords.GUID - }; - - public static final String[] HistoryColumns = new String[] { - CommonColumns._ID, - SyncColumns.GUID, - SyncColumns.DATE_CREATED, - SyncColumns.DATE_MODIFIED, - SyncColumns.IS_DELETED, - History.TITLE, - History.URL, - History.DATE_LAST_VISITED, - History.VISITS - }; - - public static final String[] BookmarkColumns = new String[] { - CommonColumns._ID, - SyncColumns.GUID, - SyncColumns.DATE_CREATED, - SyncColumns.DATE_MODIFIED, - SyncColumns.IS_DELETED, - Bookmarks.TITLE, - Bookmarks.URL, - Bookmarks.TYPE, - Bookmarks.PARENT, - Bookmarks.POSITION, - Bookmarks.TAGS, - Bookmarks.DESCRIPTION, - Bookmarks.KEYWORD - }; - - public static final String[] FormHistoryColumns = new String[] { - FormHistory.ID, - FormHistory.GUID, - FormHistory.FIELD_NAME, - FormHistory.VALUE, - FormHistory.TIMES_USED, - FormHistory.FIRST_USED, - FormHistory.LAST_USED - }; - - public static final String[] DeletedColumns = new String[] { - BrowserContract.DeletedColumns.ID, - BrowserContract.DeletedColumns.GUID, - BrowserContract.DeletedColumns.TIME_DELETED - }; - - // Mapping from Sync types to Fennec types. - public static final String[] BOOKMARK_TYPE_CODE_TO_STRING = { - // Observe omissions: "microsummary", "item". - "folder", "bookmark", "separator", "livemark", "query" - }; - private static final int MAX_BOOKMARK_TYPE_CODE = BOOKMARK_TYPE_CODE_TO_STRING.length - 1; - public static final Map<String, Integer> BOOKMARK_TYPE_STRING_TO_CODE; - static { - HashMap<String, Integer> t = new HashMap<String, Integer>(); - t.put("folder", Bookmarks.TYPE_FOLDER); - t.put("bookmark", Bookmarks.TYPE_BOOKMARK); - t.put("separator", Bookmarks.TYPE_SEPARATOR); - t.put("livemark", Bookmarks.TYPE_LIVEMARK); - t.put("query", Bookmarks.TYPE_QUERY); - BOOKMARK_TYPE_STRING_TO_CODE = Collections.unmodifiableMap(t); - } - - /** - * Convert a database bookmark type code into the Sync string equivalent. - * - * @param code one of the <code>Bookmarks.TYPE_*</code> enumerations. - * @return the string equivalent, or null if not found. - */ - public static String typeStringForCode(int code) { - if (0 <= code && code <= MAX_BOOKMARK_TYPE_CODE) { - return BOOKMARK_TYPE_CODE_TO_STRING[code]; - } - return null; - } - - /** - * Convert a Sync type string into a Fennec type code. - * - * @param type a type string, such as "livemark". - * @return the type code, or -1 if not found. - */ - public static int typeCodeForString(String type) { - Integer found = BOOKMARK_TYPE_STRING_TO_CODE.get(type); - if (found == null) { - return -1; - } - return found; - } - - public static boolean isSupportedType(String type) { - return BOOKMARK_TYPE_STRING_TO_CODE.containsKey(type); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java deleted file mode 100644 index 5c17f9b85..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDatabase.CursorFactory; -import android.database.sqlite.SQLiteOpenHelper; - -public abstract class CachedSQLiteOpenHelper extends SQLiteOpenHelper { - - public CachedSQLiteOpenHelper(Context context, String name, CursorFactory factory, - int version) { - super(context, name, factory, version); - } - - // Cache these so we don't have to track them across cursors. Call `close` - // when you're done. - private SQLiteDatabase readableDatabase; - private SQLiteDatabase writableDatabase; - - synchronized protected SQLiteDatabase getCachedReadableDatabase() { - if (readableDatabase == null) { - if (writableDatabase == null) { - readableDatabase = this.getReadableDatabase(); - return readableDatabase; - } else { - return writableDatabase; - } - } else { - return readableDatabase; - } - } - - synchronized protected SQLiteDatabase getCachedWritableDatabase() { - if (writableDatabase == null) { - writableDatabase = this.getWritableDatabase(); - } - return writableDatabase; - } - - @Override - synchronized public void close() { - if (readableDatabase != null) { - readableDatabase.close(); - readableDatabase = null; - } - if (writableDatabase != null) { - writableDatabase.close(); - writableDatabase = null; - } - super.close(); - } - - // Used for testing. - public boolean isClosed() { - return readableDatabase == null && - writableDatabase == null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java deleted file mode 100644 index 4962a20c6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java +++ /dev/null @@ -1,252 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -public class ClientsDatabase extends CachedSQLiteOpenHelper { - - public static final String LOG_TAG = "ClientsDatabase"; - - // Database Specifications. - protected static final String DB_NAME = "clients_database"; - protected static final int SCHEMA_VERSION = 3; - - // Clients Table. - public static final String TBL_CLIENTS = "clients"; - public static final String COL_ACCOUNT_GUID = "guid"; - public static final String COL_PROFILE = "profile"; - public static final String COL_NAME = "name"; - public static final String COL_TYPE = "device_type"; - - // Optional fields. - public static final String COL_FORMFACTOR = "formfactor"; - public static final String COL_OS = "os"; - public static final String COL_APPLICATION = "application"; - public static final String COL_APP_PACKAGE = "appPackage"; - public static final String COL_DEVICE = "device"; - - public static final String[] TBL_CLIENTS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_PROFILE, COL_NAME, COL_TYPE, - COL_FORMFACTOR, COL_OS, COL_APPLICATION, COL_APP_PACKAGE, COL_DEVICE }; - public static final String TBL_CLIENTS_KEY = COL_ACCOUNT_GUID + " = ? AND " + - COL_PROFILE + " = ?"; - - // Commands Table. - public static final String TBL_COMMANDS = "commands"; - public static final String COL_COMMAND = "command"; - public static final String COL_ARGS = "args"; - - public static final String[] TBL_COMMANDS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_COMMAND, COL_ARGS }; - public static final String TBL_COMMANDS_KEY = COL_ACCOUNT_GUID + " = ? AND " + - COL_COMMAND + " = ? AND " + - COL_ARGS + " = ?"; - public static final String TBL_COMMANDS_GUID_QUERY = COL_ACCOUNT_GUID + " = ? "; - - private final RepoUtils.QueryHelper queryHelper; - - public ClientsDatabase(Context context) { - super(context, DB_NAME, null, SCHEMA_VERSION); - this.queryHelper = new RepoUtils.QueryHelper(context, null, LOG_TAG); - Logger.debug(LOG_TAG, "ClientsDatabase instantiated."); - } - - @Override - public void onCreate(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.onCreate()."); - createClientsTable(db); - createCommandsTable(db); - } - - public static void createClientsTable(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.createClientsTable()."); - String createClientsTableSql = "CREATE TABLE " + TBL_CLIENTS + " (" - + COL_ACCOUNT_GUID + " TEXT, " - + COL_PROFILE + " TEXT, " - + COL_NAME + " TEXT, " - + COL_TYPE + " TEXT, " - + COL_FORMFACTOR + " TEXT, " - + COL_OS + " TEXT, " - + COL_APPLICATION + " TEXT, " - + COL_APP_PACKAGE + " TEXT, " - + COL_DEVICE + " TEXT, " - + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_PROFILE + "))"; - db.execSQL(createClientsTableSql); - } - - public static void createCommandsTable(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.createCommandsTable()."); - String createCommandsTableSql = "CREATE TABLE " + TBL_COMMANDS + " (" - + COL_ACCOUNT_GUID + " TEXT, " - + COL_COMMAND + " TEXT, " - + COL_ARGS + " TEXT, " - + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_COMMAND + ", " + COL_ARGS + "), " - + "FOREIGN KEY (" + COL_ACCOUNT_GUID + ") REFERENCES " + TBL_CLIENTS + " (" + COL_ACCOUNT_GUID + "))"; - db.execSQL(createCommandsTableSql); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Logger.debug(LOG_TAG, "ClientsDatabase.onUpgrade(" + oldVersion + ", " + newVersion + ")."); - if (oldVersion < 2) { - // For now we'll just drop and recreate the tables. - db.execSQL("DROP TABLE IF EXISTS " + TBL_CLIENTS); - db.execSQL("DROP TABLE IF EXISTS " + TBL_COMMANDS); - onCreate(db); - return; - } - - if (newVersion >= 3) { - // Add the optional columns to clients. - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_FORMFACTOR + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_OS + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APPLICATION + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APP_PACKAGE + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_DEVICE + " TEXT"); - } - } - - public void wipeDB() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - onUpgrade(db, 0, SCHEMA_VERSION); - } - - public void wipeClientsTable() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.execSQL("DELETE FROM " + TBL_CLIENTS); - } - - public void wipeCommandsTable() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.execSQL("DELETE FROM " + TBL_COMMANDS); - } - - // If a record with given GUID exists, we'll update it, - // otherwise we'll insert it. - public void store(String profileId, ClientRecord record) { - SQLiteDatabase db = this.getCachedWritableDatabase(); - - ContentValues cv = new ContentValues(); - cv.put(COL_ACCOUNT_GUID, record.guid); - cv.put(COL_PROFILE, profileId); - cv.put(COL_NAME, record.name); - cv.put(COL_TYPE, record.type); - - if (record.formfactor != null) { - cv.put(COL_FORMFACTOR, record.formfactor); - } - - if (record.os != null) { - cv.put(COL_OS, record.os); - } - - if (record.application != null) { - cv.put(COL_APPLICATION, record.application); - } - - if (record.appPackage != null) { - cv.put(COL_APP_PACKAGE, record.appPackage); - } - - if (record.device != null) { - cv.put(COL_DEVICE, record.device); - } - - String[] args = new String[] { record.guid, profileId }; - int rowsUpdated = db.update(TBL_CLIENTS, cv, TBL_CLIENTS_KEY, args); - - if (rowsUpdated >= 1) { - Logger.debug(LOG_TAG, "Replaced client record for row with accountGUID " + record.guid); - } else { - long rowId = db.insert(TBL_CLIENTS, null, cv); - Logger.debug(LOG_TAG, "Inserted client record into row: " + rowId); - } - } - - /** - * Store a command in the commands database if it doesn't already exist. - * - * @param accountGUID - * @param command - The command type - * @param args - A JSON string of args - * @throws NullCursorException - */ - public void store(String accountGUID, String command, String args) throws NullCursorException { - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Storing command " + command + " with args " + args); - } else { - Logger.trace(LOG_TAG, "Storing command " + command + "."); - } - SQLiteDatabase db = this.getCachedWritableDatabase(); - - ContentValues cv = new ContentValues(); - cv.put(COL_ACCOUNT_GUID, accountGUID); - cv.put(COL_COMMAND, command); - if (args == null) { - cv.put(COL_ARGS, "[]"); - } else { - cv.put(COL_ARGS, args); - } - - Cursor cur = this.fetchSpecificCommand(accountGUID, command, args); - try { - if (cur.moveToFirst()) { - Logger.debug(LOG_TAG, "Command already exists in database."); - return; - } - } finally { - cur.close(); - } - - long rowId = db.insert(TBL_COMMANDS, null, cv); - Logger.debug(LOG_TAG, "Inserted command into row: " + rowId); - } - - public Cursor fetchClientsCursor(String accountGUID, String profileId) throws NullCursorException { - String[] args = new String[] { accountGUID, profileId }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchClientsCursor", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, TBL_CLIENTS_KEY, args); - } - - public Cursor fetchSpecificCommand(String accountGUID, String command, String commandArgs) throws NullCursorException { - String[] args = new String[] { accountGUID, command, commandArgs }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchSpecificCommand", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_KEY, args); - } - - public Cursor fetchCommandsForClient(String accountGUID) throws NullCursorException { - String[] args = new String[] { accountGUID }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchCommandsForClient", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_GUID_QUERY, args); - } - - public Cursor fetchAllClients() throws NullCursorException { - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchAllClients", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, null, null); - } - - public Cursor fetchAllCommands() throws NullCursorException { - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchAllCommands", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, null, null); - } - - public void deleteClient(String accountGUID, String profileId) { - String[] args = new String[] { accountGUID, profileId }; - - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.delete(TBL_CLIENTS, TBL_CLIENTS_KEY, args); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java deleted file mode 100644 index 4af84ceaf..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java +++ /dev/null @@ -1,178 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.simple.JSONArray; - -import org.mozilla.gecko.sync.CommandProcessor.Command; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.setup.Constants; - -import android.content.Context; -import android.database.Cursor; - -public class ClientsDatabaseAccessor { - - public static final String LOG_TAG = "ClientsDatabaseAccessor"; - - private ClientsDatabase db; - - // Need this so we can properly stub out the class for testing. - public ClientsDatabaseAccessor() {} - - public ClientsDatabaseAccessor(Context context) { - db = new ClientsDatabase(context); - } - - public void store(ClientRecord record) { - db.store(getProfileId(), record); - } - - public void store(Collection<ClientRecord> records) { - for (ClientRecord record : records) { - this.store(record); - } - } - - public void store(String accountGUID, Command command) throws NullCursorException { - db.store(accountGUID, command.commandType, command.args.toJSONString()); - } - - public ClientRecord fetchClient(String accountGUID) throws NullCursorException { - final Cursor cur = db.fetchClientsCursor(accountGUID, getProfileId()); - try { - if (!cur.moveToFirst()) { - return null; - } - return recordFromCursor(cur); - } finally { - cur.close(); - } - } - - public Map<String, ClientRecord> fetchAllClients() throws NullCursorException { - final HashMap<String, ClientRecord> map = new HashMap<String, ClientRecord>(); - final Cursor cur = db.fetchAllClients(); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableMap(map); - } - - while (!cur.isAfterLast()) { - ClientRecord clientRecord = recordFromCursor(cur); - map.put(clientRecord.guid, clientRecord); - cur.moveToNext(); - } - return Collections.unmodifiableMap(map); - } finally { - cur.close(); - } - } - - public List<Command> fetchAllCommands() throws NullCursorException { - final List<Command> commands = new ArrayList<Command>(); - final Cursor cur = db.fetchAllCommands(); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableList(commands); - } - - while (!cur.isAfterLast()) { - Command command = commandFromCursor(cur); - commands.add(command); - cur.moveToNext(); - } - return Collections.unmodifiableList(commands); - } finally { - cur.close(); - } - } - - public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException { - final List<Command> commands = new ArrayList<Command>(); - final Cursor cur = db.fetchCommandsForClient(accountGUID); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableList(commands); - } - - while(!cur.isAfterLast()) { - Command command = commandFromCursor(cur); - commands.add(command); - cur.moveToNext(); - } - return Collections.unmodifiableList(commands); - } finally { - cur.close(); - } - } - - protected static ClientRecord recordFromCursor(Cursor cur) { - final String accountGUID = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID); - final String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME); - final String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE); - - final ClientRecord record = new ClientRecord(accountGUID); - record.name = clientName; - record.type = clientType; - - // Optional fields. These will either be null or strings. - record.formfactor = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_FORMFACTOR); - record.os = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_OS); - record.device = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_DEVICE); - record.appPackage = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APP_PACKAGE); - record.application = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APPLICATION); - - return record; - } - - protected static Command commandFromCursor(Cursor cur) { - String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND); - JSONArray commandArgs = RepoUtils.getJSONArrayFromCursor(cur, ClientsDatabase.COL_ARGS); - return new Command(commandType, commandArgs); - } - - public int clientsCount() { - try { - final Cursor cur = db.fetchAllClients(); - try { - return cur.getCount(); - } finally { - cur.close(); - } - } catch (NullCursorException e) { - return 0; - } - - } - - private String getProfileId() { - return Constants.DEFAULT_PROFILE; - } - - public void wipeDB() { - db.wipeDB(); - } - - public void wipeClientsTable() { - db.wipeClientsTable(); - } - - public void wipeCommandsTable() { - db.wipeCommandsTable(); - } - - public void close() { - db.close(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java deleted file mode 100644 index 720d856eb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java +++ /dev/null @@ -1,383 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.Tab; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Clients; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoContentProviderException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; -import org.mozilla.gecko.sync.repositories.domain.TabsRecord; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class FennecTabsRepository extends Repository { - private static final String LOG_TAG = "FennecTabsRepository"; - - protected final ClientsDataDelegate clientsDataDelegate; - - public FennecTabsRepository(ClientsDataDelegate clientsDataDelegate) { - this.clientsDataDelegate = clientsDataDelegate; - } - - /** - * Note that -- unlike most repositories -- this will only fetch Fennec's tabs, - * and only store tabs from other clients. - * - * It will never retrieve tabs from other clients, or store tabs for Fennec, - * unless you use {@link #fetch(String[], RepositorySessionFetchRecordsDelegate)} - * and specify an explicit GUID. - */ - public class FennecTabsRepositorySession extends RepositorySession { - protected static final String LOG_TAG = "FennecTabsSession"; - - private final ContentProviderClient tabsProvider; - private final ContentProviderClient clientsProvider; - - protected final RepoUtils.QueryHelper tabsHelper; - - protected final ClientsDatabaseAccessor clientsDatabase; - - protected ContentProviderClient getContentProvider(final Context context, final Uri uri) throws NoContentProviderException { - ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); - if (client == null) { - throw new NoContentProviderException(uri); - } - return client; - } - - protected void releaseProviders() { - try { - clientsProvider.release(); - } catch (Exception e) {} - try { - tabsProvider.release(); - } catch (Exception e) {} - clientsDatabase.close(); - } - - public FennecTabsRepositorySession(Repository repository, Context context) throws NoContentProviderException { - super(repository); - clientsProvider = getContentProvider(context, BrowserContractHelpers.CLIENTS_CONTENT_URI); - try { - tabsProvider = getContentProvider(context, BrowserContractHelpers.TABS_CONTENT_URI); - } catch (NoContentProviderException e) { - clientsProvider.release(); - throw e; - } catch (Exception e) { - clientsProvider.release(); - // Oh, Java. - throw new RuntimeException(e); - } - - tabsHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.TABS_CONTENT_URI, LOG_TAG); - clientsDatabase = new ClientsDatabaseAccessor(context); - } - - @Override - public void abort() { - releaseProviders(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - releaseProviders(); - super.finish(delegate); - } - - // Default parameters for local data: local client has null GUID. Override - // these to test against non-live data. - protected String localClientSelection() { - return BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; - } - - protected String[] localClientSelectionArgs() { - return null; - } - - @Override - public void guidsSince(final long timestamp, - final RepositorySessionGuidsSinceDelegate delegate) { - // Bug 783692: Now that Bug 730039 has landed, we could implement this, - // but it's not a priority since it's not used (yet). - Logger.warn(LOG_TAG, "Not returning anything from guidsSince."); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onGuidsSinceSucceeded(new String[] {}); - } - }); - } - - @Override - public void fetchSince(final long timestamp, - final RepositorySessionFetchRecordsDelegate delegate) { - if (tabsProvider == null) { - throw new IllegalArgumentException("tabsProvider was null."); - } - if (tabsHelper == null) { - throw new IllegalArgumentException("tabsHelper was null."); - } - - final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; - - final String localClientSelection = localClientSelection(); - final String[] localClientSelectionArgs = localClientSelectionArgs(); - - final Runnable command = new Runnable() { - @Override - public void run() { - // We fetch all local tabs (since the record must contain them all) - // but only process the record if the timestamp is sufficiently - // recent, or if the client data has been modified. - try { - final Cursor cursor = tabsHelper.safeQuery(tabsProvider, ".fetchSince()", null, - localClientSelection, localClientSelectionArgs, positionAscending); - try { - final String localClientGuid = clientsDataDelegate.getAccountGUID(); - final String localClientName = clientsDataDelegate.getClientName(); - final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, localClientGuid, localClientName); - - if (tabsRecord.lastModified >= timestamp || - clientsDataDelegate.getLastModifiedTimestamp() >= timestamp) { - delegate.onFetchedRecord(tabsRecord); - } - } finally { - cursor.close(); - } - } catch (Exception e) { - delegate.onFetchFailed(e, null); - return; - } - delegate.onFetchCompleted(now()); - } - }; - - delegateQueue.execute(command); - } - - @Override - public void fetch(final String[] guids, - final RepositorySessionFetchRecordsDelegate delegate) { - // Bug 783692: Now that Bug 730039 has landed, we could implement this, - // but it's not a priority since it's not used (yet). - Logger.warn(LOG_TAG, "Not returning anything from fetch"); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onFetchCompleted(now()); - } - }); - } - - @Override - public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) { - fetchSince(0, delegate); - } - - private static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?"; - private static final String CLIENT_GUID_IS = BrowserContract.Clients.GUID + " = ?"; - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - Logger.warn(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to FennecTabsRepositorySession.store()."); - } - if (!(record instanceof TabsRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a TabsRecord"); - throw new IllegalArgumentException("Non-TabsRecord passed to FennecTabsRepositorySession.store()."); - } - final TabsRecord tabsRecord = (TabsRecord) record; - - Runnable command = new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Storing tabs for client " + tabsRecord.guid); - if (!isActive()) { - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - if (tabsRecord.guid == null) { - delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid); - return; - } - - try { - // This is nice and easy: we *always* store. - final String[] selectionArgs = new String[] { tabsRecord.guid }; - if (tabsRecord.deleted) { - try { - Logger.debug(LOG_TAG, "Clearing entry for client " + tabsRecord.guid); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, - CLIENT_GUID_IS, - selectionArgs); - delegate.onRecordStoreSucceeded(record.guid); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - } - return; - } - - // If it exists, update the client record; otherwise insert. - final ContentValues clientsCV = tabsRecord.getClientsContentValues(); - - final ClientRecord clientRecord = clientsDatabase.fetchClient(tabsRecord.guid); - if (null != clientRecord) { - // Null is an acceptable device type. - clientsCV.put(Clients.DEVICE_TYPE, clientRecord.type); - } - - Logger.debug(LOG_TAG, "Updating clients provider."); - final int updated = clientsProvider.update(BrowserContractHelpers.CLIENTS_CONTENT_URI, - clientsCV, - CLIENT_GUID_IS, - selectionArgs); - if (0 == updated) { - clientsProvider.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, clientsCV); - } - - // Now insert tabs. - final ContentValues[] tabsArray = tabsRecord.getTabsContentValues(); - Logger.debug(LOG_TAG, "Inserting " + tabsArray.length + " tabs for client " + tabsRecord.guid); - - tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, selectionArgs); - final int inserted = tabsProvider.bulkInsert(BrowserContractHelpers.TABS_CONTENT_URI, tabsArray); - Logger.trace(LOG_TAG, "Inserted: " + inserted); - - delegate.onRecordStoreSucceeded(record.guid); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error storing tabs.", e); - delegate.onRecordStoreFailed(e, record.guid); - } - } - }; - - storeWorkQueue.execute(command); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - try { - tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, null, null); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null); - } catch (RemoteException e) { - Logger.warn(LOG_TAG, "Got RemoteException in wipe.", e); - delegate.onWipeFailed(e); - return; - } - delegate.onWipeSucceeded(); - } - } - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - try { - final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context); - delegate.onSessionCreated(session); - } catch (Exception e) { - delegate.onSessionCreateFailed(e); - } - } - - /** - * Extract a <code>TabsRecord</code> from a cursor. - * <p> - * Caller is responsible for creating and closing cursor. Each row of the - * cursor should be an individual tab record. - * <p> - * The extracted tabs record has the given client GUID and client name. - * - * @param cursor - * to inspect. - * @param clientGuid - * returned tabs record will have this client GUID. - * @param clientName - * returned tabs record will have this client name. - * @return <code>TabsRecord</code> instance. - */ - public static TabsRecord tabsRecordFromCursor(final Cursor cursor, final String clientGuid, final String clientName) { - final String collection = "tabs"; - final TabsRecord record = new TabsRecord(clientGuid, collection, 0, false); - record.tabs = new ArrayList<Tab>(); - record.clientName = clientName; - - record.androidID = -1; - record.deleted = false; - - record.lastModified = 0; - - int position = cursor.getPosition(); - try { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - final Tab tab = Tab.fromCursor(cursor); - record.tabs.add(tab); - - if (tab.lastUsed > record.lastModified) { - record.lastModified = tab.lastUsed; - } - - cursor.moveToNext(); - } - } finally { - cursor.moveToPosition(position); - } - - return record; - } - - /** - * Deletes all non-local clients and their associated remote tabs. - */ - public static void deleteNonLocalClientsAndTabs(Context context) { - final String nonLocalClientSelection = BrowserContract.Clients.GUID + " IS NOT NULL"; - - ContentProviderClient clientsProvider = context.getContentResolver() - .acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); - if (clientsProvider == null) { - Logger.warn(LOG_TAG, "Unable to create clientsProvider!"); - return; - } - - try { - Logger.info(LOG_TAG, "Clearing all non-local clients and their associated remote tabs for default profile."); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, nonLocalClientSelection, null); - } catch (RemoteException e) { - Logger.warn(LOG_TAG, "Error while deleting", e); - } finally { - try { - clientsProvider.release(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception releasing clientsProvider!", e); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java deleted file mode 100644 index 9beafa712..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java +++ /dev/null @@ -1,723 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Callable; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory; -import org.mozilla.gecko.db.BrowserContract.FormHistory; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoContentProviderException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class FormHistoryRepositorySession extends - StoreTrackingRepositorySession { - public static final String LOG_TAG = "FormHistoryRepoSess"; - - /** - * Number of records to insert in one batch. - */ - public static final int INSERT_ITEM_THRESHOLD = 200; - - private static final Uri FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI; - private static final Uri DELETED_FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI; - - public static class FormHistoryRepository extends Repository { - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - try { - final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context); - delegate.onSessionCreated(session); - } catch (Exception e) { - delegate.onSessionCreateFailed(e); - } - } - } - - protected final ContentProviderClient formsProvider; - protected final RepoUtils.QueryHelper regularHelper; - protected final RepoUtils.QueryHelper deletedHelper; - - /** - * Acquire the content provider client. - * <p> - * The caller is responsible for releasing the client. - * - * @param context The application context. - * @return The <code>ContentProviderClient</code>. - * @throws NoContentProviderException - */ - public static ContentProviderClient acquireContentProvider(final Context context) - throws NoContentProviderException { - Uri uri = BrowserContract.FORM_HISTORY_AUTHORITY_URI; - ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); - if (client == null) { - throw new NoContentProviderException(uri); - } - return client; - } - - protected void releaseProviders() { - try { - if (formsProvider != null) { - formsProvider.release(); - } - } catch (Exception e) { - } - } - - // Only used for testing. - public ContentProviderClient getFormsProvider() { - return formsProvider; - } - - public FormHistoryRepositorySession(Repository repository, Context context) - throws NoContentProviderException { - super(repository); - formsProvider = acquireContentProvider(context); - regularHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI, LOG_TAG); - deletedHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI, LOG_TAG); - } - - @Override - public void abort() { - releaseProviders(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) - throws InactiveSessionException { - releaseProviders(); - super.finish(delegate); - } - - protected static final String[] GUID_COLUMNS = new String[] { FormHistory.GUID }; - - @Override - public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) { - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - ArrayList<String> guids = new ArrayList<String>(); - - final long sharedEnd = now(); - Cursor cur = null; - try { - cur = regularHelper.safeQuery(formsProvider, "", GUID_COLUMNS, regularBetween(timestamp, sharedEnd), null, null); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - guids.add(cur.getString(0)); - cur.moveToNext(); - } - } catch (RemoteException | NullCursorException e) { - delegate.onGuidsSinceFailed(e); - return; - } finally { - if (cur != null) { - cur.close(); - } - } - - try { - cur = deletedHelper.safeQuery(formsProvider, "", GUID_COLUMNS, deletedBetween(timestamp, sharedEnd), null, null); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - guids.add(cur.getString(0)); - cur.moveToNext(); - } - } catch (RemoteException | NullCursorException e) { - delegate.onGuidsSinceFailed(e); - return; - } finally { - if (cur != null) { - cur.close(); - } - } - - String guidsArray[] = guids.toArray(new String[guids.size()]); - delegate.onGuidsSinceSucceeded(guidsArray); - } - }; - delegateQueue.execute(command); - } - - protected static FormHistoryRecord retrieveDuringFetch(final Cursor cursor) { - // A simple and efficient way to distinguish two tables. - if (cursor.getColumnCount() == BrowserContractHelpers.FormHistoryColumns.length) { - return formHistoryRecordFromCursor(cursor); - } else { - return deletedFormHistoryRecordFromCursor(cursor); - } - } - - protected static FormHistoryRecord formHistoryRecordFromCursor(final Cursor cursor) { - String guid = RepoUtils.getStringFromCursor(cursor, FormHistory.GUID); - String collection = "forms"; - FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false); - - record.fieldName = RepoUtils.getStringFromCursor(cursor, FormHistory.FIELD_NAME); - record.fieldValue = RepoUtils.getStringFromCursor(cursor, FormHistory.VALUE); - record.androidID = RepoUtils.getLongFromCursor(cursor, FormHistory.ID); - record.lastModified = RepoUtils.getLongFromCursor(cursor, FormHistory.FIRST_USED) / 1000; // Convert microseconds to milliseconds. - record.deleted = false; - - record.log(LOG_TAG); - return record; - } - - protected static FormHistoryRecord deletedFormHistoryRecordFromCursor(final Cursor cursor) { - String guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID); - String collection = "forms"; - FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false); - - record.guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID); - record.androidID = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.ID); - record.lastModified = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.TIME_DELETED); - record.deleted = true; - - record.log(LOG_TAG); - return record; - } - - protected static void fetchFromCursor(final Cursor cursor, final RecordFilter filter, final RepositorySessionFetchRecordsDelegate delegate) - throws NullCursorException { - Logger.debug(LOG_TAG, "Fetch from cursor"); - if (cursor == null) { - throw new NullCursorException(null); - } - try { - if (!cursor.moveToFirst()) { - return; - } - while (!cursor.isAfterLast()) { - Record r = retrieveDuringFetch(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.trace(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(r); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - cursor.moveToNext(); - } - } finally { - Logger.trace(LOG_TAG, "Closing cursor after fetch."); - cursor.close(); - } - } - - protected void fetchHelper(final RepositorySessionFetchRecordsDelegate delegate, final long end, final List<Callable<Cursor>> cursorCallables) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - - final RecordFilter filter = this.storeTracker.getFilter(); - - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - for (Callable<Cursor> cursorCallable : cursorCallables) { - Cursor cursor = null; - try { - cursor = cursorCallable.call(); - fetchFromCursor(cursor, filter, delegate); // Closes cursor. - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception during fetchHelper", e); - delegate.onFetchFailed(e, null); - return; - } - } - - delegate.onFetchCompleted(end); - } - }; - - delegateQueue.execute(command); - } - - protected static String regularBetween(long start, long end) { - return FormHistory.FIRST_USED + " >= " + Long.toString(1000 * start) + " AND " + - FormHistory.FIRST_USED + " <= " + Long.toString(1000 * end); // Microseconds. - } - - protected static String deletedBetween(long start, long end) { - return DeletedFormHistory.TIME_DELETED + " >= " + Long.toString(start) + " AND " + - DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(end); // Milliseconds. - } - - @Override - public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetchSince(" + timestamp + ")."); - - /* - * We need to be careful about the timestamp we complete the fetch with. If - * the first cursor Callable takes a year, then the second could return - * records long after the first was kicked off. To protect against this, we - * set an end point and bound our search. - */ - final long sharedEnd = now(); - - Callable<Cursor> regularCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - return regularHelper.safeQuery(formsProvider, ".fetchSince(regular)", null, regularBetween(timestamp, sharedEnd), null, null); - } - }; - - Callable<Cursor> deletedCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - return deletedHelper.safeQuery(formsProvider, ".fetchSince(deleted)", null, deletedBetween(timestamp, sharedEnd), null, null); - } - }; - - @SuppressWarnings("unchecked") - List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable); - - fetchHelper(delegate, sharedEnd, callableCursors); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetchAll."); - fetchSince(0, delegate); - } - - @Override - public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetch."); - - final long sharedEnd = now(); - final String where = RepoUtils.computeSQLInClause(guids.length, FormHistory.GUID); - - Callable<Cursor> regularCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - String regularWhere = where + " AND " + FormHistory.FIRST_USED + " <= " + Long.toString(1000 * sharedEnd); // Microseconds. - return regularHelper.safeQuery(formsProvider, ".fetch(regular)", null, regularWhere, guids, null); - } - }; - - Callable<Cursor> deletedCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - String deletedWhere = where + " AND " + DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(sharedEnd); // Milliseconds. - return deletedHelper.safeQuery(formsProvider, ".fetch(deleted)", null, deletedWhere, guids, null); - } - }; - - @SuppressWarnings("unchecked") - List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable); - - fetchHelper(delegate, sharedEnd, callableCursors); - } - - protected static final String GUID_IS = FormHistory.GUID + " = ?"; - - protected Record findExistingRecordByGuid(String guid) - throws RemoteException, NullCursorException { - Cursor cursor = null; - try { - cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(regular)", - null, GUID_IS, new String[] { guid }, null); - if (cursor.moveToFirst()) { - return formHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - try { - cursor = deletedHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(deleted)", - null, GUID_IS, new String[] { guid }, null); - if (cursor.moveToFirst()) { - return deletedFormHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return null; - } - - protected Record findExistingRecordByPayload(Record rawRecord) - throws RemoteException, NullCursorException { - if (!rawRecord.deleted) { - FormHistoryRecord record = (FormHistoryRecord) rawRecord; - Cursor cursor = null; - try { - String where = FormHistory.FIELD_NAME + " = ? AND " + FormHistory.VALUE + " = ?"; - cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByPayload", - null, where, new String[] { record.fieldName, record.fieldValue }, null); - if (cursor.moveToFirst()) { - return formHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - return null; - } - - /** - * Called when a record with locally known GUID has been reported deleted by - * the server. - * <p> - * We purge the record's GUID from the regular and deleted tables. - * - * @param existingRecord - * The local <code>Record</code> to replace. - * @throws RemoteException - */ - protected void deleteExistingRecord(Record existingRecord) throws RemoteException { - if (existingRecord.deleted) { - formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid }); - return; - } - formsProvider.delete(FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid }); - } - - protected static ContentValues contentValuesForRegularRecord(Record rawRecord) { - if (rawRecord.deleted) { - throw new IllegalArgumentException("Deleted record passed to insertNewRegularRecord."); - } - - FormHistoryRecord record = (FormHistoryRecord) rawRecord; - ContentValues cv = new ContentValues(); - cv.put(FormHistory.GUID, record.guid); - cv.put(FormHistory.FIELD_NAME, record.fieldName); - cv.put(FormHistory.VALUE, record.fieldValue); - cv.put(FormHistory.FIRST_USED, 1000 * record.lastModified); // Microseconds. - return cv; - } - - protected final Object recordsBufferMonitor = new Object(); - protected ArrayList<ContentValues> recordsBuffer = new ArrayList<ContentValues>(); - - protected void enqueueRegularRecord(Record record) { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() >= INSERT_ITEM_THRESHOLD) { - // Insert the existing contents, then enqueue. - try { - flushInsertQueue(); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - // Store the ContentValues, rather than the record. - recordsBuffer.add(contentValuesForRegularRecord(record)); - } - } - - // Should always be called from storeWorkQueue. - protected void flushInsertQueue() throws RemoteException { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() > 0) { - final ContentValues[] outgoing = recordsBuffer.toArray(new ContentValues[recordsBuffer.size()]); - recordsBuffer = new ArrayList<ContentValues>(); - - if (outgoing == null || outgoing.length == 0) { - Logger.debug(LOG_TAG, "No form history items to insert; returning immediately."); - return; - } - - long before = System.currentTimeMillis(); - formsProvider.bulkInsert(FORM_HISTORY_CONTENT_URI, outgoing); - long after = System.currentTimeMillis(); - Logger.debug(LOG_TAG, "Inserted " + outgoing.length + " form history items in (" + (after - before) + " milliseconds)."); - } - } - } - - @Override - public void storeDone() { - Runnable command = new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Checking for residual form history items to insert."); - try { - synchronized (recordsBufferMonitor) { - flushInsertQueue(); - } - storeDone(now()); - } catch (Exception e) { - // XXX TODO - delegate.onRecordStoreFailed(e, null); - } - } - }; - storeWorkQueue.execute(command); - } - - /** - * Called when a regular record with locally unknown GUID has been fetched - * from the server. - * <p> - * Since the record is regular, we insert it into the regular table. - * - * @param record The regular <code>Record</code> from the server. - * @throws RemoteException - */ - protected void insertNewRegularRecord(Record record) - throws RemoteException { - enqueueRegularRecord(record); - } - - /** - * Called when a regular record with has been fetched from the server and - * should replace an existing record. - * <p> - * We delete the existing record entirely, and then insert the new record into - * the regular table. - * - * @param toStore - * The regular <code>Record</code> from the server. - * @param existingRecord - * The local <code>Record</code> to replace. - * @throws RemoteException - */ - protected void replaceExistingRecordWithRegularRecord(Record toStore, Record existingRecord) - throws RemoteException { - if (existingRecord.deleted) { - // Need two database operations -- purge from deleted table, insert into regular table. - deleteExistingRecord(existingRecord); - insertNewRegularRecord(toStore); - return; - } - - final ContentValues cv = contentValuesForRegularRecord(toStore); - int updated = formsProvider.update(FORM_HISTORY_CONTENT_URI, cv, GUID_IS, new String[] { existingRecord.guid }); - if (updated != 1) { - Logger.warn(LOG_TAG, "Expected to update 1 record with guid " + existingRecord.guid + " but updated " + updated + " records."); - } - } - - @Override - public void store(Record rawRecord) throws NoStoreDelegateException { - if (delegate == null) { - Logger.warn(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (rawRecord == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to FormHistoryRepositorySession.store()."); - } - if (!(rawRecord instanceof FormHistoryRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a FormHistoryRecord"); - throw new IllegalArgumentException("Non-FormHistoryRecord passed to FormHistoryRepositorySession.store()."); - } - final FormHistoryRecord record = (FormHistoryRecord) rawRecord; - - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "FormHistoryRepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - // TODO: lift these into the session. - // Temporary: this matches prior syncing semantics, in which only - // the relationship between the local and remote record is considered. - // In the future we'll track these two timestamps and use them to - // determine which records have changed, and thus process incoming - // records more efficiently. - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = record.lastModified > lastRemoteRetrieval; - - Record existingRecord; - try { - // GUID matching only: deleted records don't have a payload with which to search. - existingRecord = findExistingRecordByGuid(record.guid); - if (record.deleted) { - if (existingRecord == null) { - // We're done. Don't bother with a callback. That can change later - // if we want it to. - Logger.trace(LOG_TAG, "Incoming record " + record.guid + " is deleted, and no local version. Bye!"); - return; - } - - if (existingRecord.deleted) { - Logger.trace(LOG_TAG, "Local record already deleted. Purging local."); - deleteExistingRecord(existingRecord); - return; - } - - // Which one wins? - if (!remotelyModified) { - Logger.trace(LOG_TAG, "Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - Logger.trace(LOG_TAG, "Remote modified, local not. Deleting."); - deleteExistingRecord(existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - Logger.trace(LOG_TAG, "Remote is newer, and deleted. Purging local."); - deleteExistingRecord(existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!"); - // Ensure that this is tracked for upload. - } - return; - } - // End deletion logic. - - // Now we're processing a non-deleted incoming record. - if (existingRecord == null) { - Logger.trace(LOG_TAG, "Looking up match for record " + record.guid); - existingRecord = findExistingRecordByPayload(record); - } - - if (existingRecord == null) { - // The record is new. - Logger.trace(LOG_TAG, "No match. Inserting."); - insertNewRegularRecord(record); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - // We found a local duplicate. - Logger.trace(LOG_TAG, "Incoming record " + record.guid + " dupes to local record " + existingRecord.guid); - - if (!RepoUtils.stringsEqual(record.guid, existingRecord.guid)) { - // We found a local record that does NOT have the same GUID -- keep the server's version. - Logger.trace(LOG_TAG, "Remote guid different from local guid. Storing to keep remote guid."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - // We found a local record that does have the same GUID -- check modification times. - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - Logger.trace(LOG_TAG, "Remote modified, local not. Storing."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - Logger.trace(LOG_TAG, "Remote is newer, and not deleted. Storing."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is not deleted, but local record not modified!"); - } - return; - } catch (Exception e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - }; - - storeWorkQueue.execute(command); - } - - /** - * Purge all data from the underlying databases. - */ - public static void purgeDatabases(ContentProviderClient formsProvider) - throws RemoteException { - formsProvider.delete(FORM_HISTORY_CONTENT_URI, null, null); - formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, null, null); - } - - @Override - public void wipe(final RepositorySessionWipeDelegate delegate) { - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - - try { - Logger.debug(LOG_TAG, "Wiping form history and deleted form history..."); - purgeDatabases(formsProvider); - Logger.debug(LOG_TAG, "Wiping form history and deleted form history... DONE"); - } catch (Exception e) { - delegate.onWipeFailed(e); - return; - } - - delegate.onWipeSucceeded(); - } - }; - storeWorkQueue.execute(command); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java deleted file mode 100644 index f7b7416df..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java +++ /dev/null @@ -1,725 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.DeletedColumns; -import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; -import org.mozilla.gecko.db.BrowserContract.Passwords; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class PasswordsRepositorySession extends - StoreTrackingRepositorySession { - - public static class PasswordsRepository extends Repository { - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context); - final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate(); - deferredCreationDelegate.onSessionCreated(session); - } - } - - private static final String LOG_TAG = "PasswordsRepoSession"; - private static final String COLLECTION = "passwords"; - - private final RepoUtils.QueryHelper passwordsHelper; - private final RepoUtils.QueryHelper deletedPasswordsHelper; - private final ContentProviderClient passwordsProvider; - - private final Context context; - - public PasswordsRepositorySession(Repository repository, Context context) { - super(repository); - this.context = context; - this.passwordsHelper = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG); - this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG); - this.passwordsProvider = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI); - } - - private static final String[] GUID_COLS = new String[] { Passwords.GUID }; - private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID }; - - private static final String WHERE_GUID_IS = Passwords.GUID + " = ?"; - private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?"; - - @Override - public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) { - final Runnable guidsSinceRunnable = new Runnable() { - @Override - public void run() { - - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - // Checks succeeded, now get GUIDs. - final List<String> guids = new ArrayList<String>(); - try { - Logger.debug(LOG_TAG, "Fetching guidsSince from data table."); - final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null); - try { - if (data.moveToFirst()) { - while (!data.isAfterLast()) { - guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID)); - data.moveToNext(); - } - } - } finally { - data.close(); - } - - // Fetch guids from deleted table. - Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table."); - final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null); - try { - if (deleted.moveToFirst()) { - while (!deleted.isAfterLast()) { - guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID)); - deleted.moveToNext(); - } - } - } finally { - deleted.close(); - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onGuidsSinceFailed(e); - return; - } - String[] guidStrings = new String[guids.size()]; - delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings)); - } - }; - - delegateQueue.execute(guidsSinceRunnable); - } - - @Override - public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) { - final RecordFilter filter = this.storeTracker.getFilter(); - final Runnable fetchSinceRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - final long end = now(); - try { - // Fetch from data table. - Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince", - getAllColumns(), - dateModifiedWhere(timestamp), - null, null); - if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) { - return; - } - - // Fetch from deleted table. - Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince", - getAllDeletedColumns(), - dateModifiedWhereDeleted(timestamp), - null, null); - if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) { - return; - } - - // Success! - try { - delegate.onFetchCompleted(end); - } catch (Exception e) { - Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e); - // Don't call failure callback. - return; - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - } - } - }; - - delegateQueue.execute(fetchSinceRunnable); - } - - @Override - public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) { - if (guids == null || guids.length < 1) { - Logger.error(LOG_TAG, "No guids to be fetched."); - final long end = now(); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onFetchCompleted(end); - } - }); - return; - } - - // Checks succeeded, now fetch. - final RecordFilter filter = this.storeTracker.getFilter(); - final Runnable fetchRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - final long end = now(); - final String where = RepoUtils.computeSQLInClause(guids.length, "guid"); - Logger.trace(LOG_TAG, "Fetch guids where: " + where); - - try { - // Fetch records from data table. - Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch", - getAllColumns(), - where, guids, null); - if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) { - return; - } - - // Fetch records from deleted table. - Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch", - getAllDeletedColumns(), - where, guids, null); - if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) { - return; - } - - delegate.onFetchCompleted(end); - - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - } - } - }; - - delegateQueue.execute(fetchRunnable); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - fetchSince(0, delegate); - } - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - Logger.error(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null."); - throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store()."); - } - if (!(record instanceof PasswordRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord."); - throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store()."); - } - - final PasswordRecord remoteRecord = (PasswordRecord) record; - - final Runnable storeRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - final String guid = remoteRecord.guid; - if (guid == null) { - delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid); - return; - } - - PasswordRecord existingRecord; - try { - existingRecord = retrieveByGUID(guid); - } catch (NullCursorException | RemoteException e) { - // Indicates a serious problem. - delegate.onRecordStoreFailed(e, record.guid); - return; - } - - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval; - - // Check deleted state first. - if (remoteRecord.deleted) { - if (existingRecord == null) { - // Do nothing, record does not exist anyways. - Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version."); - return; - } - - if (existingRecord.deleted) { - // Record is already tracked as deleted. Delete from local. - storeRecordDeletion(existingRecord); // different from ABRepoSess. - Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted."); - return; - } - - // Which one wins? - if (!remotelyModified) { - trace("Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - trace("Remote modified, local not. Deleting."); - storeRecordDeletion(remoteRecord); - return; - } - - trace("Both local and remote records have been modified."); - if (remoteRecord.lastModified > existingRecord.lastModified) { - trace("Remote is newer, and deleted. Deleting local."); - storeRecordDeletion(remoteRecord); - return; - } - - trace("Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!"); - // Ensure that this is tracked for upload. - } - return; - } - // End deletion logic. - - // Validate the incoming record. - if (!remoteRecord.isValid()) { - Logger.warn(LOG_TAG, "Incoming record is invalid. Reporting store failed."); - delegate.onRecordStoreFailed(new RuntimeException("Can't store invalid password record."), record.guid); - return; - } - - // Now we're processing a non-deleted incoming record. - if (existingRecord == null) { - trace("Looking up match for record " + remoteRecord.guid); - try { - existingRecord = findExistingRecord(remoteRecord); - } catch (RemoteException e) { - Logger.error(LOG_TAG, "Remote exception in findExistingRecord."); - delegate.onRecordStoreFailed(e, record.guid); - } catch (NullCursorException e) { - Logger.error(LOG_TAG, "Null cursor in findExistingRecord."); - delegate.onRecordStoreFailed(e, record.guid); - } - } - - if (existingRecord == null) { - // The record is new. - trace("No match. Inserting."); - Logger.debug(LOG_TAG, "Didn't find matching record. Inserting."); - Record inserted = null; - try { - inserted = insert(remoteRecord); - } catch (RemoteException e) { - Logger.debug(LOG_TAG, "Record insert caused a RemoteException."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - trackRecord(inserted); - delegate.onRecordStoreSucceeded(inserted.guid); - return; - } - - // We found a local dupe. - trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid); - Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid); - - if (existingRecord.deleted && existingRecord.lastModified > remoteRecord.lastModified) { - Logger.debug(LOG_TAG, "Local deletion is newer, not storing remote record."); - return; - } - - Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval); - if (toStore == null) { - Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record."); - return; - } - - // TODO: pass in timestamps? - Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid); - Record replaced = null; - try { - replaced = replace(existingRecord, toStore); - } catch (RemoteException e) { - Logger.debug(LOG_TAG, "Record replace caused a RemoteException."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - - // Note that we don't track records here; deciding that is the job - // of reconcileRecords. - Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid + - "(" + replaced.androidID + ")"); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - }; - storeWorkQueue.execute(storeRunnable); - } - - @Override - public void wipe(final RepositorySessionWipeDelegate delegate) { - Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI); - - Runnable wipeRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - - // Wipe both data and deleted. - try { - context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null); - context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null); - } catch (Exception e) { - delegate.onWipeFailed(e); - return; - } - delegate.onWipeSucceeded(); - } - }; - storeWorkQueue.execute(wipeRunnable); - } - - @Override - public void abort() { - passwordsProvider.release(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - passwordsProvider.release(); - super.finish(delegate); - } - - public void deleteGUID(String guid) throws RemoteException { - final String[] args = new String[] { guid }; - - int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) + - passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args); - if (deleted == 1) { - return; - } - Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid); - } - - /** - * Insert record and return the record with its updated androidId set. - * - * @param record the record to insert. - * @return updated record. - * @throws RemoteException - */ - public PasswordRecord insert(PasswordRecord record) throws RemoteException { - record.timePasswordChanged = now(); - // TODO: are these necessary for Fennec autocomplete? - // record.timesUsed = 1; - // record.timeLastUsed = now(); - ContentValues cv = getContentValues(record); - Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv); - if (insertedUri == null) { - throw new RemoteException(); // Not much to be done here, save throw. - } - record.androidID = ContentUris.parseId(insertedUri); - return record; - } - - public Record replace(Record origRecord, Record newRecord) throws RemoteException { - PasswordRecord newPasswordRecord = (PasswordRecord) newRecord; - PasswordRecord origPasswordRecord = (PasswordRecord) origRecord; - propagateTimes(newPasswordRecord, origPasswordRecord); - ContentValues cv = getContentValues(newPasswordRecord); - - final String[] args = new String[] { origRecord.guid }; - - if (origRecord.deleted) { - // Purge from deleted table. - deleteGUID(origRecord.guid); - insert(newPasswordRecord); - } else { - int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args); - if (updated != 1) { - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid); - } - } - - return newRecord; - } - - // When replacing a record, propagate the times. - private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) { - toRecord.timePasswordChanged = now(); - toRecord.timeCreated = fromRecord.timeCreated; - toRecord.timeLastUsed = fromRecord.timeLastUsed; - toRecord.timesUsed = fromRecord.timesUsed; - } - - private static String[] getAllColumns() { - return BrowserContractHelpers.PasswordColumns; - } - - private static String[] getAllDeletedColumns() { - return BrowserContractHelpers.DeletedColumns; - } - - /** - * Constructs the DB query string for entry age for deleted records. - * - * @param timestamp - * @return String DB query string for dates to fetch. - */ - private static String dateModifiedWhereDeleted(long timestamp) { - return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp); - } - - /** - * Constructs the DB query string for entry age for (undeleted) records. - * - * @param timestamp - * @return String DB query string for dates to fetch. - */ - private static String dateModifiedWhere(long timestamp) { - return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp); - } - - - /** - * Fetch from the cursor with the given parameters, invoking - * delegate callbacks and closing the cursor. - * Returns true on success, false if failure was signaled. - * - * @param cursor - fetch* cursor. - * @param deleted - * true if using deleted table, false when using data table. - * @param delegate - * FetchRecordsDelegate to process records. - */ - private static boolean fetchAndCloseCursorDeleted(final Cursor cursor, - final boolean deleted, - final RecordFilter filter, - final RepositorySessionFetchRecordsDelegate delegate) { - if (cursor == null) { - return true; - } - - try { - while (cursor.moveToNext()) { - Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.debug(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(r); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - return false; - } finally { - cursor.close(); - } - - return true; - } - - private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException { - final String[] guidArg = new String[] { guid }; - - // Check data table. - final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null); - try { - if (data.moveToFirst()) { - return passwordRecordFromCursor(data); - } - } finally { - data.close(); - } - - // Check deleted table. - final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null); - try { - if (deleted.moveToFirst()) { - return deletedPasswordRecordFromCursor(deleted); - } - } finally { - deleted.close(); - } - - return null; - } - - private static final String WHERE_RECORD_DATA = - Passwords.HOSTNAME + " = ? AND " + - Passwords.HTTP_REALM + " = ? AND " + - Passwords.FORM_SUBMIT_URL + " = ? AND " + - Passwords.USERNAME_FIELD + " = ? AND " + - Passwords.PASSWORD_FIELD + " = ?"; - - private PasswordRecord findExistingRecord(PasswordRecord record) throws NullCursorException, RemoteException { - PasswordRecord foundRecord = null; - Cursor cursor = null; - // Only check the data table. - // We can't encrypt username directly for query, so run a more general query and then filter. - final String[] whereArgs = new String[] { - record.hostname, - record.httpRealm, - record.formSubmitURL, - record.usernameField, - record.passwordField - }; - - try { - cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null); - while (cursor.moveToNext()) { - foundRecord = passwordRecordFromCursor(cursor); - - // We don't directly query for username because the - // username/password values are encrypted in the db. - // We don't have the keys for encrypting our query, - // so we run a more general query and then filter - // the returned records for a matching username. - Logger.pii(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]"); - if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) { - Logger.trace(LOG_TAG, "Found matching record: " + foundRecord.guid); - return foundRecord; - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - Logger.debug(LOG_TAG, "No matching records, returning null."); - return null; - } - - private void storeRecordDeletion(Record record) { - try { - deleteGUID(record.guid); - } catch (RemoteException e) { - Logger.error(LOG_TAG, "RemoteException in password delete."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - delegate.onRecordStoreSucceeded(record.guid); - } - - /** - * Make a PasswordRecord from a Cursor. - * @param cur - * Cursor from query. - * @param deleted - * true if creating a deleted Record, false if otherwise. - * @return - * PasswordRecord populated from Cursor. - */ - private static PasswordRecord passwordRecordFromCursor(Cursor cur) { - if (cur.isAfterLast()) { - return null; - } - String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID); - long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED); - - PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false); - rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID); - rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME); - rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM); - rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL); - rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD); - rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD); - rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE); - - // TODO decryption of username/password here (Bug 711636) - rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME); - rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD); - - rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED); - rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED); - rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED); - rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED); - return rec; - } - - private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) { - if (cur.isAfterLast()) { - return null; - } - String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID); - long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED); - PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true); - rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID); - return rec; - } - - private static ContentValues getContentValues(Record record) { - PasswordRecord rec = (PasswordRecord) record; - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Passwords.GUID, rec.guid); - cv.put(BrowserContract.Passwords.HOSTNAME, rec.hostname); - cv.put(BrowserContract.Passwords.HTTP_REALM, rec.httpRealm); - cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL); - cv.put(BrowserContract.Passwords.USERNAME_FIELD, rec.usernameField); - cv.put(BrowserContract.Passwords.PASSWORD_FIELD, rec.passwordField); - - // TODO Do encryption of username/password here. Bug 711636 - cv.put(BrowserContract.Passwords.ENC_TYPE, rec.encType); - cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername); - cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword); - - cv.put(BrowserContract.Passwords.TIME_CREATED, rec.timeCreated); - cv.put(BrowserContract.Passwords.TIME_LAST_USED, rec.timeLastUsed); - cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged); - cv.put(BrowserContract.Passwords.TIMES_USED, rec.timesUsed); - return cv; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java deleted file mode 100644 index 9c29953f8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java +++ /dev/null @@ -1,290 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; - -import java.io.IOException; - -public class RepoUtils { - - private static final String LOG_TAG = "RepoUtils"; - - /** - * A helper class for monotonous SQL querying. Does timing and logging, - * offers a utility to throw on a null cursor. - * - * @author rnewman - * - */ - public static class QueryHelper { - private final Context context; - private final Uri uri; - private final String tag; - - public QueryHelper(Context context, Uri uri, String tag) { - this.context = context; - this.uri = uri; - this.tag = tag; - } - - // For ContentProvider queries. - public Cursor safeQuery(String label, String[] projection, - String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); - return checkAndLogCursor(label, queryStart, c); - } - - public Cursor safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { - return this.safeQuery(null, projection, selection, selectionArgs, sortOrder); - } - - // For ContentProviderClient queries. - public Cursor safeQuery(ContentProviderClient client, String label, String[] projection, - String selection, String[] selectionArgs, String sortOrder) throws NullCursorException, RemoteException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = client.query(uri, projection, selection, selectionArgs, sortOrder); - return checkAndLogCursor(label, queryStart, c); - } - - // For SQLiteOpenHelper queries. - public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, - String selection, String[] selectionArgs, - String groupBy, String having, String orderBy, String limit) throws NullCursorException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); - return checkAndLogCursor(label, queryStart, c); - } - - public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, - String selection, String[] selectionArgs) throws NullCursorException { - return safeQuery(db, label, table, columns, selection, selectionArgs, null, null, null, null); - } - - private Cursor checkAndLogCursor(String label, long queryStart, Cursor c) throws NullCursorException { - long queryEnd = android.os.SystemClock.uptimeMillis(); - String logLabel = (label == null) ? tag : (tag + label); - RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd); - return checkNullCursor(logLabel, c); - } - - public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException { - if (cursor == null) { - Logger.error(tag, "Got null cursor exception in " + logLabel); - throw new NullCursorException(null); - } - return cursor; - } - } - - /** - * This method exists because the behavior of <code>cur.getString()</code> is undefined - * when the value in the database is <code>NULL</code>. - * This method will return <code>null</code> in that case. - */ - public static String optStringFromCursor(final Cursor cur, final String colId) { - final int col = cur.getColumnIndex(colId); - if (cur.isNull(col)) { - return null; - } - return cur.getString(col); - } - - /** - * The behavior of this method when the value in the database is <code>NULL</code> is - * determined by the implementation of the {@link Cursor}. - */ - public static String getStringFromCursor(final Cursor cur, final String colId) { - // TODO: getColumnIndexOrThrow? - // TODO: don't look up columns by name! - return cur.getString(cur.getColumnIndex(colId)); - } - - public static long getLongFromCursor(Cursor cur, String colId) { - return cur.getLong(cur.getColumnIndex(colId)); - } - - public static int getIntFromCursor(Cursor cur, String colId) { - return cur.getInt(cur.getColumnIndex(colId)); - } - - public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) { - String jsonArrayAsString = getStringFromCursor(cur, colId); - if (jsonArrayAsString == null) { - return new JSONArray(); - } - try { - return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId)); - } catch (NonArrayJSONException e) { - Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); - return null; - } catch (IOException e) { - Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); - return null; - } - } - - /** - * Return true if the provided URI is non-empty and acceptable to Fennec - * (i.e., not an undesirable scheme). - * - * This code is pilfered from Fennec, which pilfered from Places. - */ - public static boolean isValidHistoryURI(String uri) { - if (uri == null || uri.length() == 0) { - return false; - } - - // First, check the most common cases (HTTP, HTTPS) to avoid most of the work. - if (uri.startsWith("http:") || uri.startsWith("https:")) { - return true; - } - - String scheme = Uri.parse(uri).getScheme(); - if (scheme == null) { - return false; - } - - // Now check for all bad things. - if (scheme.equals("about") || - scheme.equals("imap") || - scheme.equals("news") || - scheme.equals("mailbox") || - scheme.equals("moz-anno") || - scheme.equals("view-source") || - scheme.equals("chrome") || - scheme.equals("resource") || - scheme.equals("data") || - scheme.equals("wyciwyg") || - scheme.equals("javascript")) { - return false; - } - - return true; - } - - /** - * Create a HistoryRecord object from a cursor row. - * - * @return a HistoryRecord, or null if this row would produce - * an invalid record (e.g., with a null URI or no visits). - */ - public static HistoryRecord historyFromMirrorCursor(Cursor cur) { - final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - if (guid == null) { - Logger.debug(LOG_TAG, "Skipping history record with null GUID."); - return null; - } - - final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL); - if (!isValidHistoryURI(historyURI)) { - Logger.debug(LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI); - return null; - } - - final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS); - if (visitCount <= 0) { - Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count."); - return null; - } - - final String collection = "history"; - final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); - final boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; - - final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted); - - rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID); - rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED); - rec.fennecVisitCount = visitCount; - rec.histURI = historyURI; - rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE); - - return logHistory(rec); - } - - private static HistoryRecord logHistory(HistoryRecord rec) { - try { - Logger.debug(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")"); - Logger.debug(LOG_TAG, "> Visited: " + rec.fennecDateVisited); - Logger.debug(LOG_TAG, "> Visits: " + rec.fennecVisitCount); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "> Title: " + rec.title); - Logger.pii(LOG_TAG, "> URI: " + rec.histURI); - } - } catch (Exception e) { - Logger.debug(LOG_TAG, "Exception logging history record " + rec, e); - } - return rec; - } - - public static void logClient(ClientRecord rec) { - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")"); - Logger.trace(LOG_TAG, "Client Name: " + rec.name); - Logger.trace(LOG_TAG, "Client Type: " + rec.type); - Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified); - Logger.trace(LOG_TAG, "Deleted: " + rec.deleted); - } - } - - public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) { - long elapsedTime = queryEnd - queryStart; - Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms."); - } - - public static boolean stringsEqual(String a, String b) { - // Check for nulls - if (a == b) return true; - if (a == null && b != null) return false; - if (a != null && b == null) return false; - - return a.equals(b); - } - - public static String computeSQLLongInClause(long[] items, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - int i = 0; - for (; i < items.length - 1; ++i) { - builder.append(items[i]); - builder.append(", "); - } - if (i < items.length) { - builder.append(items[i]); - } - builder.append(")"); - return builder.toString(); - } - - public static String computeSQLInClause(int items, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - int i = 0; - for (; i < items - 1; ++i) { - builder.append("?, "); - } - if (i < items) { - builder.append("?"); - } - builder.append(")"); - return builder.toString(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java deleted file mode 100644 index 9ba784759..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java +++ /dev/null @@ -1,130 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.db.BrowserContract.Visits; - -/** - * This class is used by History Sync code (see <code>AndroidBrowserHistoryDataAccessor</code> and <code>AndroidBrowserHistoryRepositorySession</code>, - * and provides utility functions for working with history visits. Primarily we're either inserting visits - * into local database based on data received from Sync, or we're preparing local visits for upload into Sync. - */ -public class VisitsHelper { - public static final boolean DEFAULT_IS_LOCAL_VALUE = false; - public static final String SYNC_TYPE_KEY = "type"; - public static final String SYNC_DATE_KEY = "date"; - - /** - * Returns a list of ContentValues of visits ready for insertion for a provided History GUID. - * Visits must have data and type. See <code>getVisitContentValues</code>. - * - * @param guid History GUID to use when inserting visit records - * @param visits <code>JSONArray</code> list of (date, type) tuples for visits - * @return visits ready for insertion - */ - public static ContentValues[] getVisitsContentValues(@NonNull String guid, @NonNull JSONArray visits) { - final ContentValues[] visitsToStore = new ContentValues[visits.size()]; - final int visitCount = visits.size(); - - if (visitCount == 0) { - return visitsToStore; - } - - for (int i = 0; i < visitCount; i++) { - visitsToStore[i] = getVisitContentValues( - guid, (JSONObject) visits.get(i), DEFAULT_IS_LOCAL_VALUE); - } - return visitsToStore; - } - - /** - * Maps up to <code>limit</code> visits for a given history GUID to an array of JSONObjects with "date" and "type" keys - * - * @param contentClient <code>ContentProviderClient</code> to use for querying Visits table - * @param guid History GUID for which to return visits - * @param limit Will return at most this number of visits - * @return <code>JSONArray</code> of all visits found for given History GUID - */ - public static JSONArray getRecentHistoryVisitsForGUID(@NonNull ContentProviderClient contentClient, - @NonNull String guid, int limit) throws RemoteException { - final JSONArray visits = new JSONArray(); - - final Cursor cursor = contentClient.query( - visitsUriWithLimit(limit), - new String[] {Visits.VISIT_TYPE, Visits.DATE_VISITED}, - Visits.HISTORY_GUID + " = ?", - new String[] {guid}, null); - if (cursor == null) { - return visits; - } - try { - if (!cursor.moveToFirst()) { - return visits; - } - - final int dateVisitedCol = cursor.getColumnIndexOrThrow(Visits.DATE_VISITED); - final int visitTypeCol = cursor.getColumnIndexOrThrow(Visits.VISIT_TYPE); - - while (!cursor.isAfterLast()) { - insertTupleIntoVisitsUnchecked(visits, - cursor.getLong(visitTypeCol), - cursor.getLong(dateVisitedCol) - ); - cursor.moveToNext(); - } - } finally { - cursor.close(); - } - - return visits; - } - - /** - * Constructs <code>ContentValues</code> object for a visit based on passed in parameters. - * - * @param visit <code>JSONObject</code> containing visit type and visit date keys for the visit - * @param guid History GUID with with to associate this visit - * @param isLocal Whether or not to mark this visit as local - * @return <code>ContentValues</code> with all visit values necessary for database insertion - * @throws IllegalArgumentException if visit object is missing date or type keys - */ - public static ContentValues getVisitContentValues(@NonNull String guid, @NonNull JSONObject visit, boolean isLocal) { - if (!visit.containsKey(SYNC_DATE_KEY) || !visit.containsKey(SYNC_TYPE_KEY)) { - throw new IllegalArgumentException("Visit missing required keys"); - } - - final ContentValues cv = new ContentValues(); - cv.put(Visits.HISTORY_GUID, guid); - cv.put(Visits.IS_LOCAL, isLocal ? Visits.VISIT_IS_LOCAL : Visits.VISIT_IS_REMOTE); - cv.put(Visits.VISIT_TYPE, (Long) visit.get(SYNC_TYPE_KEY)); - cv.put(Visits.DATE_VISITED, (Long) visit.get(SYNC_DATE_KEY)); - - return cv; - } - - @SuppressWarnings("unchecked") - private static void insertTupleIntoVisitsUnchecked(@NonNull final JSONArray visits, @NonNull Long type, @NonNull Long date) { - final JSONObject visit = new JSONObject(); - visit.put(SYNC_TYPE_KEY, type); - visit.put(SYNC_DATE_KEY, date); - visits.add(visit); - } - - private static Uri visitsUriWithLimit(int limit) { - return BrowserContractHelpers.VISITS_CONTENT_URI - .buildUpon() - .appendQueryParameter("limit", Integer.toString(limit)) - .build(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java deleted file mode 100644 index f292600e4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public abstract class DeferrableRepositorySessionCreationDelegate implements RepositorySessionCreationDelegate { - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - final RepositorySessionCreationDelegate self = this; - return new RepositorySessionCreationDelegate() { - - // TODO: rewrite to use ExecutorService. - @Override - public void onSessionCreated(final RepositorySession session) { - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.onSessionCreated(session); - }}); - } - - @Override - public void onSessionCreateFailed(final Exception ex) { - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.onSessionCreateFailed(ex); - }}); - } - - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - return this; - } - }; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java deleted file mode 100644 index 1ccdcce19..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public class DeferredRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate { - private final RepositorySessionBeginDelegate inner; - private final ExecutorService executor; - public DeferredRepositorySessionBeginDelegate(final RepositorySessionBeginDelegate inner, final ExecutorService executor) { - this.inner = inner; - this.executor = executor; - } - - @Override - public void onBeginSucceeded(final RepositorySession session) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onBeginSucceeded(session); - } - }); - } - - @Override - public void onBeginFailed(final Exception ex) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onBeginFailed(ex); - } - }); - } - - @Override - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java deleted file mode 100644 index 1178d9b5b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class DeferredRepositorySessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate { - private final RepositorySessionFetchRecordsDelegate inner; - private final ExecutorService executor; - public DeferredRepositorySessionFetchRecordsDelegate(final RepositorySessionFetchRecordsDelegate inner, final ExecutorService executor) { - this.inner = inner; - this.executor = executor; - } - - @Override - public void onFetchedRecord(final Record record) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFetchedRecord(record); - } - }); - } - - @Override - public void onFetchFailed(final Exception ex, final Record record) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFetchFailed(ex, record); - } - }); - } - - @Override - public void onFetchCompleted(final long fetchEnd) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFetchCompleted(fetchEnd); - } - }); - } - - @Override - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java deleted file mode 100644 index dbe7e4327..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; - -public class DeferredRepositorySessionFinishDelegate implements - RepositorySessionFinishDelegate { - protected final ExecutorService executor; - protected final RepositorySessionFinishDelegate inner; - - public DeferredRepositorySessionFinishDelegate(RepositorySessionFinishDelegate inner, - ExecutorService executor) { - this.executor = executor; - this.inner = inner; - } - - @Override - public void onFinishSucceeded(final RepositorySession session, - final RepositorySessionBundle bundle) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFinishSucceeded(session, bundle); - } - }); - } - - @Override - public void onFinishFailed(final Exception ex) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFinishFailed(ex); - } - }); - } - - @Override - public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java deleted file mode 100644 index 2f659c733..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -public class DeferredRepositorySessionStoreDelegate implements - RepositorySessionStoreDelegate { - protected final RepositorySessionStoreDelegate inner; - protected final ExecutorService executor; - - public DeferredRepositorySessionStoreDelegate( - RepositorySessionStoreDelegate inner, ExecutorService executor) { - this.inner = inner; - this.executor = executor; - } - - @Override - public void onRecordStoreSucceeded(final String guid) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onRecordStoreSucceeded(guid); - } - }); - } - - @Override - public void onRecordStoreFailed(final Exception ex, final String guid) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onRecordStoreFailed(ex, guid); - } - }); - } - - @Override - public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } - - @Override - public void onStoreCompleted(final long storeEnd) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onStoreCompleted(storeEnd); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java deleted file mode 100644 index f5853647f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -/** - * One of these two methods is guaranteed to be called after session.begin() is - * invoked (possibly during the invocation). The callback will be invoked prior - * to any other RepositorySession callbacks. - * - * @author rnewman - * - */ -public interface RepositorySessionBeginDelegate { - public void onBeginFailed(Exception ex); - public void onBeginSucceeded(RepositorySession session); - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java deleted file mode 100644 index 139c561a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.repositories.Repository; - -public interface RepositorySessionCleanDelegate { - public void onCleaned(Repository repo); - public void onCleanFailed(Repository repo, Exception ex); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java deleted file mode 100644 index 6ad4991c3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -// Used to provide the sessionCallback and storeCallback -// mechanism to repository instances. -public interface RepositorySessionCreationDelegate { - public void onSessionCreateFailed(Exception ex); - public void onSessionCreated(RepositorySession session); - public RepositorySessionCreationDelegate deferredCreationDelegate(); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java deleted file mode 100644 index 589a093dc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java +++ /dev/null @@ -1,27 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public interface RepositorySessionFetchRecordsDelegate { - public void onFetchFailed(Exception ex, Record record); - public void onFetchedRecord(Record record); - - /** - * Called when all records in this fetch have been returned. - * - * @param fetchEnd - * A millisecond-resolution timestamp indicating the *remote* timestamp - * at the end of the range of records. Usually this is the timestamp at - * which the request was received. - * E.g., the (normalized) value of the X-Weave-Timestamp header. - */ - public void onFetchCompleted(final long fetchEnd); - - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java deleted file mode 100644 index 40296dd4f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; - -public interface RepositorySessionFinishDelegate { - public void onFinishFailed(Exception ex); - public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle); - public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java deleted file mode 100644 index 4f82768f1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -public interface RepositorySessionGuidsSinceDelegate { - public void onGuidsSinceFailed(Exception ex); - public void onGuidsSinceSucceeded(String[] guids); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java deleted file mode 100644 index 01e44c3ae..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -/** - * These methods *must* be invoked asynchronously. Use deferredStoreDelegate if you - * need help doing this. - * - * @author rnewman - * - */ -public interface RepositorySessionStoreDelegate { - public void onRecordStoreFailed(Exception ex, String recordGuid); - - // Called with a GUID when store has succeeded. - public void onRecordStoreSucceeded(String guid); - public void onStoreCompleted(long storeEnd); - public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java deleted file mode 100644 index cc8830729..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -public interface RepositorySessionWipeDelegate { - public void onWipeFailed(Exception ex); - public void onWipeSucceeded(); - public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java deleted file mode 100644 index 27b8e7151..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java +++ /dev/null @@ -1,488 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -/** - * Covers the fields used by all bookmark objects. - * @author rnewman - * - */ -public class BookmarkRecord extends Record { - public static final String PLACES_URI_PREFIX = "places:"; - - private static final String LOG_TAG = "BookmarkRecord"; - - public static final String COLLECTION_NAME = "bookmarks"; - public static final long BOOKMARKS_TTL = -1; // Never ttl bookmarks. - - public BookmarkRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = BOOKMARKS_TTL; - } - public BookmarkRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public BookmarkRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public BookmarkRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public BookmarkRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - // Note: redundant accessors are evil. We're all grownups; let's just use - // public fields. - public String title; - public String bookmarkURI; - public String description; - public String keyword; - public String parentID; - public String parentName; - public long androidParentID; - public String type; - public long androidPosition; - - public JSONArray children; - public JSONArray tags; - - @Override - public String toString() { - return "#<Bookmark " + guid + " (" + androidID + "), parent " + - parentID + "/" + androidParentID + "/" + parentName + ">"; - } - - // Oh God, this is terribly thread-unsafe. These record objects should be immutable. - @SuppressWarnings("unchecked") - protected JSONArray copyChildren() { - if (this.children == null) { - return null; - } - JSONArray children = new JSONArray(); - children.addAll(this.children); - return children; - } - - @SuppressWarnings("unchecked") - protected JSONArray copyTags() { - if (this.tags == null) { - return null; - } - JSONArray tags = new JSONArray(); - tags.addAll(this.tags); - return tags; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - BookmarkRecord out = new BookmarkRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy BookmarkRecord fields. - out.title = this.title; - out.bookmarkURI = this.bookmarkURI; - out.description = this.description; - out.keyword = this.keyword; - out.parentID = this.parentID; - out.parentName = this.parentName; - out.androidParentID = this.androidParentID; - out.type = this.type; - out.androidPosition = this.androidPosition; - - out.children = this.copyChildren(); - out.tags = this.copyTags(); - - return out; - } - - public boolean isBookmark() { - if (type == null) { - return false; - } - return type.equals("bookmark"); - } - - public boolean isFolder() { - if (type == null) { - return false; - } - return type.equals("folder"); - } - - public boolean isLivemark() { - if (type == null) { - return false; - } - return type.equals("livemark"); - } - - public boolean isSeparator() { - if (type == null) { - return false; - } - return type.equals("separator"); - } - - public boolean isMicrosummary() { - if (type == null) { - return false; - } - return type.equals("microsummary"); - } - - public boolean isQuery() { - if (type == null) { - return false; - } - return type.equals("query"); - } - - /** - * Return true if this record should have the Sync fields - * of a bookmark, microsummary, or query. - */ - private boolean isBookmarkIsh() { - if (type == null) { - return false; - } - return type.equals("bookmark") || - type.equals("microsummary") || - type.equals("query"); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.type = payload.getString("type"); - this.title = payload.getString("title"); - this.description = payload.getString("description"); - this.parentID = payload.getString("parentid"); - this.parentName = payload.getString("parentName"); - - if (isFolder()) { - try { - this.children = payload.getArray("children"); - } catch (NonArrayJSONException e) { - Logger.error(LOG_TAG, "Got non-array children in bookmark record " + this.guid, e); - // Let's see if we can recover later by using the parentid pointers. - this.children = new JSONArray(); - } - return; - } - - final String bmkUri = payload.getString("bmkUri"); - - // bookmark, microsummary, query. - if (isBookmarkIsh()) { - this.keyword = payload.getString("keyword"); - try { - this.tags = payload.getArray("tags"); - } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "Got non-array tags in bookmark record " + this.guid, e); - this.tags = new JSONArray(); - } - } - - if (isBookmark()) { - this.bookmarkURI = bmkUri; - return; - } - - if (isLivemark()) { - String siteUri = payload.getString("siteUri"); - String feedUri = payload.getString("feedUri"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "siteUri", siteUri, - "feedUri", feedUri); - return; - } - if (isQuery()) { - String queryId = payload.getString("queryId"); - String folderName = payload.getString("folderName"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "queryId", queryId, - "folderName", folderName); - return; - } - if (isMicrosummary()) { - String generatorUri = payload.getString("generatorUri"); - String staticTitle = payload.getString("staticTitle"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "generatorUri", generatorUri, - "staticTitle", staticTitle); - return; - } - if (isSeparator()) { - Object p = payload.get("pos"); - if (p instanceof Long) { - this.androidPosition = (Long) p; - } else if (p instanceof String) { - try { - this.androidPosition = Long.parseLong((String) p, 10); - } catch (NumberFormatException e) { - return; - } - } else { - Logger.warn(LOG_TAG, "Unsupported position value " + p); - return; - } - String pos = String.valueOf(this.androidPosition); - this.bookmarkURI = encodeUnsupportedTypeURI(null, "pos", pos, null, null); - return; - } - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "type", this.type); - putPayload(payload, "title", this.title); - putPayload(payload, "description", this.description); - putPayload(payload, "parentid", this.parentID); - putPayload(payload, "parentName", this.parentName); - putPayload(payload, "keyword", this.keyword); - - if (isFolder()) { - payload.put("children", this.children); - return; - } - - // bookmark, microsummary, query. - if (isBookmarkIsh()) { - if (isBookmark()) { - payload.put("bmkUri", bookmarkURI); - } - - if (isQuery()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "queryId", parts.get("queryId"), true); - putPayload(payload, "folderName", parts.get("folderName"), true); - putPayload(payload, "bmkUri", parts.get("uri")); - return; - } - - if (this.tags != null) { - payload.put("tags", this.tags); - } - - putPayload(payload, "keyword", this.keyword); - return; - } - - if (isLivemark()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "siteUri", parts.get("siteUri")); - putPayload(payload, "feedUri", parts.get("feedUri")); - return; - } - if (isMicrosummary()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "generatorUri", parts.get("generatorUri")); - putPayload(payload, "staticTitle", parts.get("staticTitle")); - return; - } - if (isSeparator()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - String pos = parts.get("pos"); - if (pos == null) { - return; - } - try { - payload.put("pos", Long.parseLong(pos, 10)); - } catch (NumberFormatException e) { - return; - } - return; - } - } - - private void trace(String s) { - Logger.trace(LOG_TAG, s); - } - - @Override - public boolean equalPayloads(Object o) { - trace("Calling BookmarkRecord.equalPayloads."); - if (!(o instanceof BookmarkRecord)) { - return false; - } - - BookmarkRecord other = (BookmarkRecord) o; - if (!super.equalPayloads(other)) { - return false; - } - - if (!RepoUtils.stringsEqual(this.type, other.type)) { - return false; - } - - // Check children. - if (isFolder() && (this.children != other.children)) { - trace("BookmarkRecord.equals: this folder: " + this.title + ", " + this.guid); - trace("BookmarkRecord.equals: other: " + other.title + ", " + other.guid); - if (this.children == null && - other.children != null) { - trace("Records differ: one children array is null."); - return false; - } - if (this.children != null && - other.children == null) { - trace("Records differ: one children array is null."); - return false; - } - if (this.children.size() != other.children.size()) { - trace("Records differ: children arrays differ in size (" + - this.children.size() + " vs. " + other.children.size() + ")."); - return false; - } - - for (int i = 0; i < this.children.size(); i++) { - String child = (String) this.children.get(i); - if (!other.children.contains(child)) { - trace("Records differ: child " + child + " not found."); - return false; - } - } - } - - trace("Checking strings."); - return RepoUtils.stringsEqual(this.title, other.title) - && RepoUtils.stringsEqual(this.bookmarkURI, other.bookmarkURI) - && RepoUtils.stringsEqual(this.parentID, other.parentID) - && RepoUtils.stringsEqual(this.parentName, other.parentName) - && RepoUtils.stringsEqual(this.description, other.description) - && RepoUtils.stringsEqual(this.keyword, other.keyword) - && jsonArrayStringsEqual(this.tags, other.tags); - } - - // TODO: two records can be congruent if their child lists are different. - @Override - public boolean congruentWith(Object o) { - return this.equalPayloads(o) && - super.congruentWith(o); - } - - // Converts two JSONArrays to strings and checks if they are the same. - // This is only useful for stuff like tags where we aren't actually - // touching the data there (and therefore ordering won't change) - private boolean jsonArrayStringsEqual(JSONArray a, JSONArray b) { - // Check for nulls - if (a == b) return true; - if (a == null && b != null) return false; - if (a != null && b == null) return false; - return RepoUtils.stringsEqual(a.toJSONString(), b.toJSONString()); - } - - /** - * URL-encode the provided string. If the input is null, - * the empty string is returned. - * - * @param in the string to encode. - * @return a URL-encoded version of the input. - */ - protected static String encode(String in) { - if (in == null) { - return ""; - } - try { - return URLEncoder.encode(in, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // Will never occur. - return null; - } - } - - /** - * Take the provided URI and two parameters, constructing a URI like - * - * places:uri=$uri&p1=$p1&p2=$p2 - * - * null values in either parameter or value result in the parameter being omitted. - */ - protected static String encodeUnsupportedTypeURI(String originalURI, String p1, String v1, String p2, String v2) { - StringBuilder b = new StringBuilder(PLACES_URI_PREFIX); - boolean previous = false; - if (originalURI != null) { - b.append("uri="); - b.append(encode(originalURI)); - previous = true; - } - if (p1 != null && v1 != null) { - if (previous) { - b.append("&"); - } - b.append(p1); - b.append("="); - b.append(encode(v1)); - previous = true; - } - if (p2 != null && v2 != null) { - if (previous) { - b.append("&"); - } - b.append(p2); - b.append("="); - b.append(encode(v2)); - previous = true; - } - return b.toString(); - } -} - - -/* -// Bookmark: -{cleartext: - {id: "l7p2xqOTMMXw", - type: "bookmark", - title: "Your Flight Status", - parentName: "mobile", - bmkUri: "http: //www.flightstats.com/go/Mobile/flightStatusByFlightProcess.do;jsessionid=13A6C8DCC9592AF141A43349040262CE.web3: 8009?utm_medium=cpc&utm_campaign=co-op&utm_source=airlineInformationAndStatus&id=212492593", - tags: [], - keyword: null, - description: null, - loadInSidebar: false, - parentid: "mobile"}, - data: {payload: {ciphertext: null}, - id: "l7p2xqOTMMXw", - sortindex: 107}, - collection: "bookmarks"} - -// Folder: -{cleartext: - {id: "mobile", - type: "folder", - parentName: "", - title: "mobile", - description: null, - children: ["1ROdlTuIoddD", "3Z_bMIHPSZQ8", "4mSDUuOo2iVB", "8aEdE9IIrJVr", - "9DzPTmkkZRDb", "Qwwb99HtVKsD", "s8tM36aGPKbq", "JMTi61hOO3JV", - "JQUDk0wSvYip", "LmVH-J1r3HLz", "NhgQlC5ykYGW", "OVanevUUaqO2", - "OtQVX0PMiWQj", "_GP5cF595iie", "fkRssjXSZDL3", "k7K_NwIA1Ya0", - "raox_QGzvqh1", "vXYL-xHjK06k", "QKHKUN6Dm-xv", "pmN2dYWT2MJ_", - "EVeO_J1SQiwL", "7N-qkepS7bec", "NIGa3ha-HVOE", "2Phv1I25wbuH", - "TTSIAH1fV0VE", "WOmZ8PfH39Da", "gDTXNg4m1AJZ", "ayI30OZslHbO", - "zSEs4O3n6CzQ", "oWTDR0gO2aWf", "wWHUoFaInXi9", "F7QTuVJDpsTM", - "FIboggegplk-", "G4HWrT5nfRYS", "MHA7y9bupDdv", "T_Ldzmj0Ttte", - "U9eYu3SxsE_U", "bk463Kl9IO_m", "brUfrqJjFNSR", "ccpawfWsD-bY", - "l7p2xqOTMMXw", "o-nSDKtXYln7"], - parentid: "places"}, - data: {payload: {ciphertext: null}, - id: "mobile", - sortindex: 1000000}, - collection: "bookmarks"} -*/ diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java deleted file mode 100644 index edf7b288c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -/** - * Turns CryptoRecords into BookmarkRecords. - * - * @author rnewman - * - */ -public class BookmarkRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - BookmarkRecord r = new BookmarkRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java deleted file mode 100644 index 0c513a4a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java +++ /dev/null @@ -1,231 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -public class ClientRecord extends Record { - private static final String LOG_TAG = "ClientRecord"; - - public static final String CLIENT_TYPE = "mobile"; - public static final String COLLECTION_NAME = "clients"; - public static final long CLIENTS_TTL = 21 * 24 * 60 * 60; // 21 days in seconds. - public static final String DEFAULT_CLIENT_NAME = "Default Name"; - - public static final String PROTOCOL_LEGACY_SYNC = "1.1"; - public static final String PROTOCOL_FXA_SYNC = "1.5"; - - /** - * Each of these fields is 'owned' by the client it represents. For example, - * the "version" field is the Firefox version of that client; some time after - * that client upgrades, it'll upload a new record with its new version. - * - * The only exception is for commands. When a command is sent to a client, the - * sender will download its current record, append the command to the - * "commands" array, and reupload the record. After processing, the recipient - * will reupload its record with an empty commands array. - * - * Note that the version, then, will remain the version of the recipient, as - * with the other descriptive fields. - */ - public String name = ClientRecord.DEFAULT_CLIENT_NAME; - public String type = ClientRecord.CLIENT_TYPE; - public String version = null; // Free-form string, optional. - public JSONArray commands; - public JSONArray protocols; - - // Optional fields. - // See <https://github.com/mozilla-services/docs/blob/master/source/sync/objectformats.rst#user-content-clients> - // for full formats. - // If a value isn't known, the field is omitted. - public String formfactor; // "phone", "largetablet", "smalltablet", "desktop", "laptop", "tv". - public String os; // One of "Android", "Darwin", "WINNT", "Linux", "iOS", "Firefox OS". - public String application; // Display name, E.g., "Firefox Beta" - public String appPackage; // E.g., "org.mozilla.firefox_beta" - public String device; // E.g., "HTC One" - public String fxaDeviceId; // E.g., "525b624eaaf1e40d21ec8997c3116ad8" - - public ClientRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = CLIENTS_TTL; - } - - public ClientRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - - public ClientRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - - public ClientRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - - public ClientRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.name = (String) payload.get("name"); - this.type = (String) payload.get("type"); - try { - this.version = (String) payload.get("version"); - } catch (Exception e) { - // Oh well. - } - - try { - commands = payload.getArray("commands"); - } catch (NonArrayJSONException e) { - Logger.debug(LOG_TAG, "Got non-array commands in client record " + guid, e); - commands = null; - } - - try { - protocols = payload.getArray("protocols"); - } catch (NonArrayJSONException e) { - Logger.debug(LOG_TAG, "Got non-array protocols in client record " + guid, e); - protocols = null; - } - - if (payload.containsKey("formfactor")) { - this.formfactor = payload.getString("formfactor"); - } - - if (payload.containsKey("os")) { - this.os = payload.getString("os"); - } - - if (payload.containsKey("application")) { - this.application = payload.getString("application"); - } - - if (payload.containsKey("appPackage")) { - this.appPackage = payload.getString("appPackage"); - } - - if (payload.containsKey("device")) { - this.device = payload.getString("device"); - } - - if (payload.containsKey("fxaDeviceId")) { - this.fxaDeviceId = payload.getString("fxaDeviceId"); - } - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "name", this.name); - putPayload(payload, "type", this.type); - putPayload(payload, "version", this.version); - - if (this.commands != null) { - payload.put("commands", this.commands); - } - - if (this.protocols != null) { - payload.put("protocols", this.protocols); - } - - if (this.formfactor != null) { - payload.put("formfactor", this.formfactor); - } - - if (this.os != null) { - payload.put("os", this.os); - } - - if (this.application != null) { - payload.put("application", this.application); - } - - if (this.appPackage != null) { - payload.put("appPackage", this.appPackage); - } - - if (this.device != null) { - payload.put("device", this.device); - } - - if (this.fxaDeviceId != null) { - payload.put("fxaDeviceId", this.fxaDeviceId); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ClientRecord) || !super.equals(o)) { - return false; - } - - return this.equalPayloads(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof ClientRecord) || !super.equalPayloads(o)) { - return false; - } - - // Don't compare versions, protocols, or other optional fields, no matter how much we might want to. - // They're not required by the spec. - ClientRecord other = (ClientRecord) o; - if (!RepoUtils.stringsEqual(other.name, this.name) || - !RepoUtils.stringsEqual(other.type, this.type)) { - return false; - } - return true; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - ClientRecord out = new ClientRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - out.name = this.name; - out.type = this.type; - out.version = this.version; - out.protocols = this.protocols; - - out.formfactor = this.formfactor; - out.os = this.os; - out.application = this.application; - out.appPackage = this.appPackage; - out.device = this.device; - out.fxaDeviceId = this.fxaDeviceId; - - return out; - } - -/* -Example record: - -{id:"relf31w7B4F1", - name:"marina_mac", - type:"mobile" - commands:[{"args":["bookmarks"],"command":"wipeEngine"}, - {"args":["forms"],"command":"wipeEngine"}, - {"args":["history"],"command":"wipeEngine"}, - {"args":["passwords"],"command":"wipeEngine"}, - {"args":["prefs"],"command":"wipeEngine"}, - {"args":["tabs"],"command":"wipeEngine"}, - {"args":["addons"],"command":"wipeEngine"}]} -*/ -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java deleted file mode 100644 index 897d2859c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -public class ClientRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - ClientRecord r = new ClientRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java deleted file mode 100644 index e7ca70cb4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java +++ /dev/null @@ -1,139 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -/** - * A FormHistoryRecord represents a saved form element. - * - * I map a <code>fieldName</code> string to a <code>value</code> string. - * - * @see "<a href='http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js'>http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js</a>." - */ -public class FormHistoryRecord extends Record { - private static final String LOG_TAG = "FormHistoryRecord"; - - public static final String COLLECTION_NAME = "forms"; - private static final String PAYLOAD_NAME = "name"; - private static final String PAYLOAD_VALUE = "value"; - public static final long FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds. - - /** - * The name of the saved form field. - */ - public String fieldName; - - /** - * The value of the saved form field. - */ - public String fieldValue; - - public FormHistoryRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = FORMS_TTL; - } - - public FormHistoryRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - - public FormHistoryRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - - public FormHistoryRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - - public FormHistoryRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - FormHistoryRecord out = new FormHistoryRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - - // Copy FormHistoryRecord fields. - out.fieldName = this.fieldName; - out.fieldValue = this.fieldValue; - - return out; - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, PAYLOAD_NAME, this.fieldName); - putPayload(payload, PAYLOAD_VALUE, this.fieldValue); - } - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - this.fieldName = payload.getString(PAYLOAD_NAME); - this.fieldValue = payload.getString(PAYLOAD_VALUE); - } - - /** - * We consider two form history records to be congruent if they represent the - * same form element regardless of times used. - */ - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof FormHistoryRecord)) { - return false; - } - FormHistoryRecord other = (FormHistoryRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.fieldName, other.fieldName) && - RepoUtils.stringsEqual(this.fieldValue, other.fieldValue); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof FormHistoryRecord)) { - Logger.debug(LOG_TAG, "Not a FormHistoryRecord: " + o.getClass()); - return false; - } - FormHistoryRecord other = (FormHistoryRecord) o; - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - - if (this.deleted) { - // FormHistoryRecords are equal if they are both deleted (which - // they are, since super.equalPayloads is true) and have the - // same GUID. - if (other.deleted) { - return RepoUtils.stringsEqual(this.guid, other.guid); - } - return false; - } - - return RepoUtils.stringsEqual(this.fieldName, other.fieldName) && - RepoUtils.stringsEqual(this.fieldValue, other.fieldValue); - } - - public FormHistoryRecord log(String logTag) { - try { - Logger.debug(logTag, "Returning form history record " + guid + " (" + androidID + ")"); - Logger.debug(logTag, "> Last modified: " + lastModified); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(logTag, "> Field name: " + fieldName); - Logger.pii(logTag, "> Field value: " + fieldValue); - } - } catch (Exception e) { - Logger.debug(logTag, "Exception logging form history record " + this, e); - } - return this; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java deleted file mode 100644 index 94eae13a7..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java +++ /dev/null @@ -1,217 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.util.HashMap; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -/** - * Visits are in microsecond precision. - * - * @author rnewman - * - */ -public class HistoryRecord extends Record { - private static final String LOG_TAG = "HistoryRecord"; - - public static final String COLLECTION_NAME = "history"; - public static final long HISTORY_TTL = 60 * 24 * 60 * 60; // 60 days in seconds. - - public HistoryRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = HISTORY_TTL; - } - public HistoryRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public HistoryRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public HistoryRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public HistoryRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String title; - public String histURI; - public JSONArray visits; - public long fennecDateVisited; - public long fennecVisitCount; - - @SuppressWarnings("unchecked") - private JSONArray copyVisits() { - if (this.visits == null) { - return null; - } - JSONArray out = new JSONArray(); - out.addAll(this.visits); - return out; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - HistoryRecord out = new HistoryRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy HistoryRecord fields. - out.title = this.title; - out.histURI = this.histURI; - out.fennecDateVisited = this.fennecDateVisited; - out.fennecVisitCount = this.fennecVisitCount; - out.visits = this.copyVisits(); - - return out; - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "title", this.title); - putPayload(payload, "histUri", this.histURI); // TODO: encoding? - payload.put("visits", this.visits); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.histURI = (String) payload.get("histUri"); - this.title = (String) payload.get("title"); - try { - this.visits = payload.getArray("visits"); - } catch (NonArrayJSONException e) { - Logger.error(LOG_TAG, "Got non-array visits in history record " + this.guid, e); - this.visits = new JSONArray(); - } - } - - /** - * We consider two history records to be congruent if they represent the - * same history record regardless of visits. Titles are allowed to differ, - * but the URI must be the same. - */ - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof HistoryRecord)) { - return false; - } - HistoryRecord other = (HistoryRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.histURI, other.histURI); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof HistoryRecord)) { - Logger.debug(LOG_TAG, "Not a HistoryRecord: " + o.getClass()); - return false; - } - HistoryRecord other = (HistoryRecord) o; - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - return RepoUtils.stringsEqual(this.title, other.title) && - RepoUtils.stringsEqual(this.histURI, other.histURI) && - checkVisitsEquals(other); - } - - @Override - public boolean equalAndroidIDs(Record other) { - return super.equalAndroidIDs(other) && - this.equalFennecVisits(other); - } - - private boolean equalFennecVisits(Record other) { - if (!(other instanceof HistoryRecord)) { - return false; - } - HistoryRecord h = (HistoryRecord) other; - return this.fennecDateVisited == h.fennecDateVisited && - this.fennecVisitCount == h.fennecVisitCount; - } - - private boolean checkVisitsEquals(HistoryRecord other) { - Logger.debug(LOG_TAG, "Checking visits."); - if (Logger.LOG_PERSONAL_INFORMATION) { - // Don't JSON-encode unless we're logging. - Logger.pii(LOG_TAG, ">> Mine: " + ((this.visits == null) ? "null" : this.visits.toJSONString())); - Logger.pii(LOG_TAG, ">> Theirs: " + ((other.visits == null) ? "null" : other.visits.toJSONString())); - } - - // Handle nulls. - if (this.visits == other.visits) { - return true; - } - - // Now they can't both be null. - int aSize = this.visits == null ? 0 : this.visits.size(); - int bSize = other.visits == null ? 0 : other.visits.size(); - - if (aSize != bSize) { - return false; - } - - // Now neither of them can be null. - - // TODO: do this by maintaining visits as a sorted array. - HashMap<Long, Long> otherVisits = new HashMap<Long, Long>(); - for (int i = 0; i < bSize; i++) { - JSONObject visit = (JSONObject) other.visits.get(i); - otherVisits.put((Long) visit.get("date"), (Long) visit.get("type")); - } - - for (int i = 0; i < aSize; i++) { - JSONObject visit = (JSONObject) this.visits.get(i); - if (!otherVisits.containsKey(visit.get("date"))) { - return false; - } - Long otherDate = (Long) visit.get("date"); - Long otherType = otherVisits.get(otherDate); - if (otherType == null) { - return false; - } - if (!otherType.equals((Long) visit.get("type"))) { - return false; - } - } - - return true; - } - -// -// Example record (note microsecond resolution): -// -// {id:"--DUvUomABNq", -// histUri:"https://bugzilla.mozilla.org/show_bug.cgi?id=697634", -// title:"697634 \u2013 xpcshell test failures on 10.7", -// visits:[{date:1320087601465600, type:2}, -// {date:1320084970724990, type:1}, -// {date:1320084847035717, type:1}, -// {date:1319764134412287, type:1}, -// {date:1319757917982518, type:1}, -// {date:1319751664627351, type:1}, -// {date:1319681421072326, type:1}, -// {date:1319681306455594, type:1}, -// {date:1319678117125234, type:1}, -// {date:1319677508862901, type:1}] -// } -// -//"type" is a transition type: -// -//https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsINavHistoryService#Transition_type_constants - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java deleted file mode 100644 index ac2c6a1dc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -/** - * Turns CryptoRecords into HistoryRecords. - * - * @author rnewman - * - */ -public class HistoryRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - HistoryRecord r = new HistoryRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java deleted file mode 100644 index b2de60f3c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java +++ /dev/null @@ -1,205 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -public class PasswordRecord extends Record { - private static final String LOG_TAG = "PasswordRecord"; - - public static final String COLLECTION_NAME = "passwords"; - public static long PASSWORDS_TTL = -1; // Never expire passwords. - - // Payload strings. - public static final String PAYLOAD_HOSTNAME = "hostname"; - public static final String PAYLOAD_FORM_SUBMIT_URL = "formSubmitURL"; - public static final String PAYLOAD_HTTP_REALM = "httpRealm"; - public static final String PAYLOAD_USERNAME = "username"; - public static final String PAYLOAD_PASSWORD = "password"; - public static final String PAYLOAD_USERNAME_FIELD = "usernameField"; - public static final String PAYLOAD_PASSWORD_FIELD = "passwordField"; - - public PasswordRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = PASSWORDS_TTL; - } - public PasswordRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public PasswordRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public PasswordRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public PasswordRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String id; - public String hostname; - public String formSubmitURL; - public String httpRealm; - // TODO these are encrypted in the passwords content provider, - // need to figure out what we need to do here. - public String usernameField; - public String passwordField; - public String encryptedUsername; - public String encryptedPassword; - public String encType; - - public long timeCreated; - public long timeLastUsed; - public long timePasswordChanged; - public long timesUsed; - - - @Override - public Record copyWithIDs(String guid, long androidID) { - PasswordRecord out = new PasswordRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy PasswordRecord fields. - out.id = this.id; - out.hostname = this.hostname; - out.formSubmitURL = this.formSubmitURL; - out.httpRealm = this.httpRealm; - - out.usernameField = this.usernameField; - out.passwordField = this.passwordField; - out.encryptedUsername = this.encryptedUsername; - out.encryptedPassword = this.encryptedPassword; - out.encType = this.encType; - - out.timeCreated = this.timeCreated; - out.timeLastUsed = this.timeLastUsed; - out.timePasswordChanged = this.timePasswordChanged; - out.timesUsed = this.timesUsed; - - return out; - } - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - this.hostname = payload.getString(PAYLOAD_HOSTNAME); - this.formSubmitURL = payload.getString(PAYLOAD_FORM_SUBMIT_URL); - this.httpRealm = payload.getString(PAYLOAD_HTTP_REALM); - this.encryptedUsername = payload.getString(PAYLOAD_USERNAME); - this.encryptedPassword = payload.getString(PAYLOAD_PASSWORD); - this.usernameField = payload.getString(PAYLOAD_USERNAME_FIELD); - this.passwordField = payload.getString(PAYLOAD_PASSWORD_FIELD); - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, PAYLOAD_HOSTNAME, this.hostname); - putPayload(payload, PAYLOAD_FORM_SUBMIT_URL, this.formSubmitURL); - putPayload(payload, PAYLOAD_HTTP_REALM, this.httpRealm); - putPayload(payload, PAYLOAD_USERNAME, this.encryptedUsername); - putPayload(payload, PAYLOAD_PASSWORD, this.encryptedPassword); - putPayload(payload, PAYLOAD_USERNAME_FIELD, this.usernameField); - putPayload(payload, PAYLOAD_PASSWORD_FIELD, this.passwordField); - } - - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof PasswordRecord)) { - return false; - } - PasswordRecord other = (PasswordRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.hostname, other.hostname) - && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL) - // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues. - // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm) - // && RepoUtils.stringsEqual(this.encType, other.encType) - && RepoUtils.stringsEqual(this.usernameField, other.usernameField) - && RepoUtils.stringsEqual(this.passwordField, other.passwordField) - && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername) - && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof PasswordRecord)) { - return false; - } - - PasswordRecord other = (PasswordRecord) o; - Logger.debug("PasswordRecord", "thisRecord:" + this.toString()); - Logger.debug("PasswordRecord", "otherRecord:" + o.toString()); - - if (this.deleted) { - if (other.deleted) { - // Deleted records are equal if their guids match. - return RepoUtils.stringsEqual(this.guid, other.guid); - } - // One record is deleted, the other is not. Not equal. - return false; - } - - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - - return RepoUtils.stringsEqual(this.hostname, other.hostname) - && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL) - // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues. - // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm) - // && RepoUtils.stringsEqual(this.encType, other.encType) - && RepoUtils.stringsEqual(this.usernameField, other.usernameField) - && RepoUtils.stringsEqual(this.passwordField, other.passwordField) - && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername) - && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword); - // Desktop sync never sets timeCreated so this isn't relevant for sync records. - } - - @Override - public String toString() { - return "PasswordRecord {" - + "lastModified: " + this.lastModified + ", " - + "hostname null?: " + (this.hostname == null) + ", " - + "formSubmitURL null?: " + (this.formSubmitURL == null) + ", " - + "httpRealm null?: " + (this.httpRealm == null) + ", " - + "usernameField null?: " + (this.usernameField == null) + ", " - + "passwordField null?: " + (this.passwordField == null) + ", " - + "encryptedUsername null?: " + (this.encryptedUsername == null) + ", " - + "encryptedPassword null?: " + (this.encryptedPassword == null) + ", " - + "encType: " + this.encType + ", " - + "timeCreated: " + this.timeCreated + ", " - + "timeLastUsed: " + this.timeLastUsed + ", " - + "timePasswordChanged: " + this.timePasswordChanged + ", " - + "timesUsed: " + this.timesUsed; - } - - /** - * A PasswordRecord is considered valid if it abides by the database - * constraints of the PasswordsProvider (moz_logins). - * - * See toolkit/components/passwordmgr/storage-mozStorage.js for the - * definitions: - * - * http://hg.mozilla.org/mozilla-central/file/00955d61cc94/toolkit/components/passwordmgr/storage-mozStorage.js#l98 - */ - public boolean isValid() { - if (this.deleted) { - return true; - } - - return this.hostname != null && - this.encryptedUsername != null && - this.encryptedPassword != null && - this.usernameField != null && - this.passwordField != null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java deleted file mode 100644 index fc7ef916d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class PasswordRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - PasswordRecord r = new PasswordRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java deleted file mode 100644 index 145704c1c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java +++ /dev/null @@ -1,308 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.io.UnsupportedEncodingException; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -/** - * Record is the abstract base class for all entries that Sync processes: - * bookmarks, passwords, history, and such. - * - * A Record can be initialized from or serialized to a CryptoRecord for - * submission to an encrypted store. - * - * Records should be considered to be conventionally immutable: modifications - * should be completed before the new record object escapes its constructing - * scope. Note that this is a critically important part of equality. As Rich - * Hickey notes: - * - * … the only things you can really compare for equality are immutable things, - * because if you compare two things for equality that are mutable, and ever - * say true, and they're ever not the same thing, you are wrong. Or you will - * become wrong at some point in the future. - * - * Records have a layered definition of equality. Two records can be said to be - * "equal" if: - * - * * They have the same GUID and collection. Two crypto/keys records are in some - * way "the same". - * This is `equalIdentifiers`. - * - * * Their most significant fields are the same. That is to say, they share a - * GUID, a collection, deletion, and domain-specific fields. Two copies of - * crypto/keys, neither deleted, with the same encrypted data but different - * modified times and sortIndex are in a stronger way "the same". - * This is `equalPayloads`. - * - * * Their most significant fields are the same, and their local fields (e.g., - * the androidID to which we have decided that this record maps) are congruent. - * A record with the same androidID, or one whose androidID has not been set, - * can be considered "the same". - * This concept can be extended by Record subclasses. The key point is that - * reconciling should be applied to the contents of these records. For example, - * two history records with the same URI and GUID, but different visit arrays, - * can be said to be congruent. - * This is `congruentWith`. - * - * * They are strictly identical. Every field that is persisted, including - * lastModified and androidID, is equal. - * This is `equals`. - * - * Different parts of the codebase have use for different layers of this - * comparison hierarchy. For instance, lastModified times change every time a - * record is stored; a store followed by a retrieval will return a Record that - * shares its most significant fields with the input, but has a later - * lastModified time and might not yet have values set for others. Reconciling - * will thus ignore the modification time of a record. - * - * @author rnewman - * - */ -public abstract class Record { - - public String guid; - public String collection; - public long lastModified; - public boolean deleted; - public long androidID; - /** - * An integer indicating the relative importance of this item in the collection. - * <p> - * Default is 0. - */ - public long sortIndex; - /** - * The number of seconds to keep this record. After that time this item will - * no longer be returned in response to any request, and it may be pruned from - * the database. - * <p> - * Negative values mean never forget this record. - * <p> - * Default is 1 year. - */ - public long ttl; - - public Record(String guid, String collection, long lastModified, boolean deleted) { - this.guid = guid; - this.collection = collection; - this.lastModified = lastModified; - this.deleted = deleted; - this.sortIndex = 0; - this.ttl = 365 * 24 * 60 * 60; // Seconds. - this.androidID = -1; - } - - /** - * Return true iff the input is a Record and has the same - * collection and guid as this object. - */ - public boolean equalIdentifiers(Object o) { - if (!(o instanceof Record)) { - return false; - } - - Record other = (Record) o; - if (this.guid == null) { - if (other.guid != null) { - return false; - } - } else { - if (!this.guid.equals(other.guid)) { - return false; - } - } - if (this.collection == null) { - if (other.collection != null) { - return false; - } - } else { - if (!this.collection.equals(other.collection)) { - return false; - } - } - return true; - } - - /** - * @param o - * The object to which this object should be compared. - * @return - * true iff the input is a Record which is substantially the - * same as this object. - */ - public boolean equalPayloads(Object o) { - if (!this.equalIdentifiers(o)) { - return false; - } - Record other = (Record) o; - return this.deleted == other.deleted; - } - - /** - * - * - * @param o - * The object to which this object should be compared. - * @return - * true iff the input is a Record which is substantially the - * same as this object, considering the ability and desire to - * reconcile the two objects if possible. - */ - public boolean congruentWith(Object o) { - if (!this.equalIdentifiers(o)) { - return false; - } - Record other = (Record) o; - return congruentAndroidIDs(other) && - (this.deleted == other.deleted); - } - - public boolean congruentAndroidIDs(Record other) { - // We treat -1 as "unset", and treat this as - // congruent with any other value. - if (this.androidID != -1 && - other.androidID != -1 && - this.androidID != other.androidID) { - return false; - } - return true; - } - - /** - * Return true iff the input is both equal in terms of payload, - * and also shares transient values such as timestamps. - */ - @Override - public boolean equals(Object o) { - if (!(o instanceof Record)) { - return false; - } - - Record other = (Record) o; - return equalTimestamps(other) && - equalSortIndices(other) && - equalAndroidIDs(other) && - equalPayloads(o); - } - - public boolean equalAndroidIDs(Record other) { - return this.androidID == other.androidID; - } - - public boolean equalSortIndices(Record other) { - return this.sortIndex == other.sortIndex; - } - - public boolean equalTimestamps(Object o) { - if (!(o instanceof Record)) { - return false; - } - return ((Record) o).lastModified == this.lastModified; - } - - protected abstract void populatePayload(ExtendedJSONObject payload); - protected abstract void initFromPayload(ExtendedJSONObject payload); - - public void initFromEnvelope(CryptoRecord envelope) { - ExtendedJSONObject p = envelope.payload; - this.guid = envelope.guid; - checkGUIDs(p); - - this.collection = envelope.collection; - this.lastModified = envelope.lastModified; - - final Object del = p.get("deleted"); - if (del instanceof Boolean) { - this.deleted = (Boolean) del; - } else { - this.initFromPayload(p); - } - - } - - public CryptoRecord getEnvelope() { - CryptoRecord rec = new CryptoRecord(this); - ExtendedJSONObject payload = new ExtendedJSONObject(); - payload.put("id", this.guid); - - if (this.deleted) { - payload.put("deleted", true); - } else { - populatePayload(payload); - } - rec.payload = payload; - return rec; - } - - @SuppressWarnings("static-method") - public String toJSONString() { - throw new RuntimeException("Cannot JSONify non-CryptoRecord Records."); - } - - public byte[] toJSONBytes() { - try { - return this.toJSONString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - // Can't happen. - return null; - } - } - - /** - * Utility for safely populating an output CryptoRecord. - * - * @param rec - * @param key - * @param value - */ - @SuppressWarnings("static-method") - protected void putPayload(CryptoRecord rec, String key, String value) { - if (value == null) { - return; - } - rec.payload.put(key, value); - } - - protected void putPayload(ExtendedJSONObject payload, String key, String value) { - this.putPayload(payload, key, value, false); - } - - @SuppressWarnings("static-method") - protected void putPayload(ExtendedJSONObject payload, String key, String value, boolean excludeEmpty) { - if (value == null) { - return; - } - if (excludeEmpty && value.equals("")) { - return; - } - payload.put(key, value); - } - - protected void checkGUIDs(ExtendedJSONObject payload) { - String payloadGUID = (String) payload.get("id"); - if (this.guid == null || - payloadGUID == null) { - String detailMessage = "Inconsistency: either envelope or payload GUID missing."; - throw new IllegalStateException(detailMessage); - } - if (!this.guid.equals(payloadGUID)) { - String detailMessage = "Inconsistency: record has envelope ID " + this.guid + ", payload ID " + payloadGUID; - throw new IllegalStateException(detailMessage); - } - } - - /** - * Oh for persistent data structures. - * - * @param guid - * @param androidID - * @return - * An identical copy of this record with the provided two values. - */ - public abstract Record copyWithIDs(String guid, long androidID); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java deleted file mode 100644 index 0d8fe90b2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - - -public class RecordParseException extends Exception { - private static final long serialVersionUID = -5145494854722254491L; - - public RecordParseException(String detailMessage) { - super(detailMessage); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java deleted file mode 100644 index eb3a4f6d0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java +++ /dev/null @@ -1,153 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.util.ArrayList; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.Tab; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.Utils; - -import android.content.ContentValues; - -/** - * Represents a client's collection of tabs. - * - * @author rnewman - * - */ -public class TabsRecord extends Record { - public static final String LOG_TAG = "TabsRecord"; - - public static final String COLLECTION_NAME = "tabs"; - public static final long TABS_TTL = 7 * 24 * 60 * 60; // 7 days in seconds. - - public TabsRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = TABS_TTL; - } - public TabsRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public TabsRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public TabsRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public TabsRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String clientName; - public ArrayList<Tab> tabs; - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - clientName = (String) payload.get("clientName"); - try { - tabs = tabsFrom(payload.getArray("tabs")); - } catch (NonArrayJSONException e) { - // Oh well. - tabs = new ArrayList<Tab>(); - } - } - - @SuppressWarnings("unchecked") - protected static JSONArray tabsToJSON(ArrayList<Tab> tabs) { - JSONArray out = new JSONArray(); - for (Tab tab : tabs) { - out.add(tabToJSONObject(tab)); - } - return out; - } - - protected static ArrayList<Tab> tabsFrom(JSONArray in) { - ArrayList<Tab> tabs = new ArrayList<Tab>(in.size()); - for (Object o : in) { - if (o instanceof JSONObject) { - try { - tabs.add(TabsRecord.tabFromJSONObject((JSONObject) o)); - } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "urlHistory is not an array for this tab.", e); - } - } - } - return tabs; - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "clientName", this.clientName); - payload.put("tabs", tabsToJSON(this.tabs)); - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - TabsRecord out = new TabsRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - out.clientName = this.clientName; - out.tabs = new ArrayList<Tab>(this.tabs); - - return out; - } - - public ContentValues getClientsContentValues() { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Clients.GUID, this.guid); - cv.put(BrowserContract.Clients.NAME, this.clientName); - cv.put(BrowserContract.Clients.LAST_MODIFIED, this.lastModified); - return cv; - } - - public ContentValues[] getTabsContentValues() { - int c = tabs.size(); - ContentValues[] out = new ContentValues[c]; - for (int i = 0; i < c; i++) { - out[i] = tabs.get(i).toContentValues(this.guid, i); - } - return out; - } - - public static Tab tabFromJSONObject(JSONObject o) throws NonArrayJSONException { - ExtendedJSONObject obj = new ExtendedJSONObject(o); - String title = obj.getString("title"); - String icon = obj.getString("icon"); - JSONArray history = obj.getArray("urlHistory"); - - // Last used is inexplicably a string in seconds. Most of the time. - long lastUsed = 0; - Object lU = obj.get("lastUsed"); - if (lU instanceof Number) { - lastUsed = ((Long) lU) * 1000L; - } else if (lU instanceof String) { - try { - lastUsed = Long.parseLong((String) lU, 10) * 1000L; - } catch (NumberFormatException e) { - Logger.debug(TabsRecord.LOG_TAG, "Invalid number format in lastUsed: " + lU); - } - } - return new Tab(title, icon, history, lastUsed); - } - - @SuppressWarnings("unchecked") - public static JSONObject tabToJSONObject(Tab tab) { - JSONObject o = new JSONObject(); - o.put("title", tab.title); - o.put("icon", tab.icon); - o.put("urlHistory", tab.history); - o.put("lastUsed", tab.lastUsed / 1000); - return o; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java deleted file mode 100644 index 9504434d8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -public class TabsRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - TabsRecord r = new TabsRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java deleted file mode 100644 index 2d3d4fd32..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -public class VersionConstants { - public static final int BOOKMARKS_ENGINE_VERSION = 2; - public static final int CLIENTS_ENGINE_VERSION = 1; - public static final int FORMS_ENGINE_VERSION = 1; - public static final int HISTORY_ENGINE_VERSION = 1; - public static final int PASSWORDS_ENGINE_VERSION = 1; - public static final int TABS_ENGINE_VERSION = 1; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java deleted file mode 100644 index 5c3037e4d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java +++ /dev/null @@ -1,310 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.downloaders; - -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.DelayedWorkTracker; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.repositories.Server11Repository; -import org.mozilla.gecko.sync.repositories.Server11RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Batching Downloader, which implements batching protocol as supported by Sync 1.5. - * - * Downloader's batching behaviour is configured via two parameters, obtained from the repository: - * - Per-batch limit, which specified how many records may be fetched in an individual GET request. - * - Total limit, which controls number of batch GET requests we will make. - * - * - * Batching is implemented via specifying a 'limit' GET parameter, and looking for an 'offset' token - * in the response. If offset token is present, this indicates that there are more records than what - * we've received so far, and we perform an additional fetch. Batching stops when either we hit a total - * limit, or offset token is no longer present (indicating that we're done). - * - * For unlimited repositories (such as passwords), both of these value will be -1. Downloader will not - * specify a limit parameter in this case, and the response will contain every record available and no - * offset token, thus fully completing in one go. - * - * In between batches, we maintain a Last-Modified timestamp, based off the value return in the header - * of the first response. Every response will have a Last-Modified header, indicating when the collection - * was modified last. We pass along this header in our subsequent requests in a X-If-Unmodified-Since - * header. Server will ensure that our collection did not change while we are batching, if it did it will - * fail our fetch with a 412 (Consequent Modification) error. Additionally, we perform the same checks - * locally. - */ -public class BatchingDownloader { - public static final String LOG_TAG = "BatchingDownloader"; - - protected final Server11Repository repository; - private final Server11RepositorySession repositorySession; - private final DelayedWorkTracker workTracker = new DelayedWorkTracker(); - // Used to track outstanding requests, so that we can abort them as needed. - @VisibleForTesting - protected final Set<SyncStorageCollectionRequest> pending = Collections.synchronizedSet(new HashSet<SyncStorageCollectionRequest>()); - /* @GuardedBy("this") */ private String lastModified; - /* @GuardedBy("this") */ private long numRecords = 0; - - public BatchingDownloader(final Server11Repository repository, final Server11RepositorySession repositorySession) { - this.repository = repository; - this.repositorySession = repositorySession; - } - - @VisibleForTesting - protected static String flattenIDs(String[] guids) { - // Consider using Utils.toDelimitedString if and when the signature changes - // to Collection<String> guids. - if (guids.length == 0) { - return ""; - } - if (guids.length == 1) { - return guids[0]; - } - // Assuming 12-char GUIDs. There should be a -1 in there, but we accumulate one comma too many. - StringBuilder b = new StringBuilder(guids.length * 12 + guids.length); - for (String guid : guids) { - b.append(guid); - b.append(","); - } - return b.substring(0, b.length() - 1); - } - - @VisibleForTesting - protected void fetchWithParameters(long newer, - long batchLimit, - boolean full, - String sort, - String ids, - SyncStorageCollectionRequest request, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) - throws URISyntaxException, UnsupportedEncodingException { - if (batchLimit > repository.getDefaultTotalLimit()) { - throw new IllegalArgumentException("Batch limit should not be greater than total limit"); - } - - request.delegate = new BatchingDownloaderDelegate(this, fetchRecordsDelegate, request, - newer, batchLimit, full, sort, ids); - this.pending.add(request); - request.get(); - } - - @VisibleForTesting - @Nullable - protected String encodeParam(String param) throws UnsupportedEncodingException { - if (param != null) { - return URLEncoder.encode(param, "UTF-8"); - } - return null; - } - - @VisibleForTesting - protected SyncStorageCollectionRequest makeSyncStorageCollectionRequest(long newer, - long batchLimit, - boolean full, - String sort, - String ids, - String offset) - throws URISyntaxException, UnsupportedEncodingException { - URI collectionURI = repository.collectionURI(full, newer, batchLimit, sort, ids, encodeParam(offset)); - Logger.debug(LOG_TAG, collectionURI.toString()); - - return new SyncStorageCollectionRequest(collectionURI); - } - - public void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - this.fetchSince(timestamp, null, fetchRecordsDelegate); - } - - private void fetchSince(long timestamp, String offset, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - long batchLimit = repository.getDefaultBatchLimit(); - String sort = repository.getDefaultSort(); - - try { - SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest(timestamp, - batchLimit, true, sort, null, offset); - this.fetchWithParameters(timestamp, batchLimit, true, sort, null, request, fetchRecordsDelegate); - } catch (URISyntaxException | UnsupportedEncodingException e) { - fetchRecordsDelegate.onFetchFailed(e, null); - } - } - - public void fetch(String[] guids, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - String ids = flattenIDs(guids); - String index = "index"; - - try { - SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest( - -1, -1, true, index, ids, null); - this.fetchWithParameters(-1, -1, true, index, ids, request, fetchRecordsDelegate); - } catch (URISyntaxException | UnsupportedEncodingException e) { - fetchRecordsDelegate.onFetchFailed(e, null); - } - } - - public Server11Repository getServerRepository() { - return this.repository; - } - - public void onFetchCompleted(SyncStorageResponse response, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request, long newer, - long limit, boolean full, String sort, String ids) { - removeRequestFromPending(request); - - // When we process our first request, we get back a X-Last-Modified header indicating when collection was modified last. - // We pass it to the server with every subsequent request (if we need to make more) as the X-If-Unmodified-Since header, - // and server is supposed to ensure that this pre-condition is met, and fail our request with a 412 error code otherwise. - // So, if all of this happens, these checks should never fail. - // However, we also track this header in client side, and can defensively validate against it here as well. - final String currentLastModifiedTimestamp = response.lastModified(); - Logger.debug(LOG_TAG, "Last modified timestamp " + currentLastModifiedTimestamp); - - // Sanity check. We also did a null check in delegate before passing it into here. - if (currentLastModifiedTimestamp == null) { - this.abort(fetchRecordsDelegate, "Last modified timestamp is missing"); - return; - } - - final boolean lastModifiedChanged; - synchronized (this) { - if (this.lastModified == null) { - // First time seeing last modified timestamp. - this.lastModified = currentLastModifiedTimestamp; - } - lastModifiedChanged = !this.lastModified.equals(currentLastModifiedTimestamp); - } - - if (lastModifiedChanged) { - this.abort(fetchRecordsDelegate, "Last modified timestamp has changed unexpectedly"); - return; - } - - final boolean hasNotReachedLimit; - synchronized (this) { - this.numRecords += response.weaveRecords(); - hasNotReachedLimit = this.numRecords < repository.getDefaultTotalLimit(); - } - - final String offset = response.weaveOffset(); - final SyncStorageCollectionRequest newRequest; - try { - newRequest = makeSyncStorageCollectionRequest(newer, - limit, full, sort, ids, offset); - } catch (final URISyntaxException | UnsupportedEncodingException e) { - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchFailed(e, null); - } - }); - return; - } - - if (offset != null && hasNotReachedLimit) { - try { - this.fetchWithParameters(newer, limit, full, sort, ids, newRequest, fetchRecordsDelegate); - } catch (final URISyntaxException | UnsupportedEncodingException e) { - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchFailed(e, null); - } - }); - } - return; - } - - final long normalizedTimestamp = response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED); - Logger.debug(LOG_TAG, "Fetch completed. Timestamp is " + normalizedTimestamp); - - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchCompleted(normalizedTimestamp); - } - }); - } - - public void onFetchFailed(final Exception ex, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request) { - removeRequestFromPending(request); - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Running onFetchFailed."); - fetchRecordsDelegate.onFetchFailed(ex, null); - } - }); - } - - public void onFetchedRecord(CryptoRecord record, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - this.workTracker.incrementOutstanding(); - try { - fetchRecordsDelegate.onFetchedRecord(record); - } catch (Exception ex) { - Logger.warn(LOG_TAG, "Got exception calling onFetchedRecord with WBO.", ex); - throw new RuntimeException(ex); - } finally { - this.workTracker.decrementOutstanding(); - } - } - - private void removeRequestFromPending(SyncStorageCollectionRequest request) { - if (request == null) { - return; - } - this.pending.remove(request); - } - - @VisibleForTesting - protected void abortRequests() { - this.repositorySession.abort(); - synchronized (this.pending) { - for (SyncStorageCollectionRequest request : this.pending) { - request.abort(); - } - this.pending.clear(); - } - } - - @Nullable - protected synchronized String getLastModified() { - return this.lastModified; - } - - private void abort(final RepositorySessionFetchRecordsDelegate delegate, final String msg) { - Logger.error(LOG_TAG, msg); - this.abortRequests(); - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - delegate.onFetchFailed( - new IllegalStateException(msg), - null); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java deleted file mode 100644 index eb9f76d6b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.downloaders; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; - -/** - * Delegate that gets passed into fetch methods to handle server response from fetch. - */ -public class BatchingDownloaderDelegate extends WBOCollectionRequestDelegate { - public static final String LOG_TAG = "BatchingDownloaderDelegate"; - - private BatchingDownloader downloader; - private RepositorySessionFetchRecordsDelegate fetchRecordsDelegate; - public SyncStorageCollectionRequest request; - // Used to pass back to BatchDownloader to start another fetch with these parameters if needed. - private long newer; - private long batchLimit; - private boolean full; - private String sort; - private String ids; - - public BatchingDownloaderDelegate(final BatchingDownloader downloader, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request, long newer, - long batchLimit, boolean full, String sort, String ids) { - this.downloader = downloader; - this.fetchRecordsDelegate = fetchRecordsDelegate; - this.request = request; - this.newer = newer; - this.batchLimit = batchLimit; - this.full = full; - this.sort = sort; - this.ids = ids; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return this.downloader.getServerRepository().getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - return this.downloader.getLastModified(); - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - Logger.debug(LOG_TAG, "Fetch done."); - if (response.lastModified() != null) { - this.downloader.onFetchCompleted(response, this.fetchRecordsDelegate, this.request, - this.newer, this.batchLimit, this.full, this.sort, this.ids); - return; - } - this.downloader.onFetchFailed( - new IllegalStateException("Missing last modified header from response"), - this.fetchRecordsDelegate, - this.request); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - this.handleRequestError(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(final Exception ex) { - Logger.warn(LOG_TAG, "Got request error.", ex); - this.downloader.onFetchFailed(ex, this.fetchRecordsDelegate, this.request); - } - - @Override - public void handleWBO(CryptoRecord record) { - this.downloader.onFetchedRecord(record, this.fetchRecordsDelegate); - } - - @Override - public KeyBundle keyBundle() { - return null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java deleted file mode 100644 index 951588586..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java +++ /dev/null @@ -1,165 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CheckResult; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.mozilla.gecko.background.common.log.Logger; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.TokenModifiedException; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedChangedUnexpectedly; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedDidNotChange; - -/** - * Keeps track of token, Last-Modified value and GUIDs of succeeded records. - */ -/* @ThreadSafe */ -public class BatchMeta extends BufferSizeTracker { - private static final String LOG_TAG = "BatchMeta"; - - // Will be set once first payload upload succeeds. We don't expect this to change until we - // commit the batch, and which point it must change. - /* @GuardedBy("this") */ private Long lastModified; - - // Will be set once first payload upload succeeds. We don't expect this to ever change until - // a commit succeeds, at which point this gets set to null. - /* @GuardedBy("this") */ private String token; - - /* @GuardedBy("accessLock") */ private boolean isUnlimited = false; - - // Accessed by synchronously running threads. - /* @GuardedBy("accessLock") */ private final List<String> successRecordGuids = new ArrayList<>(); - - /* @GuardedBy("accessLock") */ private boolean needsCommit = false; - - protected final Long collectionLastModified; - - public BatchMeta(@NonNull Object payloadLock, long maxBytes, long maxRecords, @Nullable Long collectionLastModified) { - super(payloadLock, maxBytes, maxRecords); - this.collectionLastModified = collectionLastModified; - } - - protected void setIsUnlimited(boolean isUnlimited) { - synchronized (accessLock) { - this.isUnlimited = isUnlimited; - } - } - - @Override - protected boolean canFit(long recordDeltaByteCount) { - synchronized (accessLock) { - return isUnlimited || super.canFit(recordDeltaByteCount); - } - } - - @Override - @CheckResult - protected boolean addAndEstimateIfFull(long recordDeltaByteCount) { - synchronized (accessLock) { - needsCommit = true; - boolean isFull = super.addAndEstimateIfFull(recordDeltaByteCount); - return !isUnlimited && isFull; - } - } - - protected boolean needToCommit() { - synchronized (accessLock) { - return needsCommit; - } - } - - protected synchronized String getToken() { - return token; - } - - protected synchronized void setToken(final String newToken, boolean isCommit) throws TokenModifiedException { - // Set token once in a batching mode. - // In a non-batching mode, this.token and newToken will be null, and this is a no-op. - if (token == null) { - token = newToken; - return; - } - - // Sanity checks. - if (isCommit) { - // We expect token to be null when commit payload succeeds. - if (newToken != null) { - throw new TokenModifiedException(); - } else { - token = null; - } - return; - } - - // We expect new token to always equal current token for non-commit payloads. - if (!token.equals(newToken)) { - throw new TokenModifiedException(); - } - } - - protected synchronized Long getLastModified() { - if (lastModified == null) { - return collectionLastModified; - } - return lastModified; - } - - protected synchronized void setLastModified(final Long newLastModified, final boolean expectedToChange) throws LastModifiedChangedUnexpectedly, LastModifiedDidNotChange { - if (lastModified == null) { - lastModified = newLastModified; - return; - } - - if (!expectedToChange && !lastModified.equals(newLastModified)) { - Logger.debug(LOG_TAG, "Last-Modified timestamp changed when we didn't expect it"); - throw new LastModifiedChangedUnexpectedly(); - - } else if (expectedToChange && lastModified.equals(newLastModified)) { - Logger.debug(LOG_TAG, "Last-Modified timestamp did not change when we expected it to"); - throw new LastModifiedDidNotChange(); - - } else { - lastModified = newLastModified; - } - } - - protected ArrayList<String> getSuccessRecordGuids() { - synchronized (accessLock) { - return new ArrayList<>(this.successRecordGuids); - } - } - - protected void recordSucceeded(final String recordGuid) { - // Sanity check. - if (recordGuid == null) { - throw new IllegalStateException(); - } - - synchronized (accessLock) { - successRecordGuids.add(recordGuid); - } - } - - @Override - protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) { - return isUnlimited || super.canFitRecordByteDelta(byteDelta, recordCount, byteCount); - } - - @Override - protected void reset() { - synchronized (accessLock) { - super.reset(); - token = null; - lastModified = null; - successRecordGuids.clear(); - needsCommit = false; - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java deleted file mode 100644 index 26efbd136..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java +++ /dev/null @@ -1,344 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.net.Uri; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.Server11RecordPostFailedException; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.repositories.Server11RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import java.util.ArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Uploader which implements batching introduced in Sync 1.5. - * - * Batch vs payload terminology: - * - batch is comprised of a series of payloads, which are all committed at the same time. - * -- identified via a "batch token", which is returned after first payload for the batch has been uploaded. - * - payload is a collection of records which are uploaded together. Associated with a batch. - * -- last payload, identified via commit=true, commits the batch. - * - * Limits for how many records can fit into a payload and into a batch are defined in the passed-in - * InfoConfiguration object. - * - * If we can't fit everything we'd like to upload into one batch (according to max-total-* limits), - * then we commit that batch, and start a new one. There are no explicit limits on total number of - * batches we might use, although at some point we'll start to run into storage limit errors from the API. - * - * Once we go past using one batch this uploader is no longer "atomic". Partial state is exposed - * to other clients after our first batch is committed and before our last batch is committed. - * However, our per-batch limits are high, X-I-U-S mechanics help protect downloading clients - * (as long as they implement X-I-U-S) with 412 error codes in case of interleaving upload and download, - * and most mobile clients will not be uploading large-enough amounts of data (especially structured - * data, such as bookmarks). - * - * Last-Modified header returned with the first batch payload POST success is maintained for a batch, - * to guard against concurrent-modification errors (different uploader commits before we're done). - * - * Non-batching mode notes: - * We also support Sync servers which don't enable batching for uploads. In this case, we respect - * payload limits for individual uploads, and every upload is considered a commit. Batching limits - * do not apply, and batch token is irrelevant. - * We do keep track of Last-Modified and send along X-I-U-S with our uploads, to protect against - * concurrent modifications by other clients. - */ -public class BatchingUploader { - private static final String LOG_TAG = "BatchingUploader"; - - private final Uri collectionUri; - - private volatile boolean recordUploadFailed = false; - - private final BatchMeta batchMeta; - private final Payload payload; - - // Accessed by synchronously running threads, OK to not synchronize and just make it volatile. - private volatile Boolean inBatchingMode; - - // Used to ensure we have thread-safe access to the following: - // - byte and record counts in both Payload and BatchMeta objects - // - buffers in the Payload object - private final Object payloadLock = new Object(); - - protected Executor workQueue; - protected final RepositorySessionStoreDelegate sessionStoreDelegate; - protected final Server11RepositorySession repositorySession; - - protected AtomicLong uploadTimestamp = new AtomicLong(0); - - protected static final int PER_RECORD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORD_SEPARATOR.length; - protected static final int PER_PAYLOAD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORDS_END.length; - - // Sanity check. RECORD_SEPARATOR and RECORD_START are assumed to be of the same length. - static { - if (RecordUploadRunnable.RECORD_SEPARATOR.length != RecordUploadRunnable.RECORDS_START.length) { - throw new IllegalStateException("Separator and start tokens must be of the same length"); - } - } - - public BatchingUploader(final Server11RepositorySession repositorySession, final Executor workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) { - this.repositorySession = repositorySession; - this.workQueue = workQueue; - this.sessionStoreDelegate = sessionStoreDelegate; - this.collectionUri = Uri.parse(repositorySession.getServerRepository().collectionURI().toString()); - - InfoConfiguration config = repositorySession.getServerRepository().getInfoConfiguration(); - this.batchMeta = new BatchMeta( - payloadLock, config.maxTotalBytes, config.maxTotalRecords, - repositorySession.getServerRepository().getCollectionLastModified() - ); - this.payload = new Payload(payloadLock, config.maxPostBytes, config.maxPostRecords); - } - - public void process(final Record record) { - final String guid = record.guid; - final byte[] recordBytes = record.toJSONBytes(); - final long recordDeltaByteCount = recordBytes.length + PER_RECORD_OVERHEAD_BYTE_COUNT; - - Logger.debug(LOG_TAG, "Processing a record with guid: " + guid); - - // We can't upload individual records which exceed our payload byte limit. - if ((recordDeltaByteCount + PER_PAYLOAD_OVERHEAD_BYTE_COUNT) > payload.maxBytes) { - sessionStoreDelegate.onRecordStoreFailed(new RecordTooLargeToUpload(), guid); - return; - } - - synchronized (payloadLock) { - final boolean canFitRecordIntoBatch = batchMeta.canFit(recordDeltaByteCount); - final boolean canFitRecordIntoPayload = payload.canFit(recordDeltaByteCount); - - // Record fits! - if (canFitRecordIntoBatch && canFitRecordIntoPayload) { - Logger.debug(LOG_TAG, "Record fits into the current batch and payload"); - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - - // Payload won't fit the record. - } else if (canFitRecordIntoBatch) { - Logger.debug(LOG_TAG, "Current payload won't fit incoming record, uploading payload."); - flush(false, false); - - Logger.debug(LOG_TAG, "Recording the incoming record into a new payload"); - - // Keep track of the overflow record. - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - - // Batch won't fit the record. - } else { - Logger.debug(LOG_TAG, "Current batch won't fit incoming record, committing batch."); - flush(true, false); - - Logger.debug(LOG_TAG, "Recording the incoming record into a new batch"); - batchMeta.reset(); - - // Keep track of the overflow record. - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - } - } - } - - // Convenience function used from the process method; caller must hold a payloadLock. - private void addAndFlushIfNecessary(long byteCount, byte[] recordBytes, String guid) { - boolean isPayloadFull = payload.addAndEstimateIfFull(byteCount, recordBytes, guid); - boolean isBatchFull = batchMeta.addAndEstimateIfFull(byteCount); - - // Preemptive commit batch or upload a payload if they're estimated to be full. - if (isBatchFull) { - flush(true, false); - batchMeta.reset(); - } else if (isPayloadFull) { - flush(false, false); - } - } - - public void noMoreRecordsToUpload() { - Logger.debug(LOG_TAG, "Received 'no more records to upload' signal."); - - // Run this after the last payload succeeds, so that we know for sure if we're in a batching - // mode and need to commit with a potentially empty payload. - workQueue.execute(new Runnable() { - @Override - public void run() { - commitIfNecessaryAfterLastPayload(); - } - }); - } - - @VisibleForTesting - protected void commitIfNecessaryAfterLastPayload() { - // Must be called after last payload upload finishes. - synchronized (payload) { - // If we have any pending records in the Payload, flush them! - if (!payload.isEmpty()) { - flush(true, true); - - // If we have an empty payload but need to commit the batch in the batching mode, flush! - } else if (batchMeta.needToCommit() && Boolean.TRUE.equals(inBatchingMode)) { - flush(true, true); - - // Otherwise, we're done. - } else { - finished(uploadTimestamp); - } - } - } - - /** - * We've been told by our upload delegate that a payload succeeded. - * Depending on the type of payload and batch mode status, inform our delegate of progress. - * - * @param response success response to our commit post - * @param isCommit was this a commit upload? - * @param isLastPayload was this a very last payload we'll upload? - */ - public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) { - // Sanity check. - if (inBatchingMode == null) { - throw new IllegalStateException("Can't process payload success until we know if we're in a batching mode"); - } - - // We consider records to have been committed if we're not in a batching mode or this was a commit. - // If records have been committed, notify our store delegate. - if (!inBatchingMode || isCommit) { - for (String guid : batchMeta.getSuccessRecordGuids()) { - sessionStoreDelegate.onRecordStoreSucceeded(guid); - } - } - - // If this was our very last commit, we're done storing records. - // Get Last-Modified timestamp from the response, and pass it upstream. - if (isLastPayload) { - finished(response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED)); - } - } - - public void lastPayloadFailed() { - finished(uploadTimestamp); - } - - private void finished(long lastModifiedTimestamp) { - bumpTimestampTo(uploadTimestamp, lastModifiedTimestamp); - finished(uploadTimestamp); - } - - private void finished(AtomicLong lastModifiedTimestamp) { - repositorySession.storeDone(lastModifiedTimestamp.get()); - } - - public BatchMeta getCurrentBatch() { - return batchMeta; - } - - public void setInBatchingMode(boolean inBatchingMode) { - this.inBatchingMode = inBatchingMode; - - // If we know for sure that we're not in a batching mode, - // consider our batch to be of unlimited size. - this.batchMeta.setIsUnlimited(!inBatchingMode); - } - - public Boolean getInBatchingMode() { - return inBatchingMode; - } - - public void setLastModified(final Long lastModified, final boolean isCommit) throws BatchingUploaderException { - // Sanity check. - if (inBatchingMode == null) { - throw new IllegalStateException("Can't process Last-Modified before we know we're in a batching mode."); - } - - // In non-batching mode, every time we receive a Last-Modified timestamp, we expect it to change - // since records are "committed" (become visible to other clients) on every payload. - // In batching mode, we only expect Last-Modified to change when we commit a batch. - batchMeta.setLastModified(lastModified, isCommit || !inBatchingMode); - } - - public void recordSucceeded(final String recordGuid) { - Logger.debug(LOG_TAG, "Record store succeeded: " + recordGuid); - batchMeta.recordSucceeded(recordGuid); - } - - public void recordFailed(final String recordGuid) { - recordFailed(new Server11RecordPostFailedException(), recordGuid); - } - - public void recordFailed(final Exception e, final String recordGuid) { - Logger.debug(LOG_TAG, "Record store failed for guid " + recordGuid + " with exception: " + e.toString()); - recordUploadFailed = true; - sessionStoreDelegate.onRecordStoreFailed(e, recordGuid); - } - - public Server11RepositorySession getRepositorySession() { - return repositorySession; - } - - private static void bumpTimestampTo(final AtomicLong current, long newValue) { - while (true) { - long existing = current.get(); - if (existing > newValue) { - return; - } - if (current.compareAndSet(existing, newValue)) { - return; - } - } - } - - private void flush(final boolean isCommit, final boolean isLastPayload) { - final ArrayList<byte[]> outgoing; - final ArrayList<String> outgoingGuids; - final long byteCount; - - // Even though payload object itself is thread-safe, we want to ensure we get these altogether - // as a "unit". Another approach would be to create a wrapper object for these values, but this works. - synchronized (payloadLock) { - outgoing = payload.getRecordsBuffer(); - outgoingGuids = payload.getRecordGuidsBuffer(); - byteCount = payload.getByteCount(); - } - - workQueue.execute(new RecordUploadRunnable( - new BatchingAtomicUploaderMayUploadProvider(), - collectionUri, - batchMeta, - new PayloadUploadDelegate(this, outgoingGuids, isCommit, isLastPayload), - outgoing, - byteCount, - isCommit - )); - - payload.reset(); - } - - private class BatchingAtomicUploaderMayUploadProvider implements MayUploadProvider { - public boolean mayUpload() { - return !recordUploadFailed; - } - } - - public static class BatchingUploaderException extends Exception { - private static final long serialVersionUID = 1L; - } - public static class RecordTooLargeToUpload extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class LastModifiedDidNotChange extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class LastModifiedChangedUnexpectedly extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class TokenModifiedException extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - }; -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java deleted file mode 100644 index 7f4c305f3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CallSuper; -import android.support.annotation.CheckResult; - -/** - * Implements functionality shared by BatchMeta and Payload objects, namely: - * - keeping track of byte and record counts - * - incrementing those counts when records are added - * - checking if a record can fit - */ -/* @ThreadSafe */ -public abstract class BufferSizeTracker { - protected final Object accessLock; - - /* @GuardedBy("accessLock") */ private long byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - /* @GuardedBy("accessLock") */ private long recordCount = 0; - /* @GuardedBy("accessLock") */ protected Long smallestRecordByteCount; - - protected final long maxBytes; - protected final long maxRecords; - - public BufferSizeTracker(Object accessLock, long maxBytes, long maxRecords) { - this.accessLock = accessLock; - this.maxBytes = maxBytes; - this.maxRecords = maxRecords; - } - - @CallSuper - protected boolean canFit(long recordDeltaByteCount) { - synchronized (accessLock) { - return canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount); - } - } - - protected boolean isEmpty() { - synchronized (accessLock) { - return recordCount == 0; - } - } - - /** - * Adds a record and returns a boolean indicating whether batch is estimated to be full afterwards. - */ - @CheckResult - protected boolean addAndEstimateIfFull(long recordDeltaByteCount) { - synchronized (accessLock) { - // Sanity check. Calling this method when buffer won't fit the record is an error. - if (!canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount)) { - throw new IllegalStateException("Buffer size exceeded"); - } - - byteCount += recordDeltaByteCount; - recordCount += 1; - - if (smallestRecordByteCount == null || smallestRecordByteCount > recordDeltaByteCount) { - smallestRecordByteCount = recordDeltaByteCount; - } - - // See if we're full or nearly full after adding a record. - // We're halving smallestRecordByteCount because we're erring - // on the side of "can hopefully fit". We're trying to upload as soon as we know we - // should, but we also need to be mindful of minimizing total number of uploads we make. - return !canFitRecordByteDelta(smallestRecordByteCount / 2, recordCount, byteCount); - } - } - - protected long getByteCount() { - synchronized (accessLock) { - // Ensure we account for payload overhead twice when the batch is empty. - // Payload overhead is either RECORDS_START ("[") or RECORDS_END ("]"), - // and for an empty payload we need account for both ("[]"). - if (recordCount == 0) { - return byteCount + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - } - return byteCount; - } - } - - protected long getRecordCount() { - synchronized (accessLock) { - return recordCount; - } - } - - @CallSuper - protected void reset() { - synchronized (accessLock) { - byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - recordCount = 0; - } - } - - @CallSuper - protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) { - return recordCount < maxRecords - && (byteCount + byteDelta) <= maxBytes; - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java deleted file mode 100644 index a1994cf62..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -public interface MayUploadProvider { - boolean mayUpload(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java deleted file mode 100644 index 1ed9b5798..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java +++ /dev/null @@ -1,66 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CheckResult; - -import java.util.ArrayList; - -/** - * Owns per-payload record byte and recordGuid buffers. - */ -/* @ThreadSafe */ -public class Payload extends BufferSizeTracker { - // Data of outbound records. - /* @GuardedBy("accessLock") */ private final ArrayList<byte[]> recordsBuffer = new ArrayList<>(); - - // GUIDs of outbound records. Used to fail entire payloads. - /* @GuardedBy("accessLock") */ private final ArrayList<String> recordGuidsBuffer = new ArrayList<>(); - - public Payload(Object payloadLock, long maxBytes, long maxRecords) { - super(payloadLock, maxBytes, maxRecords); - } - - @Override - protected boolean addAndEstimateIfFull(long recordDelta) { - throw new UnsupportedOperationException(); - } - - @CheckResult - protected boolean addAndEstimateIfFull(long recordDelta, byte[] recordBytes, String guid) { - synchronized (accessLock) { - recordsBuffer.add(recordBytes); - recordGuidsBuffer.add(guid); - return super.addAndEstimateIfFull(recordDelta); - } - } - - @Override - protected void reset() { - synchronized (accessLock) { - super.reset(); - recordsBuffer.clear(); - recordGuidsBuffer.clear(); - } - } - - protected ArrayList<byte[]> getRecordsBuffer() { - synchronized (accessLock) { - return new ArrayList<>(recordsBuffer); - } - } - - protected ArrayList<String> getRecordGuidsBuffer() { - synchronized (accessLock) { - return new ArrayList<>(recordGuidsBuffer); - } - } - - protected boolean isEmpty() { - synchronized (accessLock) { - return recordsBuffer.isEmpty(); - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java deleted file mode 100644 index e8bbb7df6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import java.util.ArrayList; - -public class PayloadUploadDelegate implements SyncStorageRequestDelegate { - private static final String LOG_TAG = "PayloadUploadDelegate"; - - private static final String KEY_BATCH = "batch"; - - private final BatchingUploader uploader; - private ArrayList<String> postedRecordGuids; - private final boolean isCommit; - private final boolean isLastPayload; - - public PayloadUploadDelegate(BatchingUploader uploader, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload) { - this.uploader = uploader; - this.postedRecordGuids = postedRecordGuids; - this.isCommit = isCommit; - this.isLastPayload = isLastPayload; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return uploader.getRepositorySession().getServerRepository().getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - final Long lastModified = uploader.getCurrentBatch().getLastModified(); - if (lastModified == null) { - return null; - } - return Utils.millisecondsToDecimalSecondsString(lastModified); - } - - @Override - public void handleRequestSuccess(final SyncStorageResponse response) { - // First, do some sanity checking. - if (response.getStatusCode() != 200 && response.getStatusCode() != 202) { - handleRequestError( - new IllegalStateException("handleRequestSuccess received a non-200/202 response: " + response.getStatusCode()) - ); - return; - } - - // We always expect to see a Last-Modified header. It's returned with every success response. - if (!response.httpResponse().containsHeader(SyncResponse.X_LAST_MODIFIED)) { - handleRequestError( - new IllegalStateException("Response did not have a Last-Modified header") - ); - return; - } - - // We expect to be able to parse the response as a JSON object. - final ExtendedJSONObject body; - try { - body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null. - } catch (Exception e) { - Logger.error(LOG_TAG, "Got exception parsing POST success body.", e); - this.handleRequestError(e); - return; - } - - // If we got a 200, it could be either a non-batching result, or a batch commit. - // - if we're in a batching mode, we expect this to be a commit. - // If we got a 202, we expect there to be a token present in the response - if (response.getStatusCode() == 200 && uploader.getCurrentBatch().getToken() != null) { - if (uploader.getInBatchingMode() && !isCommit) { - handleRequestError( - new IllegalStateException("Got 200 OK in batching mode, but this was not a commit payload") - ); - return; - } - } else if (response.getStatusCode() == 202) { - if (!body.containsKey(KEY_BATCH)) { - handleRequestError( - new IllegalStateException("Batch response did not have a batch ID") - ); - return; - } - } - - // With sanity checks out of the way, can now safely say if we're in a batching mode or not. - // We only do this once per session. - if (uploader.getInBatchingMode() == null) { - uploader.setInBatchingMode(body.containsKey(KEY_BATCH)); - } - - // Tell current batch about the token we've received. - // Throws if token changed after being set once, or if we got a non-null token after a commit. - try { - uploader.getCurrentBatch().setToken(body.getString(KEY_BATCH), isCommit); - } catch (BatchingUploader.BatchingUploaderException e) { - handleRequestError(e); - return; - } - - // Will throw if Last-Modified changed when it shouldn't have. - try { - uploader.setLastModified( - response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED), - isCommit); - } catch (BatchingUploader.BatchingUploaderException e) { - handleRequestError(e); - return; - } - - // All looks good up to this point, let's process success and failed arrays. - JSONArray success; - try { - success = body.getArray("success"); - } catch (NonArrayJSONException e) { - handleRequestError(e); - return; - } - - if (success != null && !success.isEmpty()) { - Logger.trace(LOG_TAG, "Successful records: " + success.toString()); - for (Object o : success) { - try { - uploader.recordSucceeded((String) o); - } catch (ClassCastException e) { - Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e); - // Not much to be done. - } - } - } - // GC - success = null; - - ExtendedJSONObject failed; - try { - failed = body.getObject("failed"); - } catch (NonObjectJSONException e) { - handleRequestError(e); - return; - } - - if (failed != null && !failed.object.isEmpty()) { - Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString()); - for (String guid : failed.keySet()) { - uploader.recordFailed(guid); - } - } - // GC - failed = null; - - // And we're done! Let uploader finish up. - uploader.payloadSucceeded(response, isCommit, isLastPayload); - } - - @Override - public void handleRequestFailure(final SyncStorageResponse response) { - this.handleRequestError(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(Exception e) { - for (String guid : postedRecordGuids) { - uploader.recordFailed(e, guid); - } - // GC - postedRecordGuids = null; - - if (isLastPayload) { - uploader.lastPayloadFailed(); - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java deleted file mode 100644 index ce2955102..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java +++ /dev/null @@ -1,176 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.net.Uri; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Server11PreviousPostFailedException; -import org.mozilla.gecko.sync.net.SyncStorageRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; - -import ch.boye.httpclientandroidlib.entity.ContentProducer; -import ch.boye.httpclientandroidlib.entity.EntityTemplate; - -/** - * Responsible for creating and posting a <code>SyncStorageRequest</code> request object. - */ -public class RecordUploadRunnable implements Runnable { - public final String LOG_TAG = "RecordUploadRunnable"; - - public final static byte[] RECORDS_START = { 91 }; // [ in UTF-8 - public final static byte[] RECORD_SEPARATOR = { 44 }; // , in UTF-8 - public final static byte[] RECORDS_END = { 93 }; // ] in UTF-8 - - private static final String QUERY_PARAM_BATCH = "batch"; - private static final String QUERY_PARAM_TRUE = "true"; - private static final String QUERY_PARAM_BATCH_COMMIT = "commit"; - - private final MayUploadProvider mayUploadProvider; - private final SyncStorageRequestDelegate uploadDelegate; - - private final ArrayList<byte[]> outgoing; - private final long byteCount; - - // Used to construct POST URI during run(). - @VisibleForTesting - public final boolean isCommit; - private final Uri collectionUri; - private final BatchMeta batchMeta; - - public RecordUploadRunnable(MayUploadProvider mayUploadProvider, - Uri collectionUri, - BatchMeta batchMeta, - SyncStorageRequestDelegate uploadDelegate, - ArrayList<byte[]> outgoing, - long byteCount, - boolean isCommit) { - this.mayUploadProvider = mayUploadProvider; - this.uploadDelegate = uploadDelegate; - this.outgoing = outgoing; - this.byteCount = byteCount; - this.batchMeta = batchMeta; - this.collectionUri = collectionUri; - this.isCommit = isCommit; - } - - public static class ByteArraysContentProducer implements ContentProducer { - ArrayList<byte[]> outgoing; - public ByteArraysContentProducer(ArrayList<byte[]> arrays) { - outgoing = arrays; - } - - @Override - public void writeTo(OutputStream outstream) throws IOException { - int count = outgoing.size(); - outstream.write(RECORDS_START); - if (count > 0) { - outstream.write(outgoing.get(0)); - for (int i = 1; i < count; ++i) { - outstream.write(RECORD_SEPARATOR); - outstream.write(outgoing.get(i)); - } - } - outstream.write(RECORDS_END); - } - - public static long outgoingBytesCount(ArrayList<byte[]> outgoing) { - final long numberOfRecords = outgoing.size(); - - // Account for start and end tokens. - long count = RECORDS_START.length + RECORDS_END.length; - - // Account for all the records. - for (int i = 0; i < numberOfRecords; i++) { - count += outgoing.get(i).length; - } - - // Account for a separator between the records. - // There's one less separator than there are records. - if (numberOfRecords > 1) { - count += RECORD_SEPARATOR.length * (numberOfRecords - 1); - } - - return count; - } - } - - public static class ByteArraysEntity extends EntityTemplate { - private final long count; - public ByteArraysEntity(ArrayList<byte[]> arrays, long totalBytes) { - super(new ByteArraysContentProducer(arrays)); - this.count = totalBytes; - this.setContentType("application/json"); - // charset is set in BaseResource. - - // Sanity check our byte counts. - long realByteCount = ByteArraysContentProducer.outgoingBytesCount(arrays); - if (realByteCount != totalBytes) { - throw new IllegalStateException("Mismatched byte counts. Received " + totalBytes + " while real byte count is " + realByteCount); - } - } - - @Override - public long getContentLength() { - return count; - } - - @Override - public boolean isRepeatable() { - return true; - } - } - - @Override - public void run() { - if (!mayUploadProvider.mayUpload()) { - Logger.info(LOG_TAG, "Told not to proceed by the uploader. Cancelling upload, failing records."); - uploadDelegate.handleRequestError(new Server11PreviousPostFailedException()); - return; - } - - Logger.trace(LOG_TAG, "Running upload task. Outgoing records: " + outgoing.size()); - - // We don't want the task queue to proceed until this request completes. - // Fortunately, BaseResource is currently synchronous. - // If that ever changes, you'll need to block here. - - final URI postURI = buildPostURI(isCommit, batchMeta, collectionUri); - final SyncStorageRequest request = new SyncStorageRequest(postURI); - request.delegate = uploadDelegate; - - ByteArraysEntity body = new ByteArraysEntity(outgoing, byteCount); - request.post(body); - } - - @VisibleForTesting - public static URI buildPostURI(boolean isCommit, BatchMeta batchMeta, Uri collectionUri) { - final Uri.Builder uriBuilder = collectionUri.buildUpon(); - final String batchToken = batchMeta.getToken(); - - if (batchToken != null) { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, batchToken); - } else { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, QUERY_PARAM_TRUE); - } - - if (isCommit) { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH_COMMIT, QUERY_PARAM_TRUE); - } - - try { - return new URI(uriBuilder.build().toString()); - } catch (URISyntaxException e) { - throw new IllegalStateException("Failed to construct a collection URI", e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java deleted file mode 100644 index 66e6768b4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/Constants.java +++ /dev/null @@ -1,29 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.setup; - -public class Constants { - public static final String DEFAULT_PROFILE = "default"; - - /** - * Key in sync extras bundle specifying stages to sync this sync session. - * <p> - * Corresponding value should be a String JSON-encoding an object, the keys of - * which are the stage names to sync. For example: - * <code>"{ \"stageToSync\": 0 }"</code>. - */ - public static final String EXTRAS_KEY_STAGES_TO_SYNC = "sync"; - - /** - * Key in sync extras bundle specifying stages to skip this sync session. - * <p> - * Corresponding value should be a String JSON-encoding an object, the keys of - * which are the stage names to skip. For example: - * <code>"{ \"stageToSkip\": 0 }"</code>. - */ - public static final String EXTRAS_KEY_STAGES_TO_SKIP = "skip"; - - public static final String JSON_KEY_ACCOUNT = "account"; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java deleted file mode 100644 index ac0fd58d0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/InvalidSyncKeyException.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.setup; - -public class InvalidSyncKeyException extends Exception { - private static final long serialVersionUID = -6504925951580479894L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java deleted file mode 100644 index 6542e1b00..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/ActivityUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.setup.activities; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import org.mozilla.gecko.background.common.GlobalConstants; -import org.mozilla.gecko.db.BrowserContract; - -public class ActivityUtils { - /** - * Open a URL in Fennec, if one is provided; or just open Fennec. - * - * @param context Android context. - * @param url to visit, or null to just open Fennec. - */ - public static void openURLInFennec(final Context context, final String url) { - Intent intent; - if (url != null) { - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - } else { - intent = new Intent(Intent.ACTION_MAIN); - } - intent.setClassName(GlobalConstants.BROWSER_INTENT_PACKAGE, GlobalConstants.BROWSER_INTENT_CLASS); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, true); - context.startActivity(intent); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java deleted file mode 100644 index 8411d2a62..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/setup/activities/WebURLFinder.java +++ /dev/null @@ -1,161 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.setup.activities; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class WebURLFinder { - /** - * These regular expressions are taken from Android's Patterns.java. - * We brought them in to standardize URL matching across Android versions, instead of relying - * on Android version-dependent built-ins that can vary across Android versions. - * The original code can be found here: - * http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/util/Patterns.java - * - */ - public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; - public static final String GOOD_GTLD_CHAR = "a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; - public static final String IRI = "[" + GOOD_IRI_CHAR + "]([" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]){0,1}"; - public static final String GTLD = "[" + GOOD_GTLD_CHAR + "]{2,63}"; - public static final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD; - public static final Pattern IP_ADDRESS = Pattern.compile("((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9]))"); - public static final Pattern DOMAIN_NAME = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); - public static final Pattern WEB_URL = Pattern.compile("((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" - + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" - + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" - + "(?:" + DOMAIN_NAME + ")" - + "(?:\\:\\d{1,5})?)" - + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" - + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" - + "(?:\\b|$)"); - - public final List<String> candidates; - - public WebURLFinder(String string) { - if (string == null) { - throw new IllegalArgumentException("string must not be null"); - } - - this.candidates = candidateWebURLs(string); - } - - public WebURLFinder(List<String> strings) { - if (strings == null) { - throw new IllegalArgumentException("strings must not be null"); - } - - this.candidates = candidateWebURLs(strings); - } - - /** - * Check if string is a Web URL. - * <p> - * A Web URL is a URI that is not a <code>file:</code> or - * <code>javascript:</code> scheme. - * - * @param string - * to check. - * @return <code>true</code> if <code>string</code> is a Web URL. - */ - public static boolean isWebURL(String string) { - try { - new URI(string); - } catch (Exception e) { - return false; - } - - if (android.webkit.URLUtil.isFileUrl(string) || - android.webkit.URLUtil.isJavaScriptUrl(string)) { - return false; - } - - return true; - } - - /** - * Return best Web URL. - * <p> - * "Best" means a Web URL with a scheme, and failing that, a Web URL without a - * scheme. - * - * @return a Web URL or <code>null</code>. - */ - public String bestWebURL() { - String firstWebURLWithScheme = firstWebURLWithScheme(); - if (firstWebURLWithScheme != null) { - return firstWebURLWithScheme; - } - - return firstWebURLWithoutScheme(); - } - - protected static List<String> candidateWebURLs(Collection<String> strings) { - List<String> candidates = new ArrayList<String>(); - - for (String string : strings) { - if (string == null) { - continue; - } - - candidates.addAll(candidateWebURLs(string)); - } - - return candidates; - } - - protected static List<String> candidateWebURLs(String string) { - Matcher matcher = WEB_URL.matcher(string); - List<String> matches = new LinkedList<String>(); - - while (matcher.find()) { - // Remove URLs with bad schemes. - if (!isWebURL(matcher.group())) { - continue; - } - - // Remove parts of email addresses. - if (matcher.start() > 0 && (string.charAt(matcher.start() - 1) == '@')) { - continue; - } - - matches.add(matcher.group()); - } - - return matches; - } - - protected String firstWebURLWithScheme() { - for (String match : candidates) { - try { - if (new URI(match).getScheme() != null) { - return match; - } - } catch (URISyntaxException e) { - // Ignore: on to the next. - continue; - } - } - - return null; - } - - protected String firstWebURLWithoutScheme() { - if (!candidates.isEmpty()) { - return candidates.get(0); - } - - return null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java deleted file mode 100644 index c910216eb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractNonRepositorySyncStage.java +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - - -/** - * This is simply a stage that is not responsible for synchronizing repositories. - */ -public abstract class AbstractNonRepositorySyncStage extends AbstractSessionManagingSyncStage { - @Override - protected void resetLocal() { - // Do nothing. - } - - @Override - protected void wipeLocal() { - // Do nothing. - } - - @Override - public Integer getStorageVersion() { - return null; // Never include these engines in any meta/global records. - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java deleted file mode 100644 index 6592c3baa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AbstractSessionManagingSyncStage.java +++ /dev/null @@ -1,43 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import org.mozilla.gecko.sync.GlobalSession; - -/** - * A global sync stage that manages a <code>GlobalSession</code> instance. This - * class is intended to be temporary: it should disappear as work to make - * data-driven syncs progresses. - * <p> - * This class is inherently <b>thread-unsafe</b>: if <code>session</code> is - * mutated after being set, all sorts of bad things could occur. At the time of - * writing, every <code>GlobalSyncStage</code> created is executed (wiped, - * reset) with the same <code>GlobalSession</code> argument. - */ -public abstract class AbstractSessionManagingSyncStage implements GlobalSyncStage { - protected GlobalSession session; - - protected abstract void execute() throws NoSuchStageException; - protected abstract void resetLocal(); - protected abstract void wipeLocal() throws Exception; - - @Override - public void resetLocal(GlobalSession session) { - this.session = session; - resetLocal(); - } - - @Override - public void wipeLocal(GlobalSession session) throws Exception { - this.session = session; - wipeLocal(); - } - - @Override - public void execute(GlobalSession session) throws NoSuchStageException { - this.session = session; - execute(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java deleted file mode 100644 index 10e209230..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserBookmarksServerSyncStage.java +++ /dev/null @@ -1,80 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.JSONRecordFetcher; -import org.mozilla.gecko.sync.MetaGlobalException; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage { - protected static final String LOG_TAG = "BookmarksStage"; - - // Eventually this kind of sync stage will be data-driven, - // and all this hard-coding can go away. - private static final String BOOKMARKS_SORT = "index"; - // Sanity limit. Batch and total limit are the same for now, and will be adjusted - // once buffer and high water mark are in place. See Bug 730142. - private static final long BOOKMARKS_BATCH_LIMIT = 5000; - private static final long BOOKMARKS_TOTAL_LIMIT = 5000; - - @Override - protected String getCollection() { - return "bookmarks"; - } - - @Override - protected String getEngineName() { - return "bookmarks"; - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.BOOKMARKS_ENGINE_VERSION; - } - - @Override - protected Repository getRemoteRepository() throws URISyntaxException { - // If this is a first sync, we need to check server counts to make sure that we aren't - // going to screw up. SafeConstrainedServer11Repository does this. See Bug 814331. - AuthHeaderProvider authHeaderProvider = session.getAuthHeaderProvider(); - final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(session.config.infoCollectionCountsURL(), authHeaderProvider); - String collection = getCollection(); - return new SafeConstrainedServer11Repository( - collection, - session.config.storageURL(), - session.getAuthHeaderProvider(), - session.config.infoCollections, - session.config.infoConfiguration, - BOOKMARKS_BATCH_LIMIT, - BOOKMARKS_TOTAL_LIMIT, - BOOKMARKS_SORT, - countsFetcher); - } - - @Override - protected Repository getLocalRepository() { - return new AndroidBrowserBookmarksRepository(); - } - - @Override - protected RecordFactory getRecordFactory() { - return new BookmarkRecordFactory(); - } - - @Override - protected boolean isEnabled() throws MetaGlobalException { - if (session == null || session.getContext() == null) { - return false; - } - return super.isEnabled(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java deleted file mode 100644 index 947a10898..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/AndroidBrowserHistoryServerSyncStage.java +++ /dev/null @@ -1,74 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.MetaGlobalException; -import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecordFactory; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage { - protected static final String LOG_TAG = "HistoryStage"; - - // Eventually this kind of sync stage will be data-driven, - // and all this hard-coding can go away. - private static final String HISTORY_SORT = "index"; - // Sanity limit. Batch and total limit are the same for now, and will be adjusted - // once buffer and high water mark are in place. See Bug 730142. - private static final long HISTORY_BATCH_LIMIT = 250; - private static final long HISTORY_TOTAL_LIMIT = 250; - - @Override - protected String getCollection() { - return "history"; - } - - @Override - protected String getEngineName() { - return "history"; - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.HISTORY_ENGINE_VERSION; - } - - @Override - protected Repository getLocalRepository() { - return new AndroidBrowserHistoryRepository(); - } - - @Override - protected Repository getRemoteRepository() throws URISyntaxException { - String collection = getCollection(); - return new ConstrainedServer11Repository( - collection, - session.config.storageURL(), - session.getAuthHeaderProvider(), - session.config.infoCollections, - session.config.infoConfiguration, - HISTORY_BATCH_LIMIT, - HISTORY_TOTAL_LIMIT, - HISTORY_SORT); - } - - @Override - protected RecordFactory getRecordFactory() { - return new HistoryRecordFactory(); - } - - @Override - protected boolean isEnabled() throws MetaGlobalException { - if (session == null || session.getContext() == null) { - return false; - } - return super.isEnabled(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java deleted file mode 100644 index b33f83ad1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CheckPreconditionsStage.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - - -public class CheckPreconditionsStage extends AbstractNonRepositorySyncStage { - @Override - public void execute() throws NoSuchStageException { - session.advance(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java deleted file mode 100644 index 7ec776324..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/CompletedStage.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - - - -public class CompletedStage extends AbstractNonRepositorySyncStage { - @Override - public void execute() throws NoSuchStageException { - // TODO: Update tracking timestamps, close connections, etc. - // TODO: call clean() on each Repository in the sync constellation. - session.completeSync(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java deleted file mode 100644 index 5031cf770..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/EnsureCrypto5KeysStage.java +++ /dev/null @@ -1,192 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; -import java.util.HashSet; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CollectionKeys; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.NoCollectionKeysSetException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public class EnsureCrypto5KeysStage -extends AbstractNonRepositorySyncStage -implements SyncStorageRequestDelegate { - - private static final String LOG_TAG = "EnsureC5KeysStage"; - private static final String CRYPTO_COLLECTION = "crypto"; - protected boolean retrying = false; - - @Override - public void execute() throws NoSuchStageException { - InfoCollections infoCollections = session.config.infoCollections; - if (infoCollections == null) { - session.abort(null, "No info/collections set in EnsureCrypto5KeysStage."); - return; - } - - PersistedCrypto5Keys pck = session.config.persistedCryptoKeys(); - long lastModified = pck.lastModified(); - if (retrying || !infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) { - // Try to use our local collection keys for this session. - Logger.debug(LOG_TAG, "Trying to use persisted collection keys for this session."); - CollectionKeys keys = pck.keys(); - if (keys != null) { - Logger.trace(LOG_TAG, "Using persisted collection keys for this session."); - session.config.setCollectionKeys(keys); - session.advance(); - return; - } - Logger.trace(LOG_TAG, "Failed to use persisted collection keys for this session."); - } - - // We need an update: fetch fresh keys. - Logger.debug(LOG_TAG, "Fetching fresh collection keys for this session."); - try { - SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys")); - request.delegate = this; - request.get(); - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI."); - } - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return session.getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - // TODO: last key time! - return null; - } - - protected void setAndPersist(PersistedCrypto5Keys pck, CollectionKeys keys, long timestamp) { - session.config.setCollectionKeys(keys); - pck.persistKeys(keys); - pck.persistLastModified(timestamp); - } - - /** - * Return collections where either the individual key has changed, or if the - * new default key is not the same as the old default key, where the - * collection is using the default key. - */ - protected Set<String> collectionsToUpdate(CollectionKeys oldKeys, CollectionKeys newKeys) { - // These keys have explicitly changed; they definitely need updating. - Set<String> changedKeys = new HashSet<String>(CollectionKeys.differences(oldKeys, newKeys)); - - boolean defaultKeyChanged = true; // Most pessimistic is to assume default key has changed. - KeyBundle newDefaultKeyBundle = null; - try { - KeyBundle oldDefaultKeyBundle = oldKeys.defaultKeyBundle(); - newDefaultKeyBundle = newKeys.defaultKeyBundle(); - defaultKeyChanged = !oldDefaultKeyBundle.equals(newDefaultKeyBundle); - } catch (NoCollectionKeysSetException e) { - Logger.warn(LOG_TAG, "NoCollectionKeysSetException in EnsureCrypto5KeysStage.", e); - } - - if (newDefaultKeyBundle == null) { - Logger.trace(LOG_TAG, "New default key not provided; returning changed individual keys."); - return changedKeys; - } - - if (!defaultKeyChanged) { - Logger.trace(LOG_TAG, "New default key is the same as old default key; returning changed individual keys."); - return changedKeys; - } - - // New keys have a different default/sync key; check known collections against the default key. - Logger.debug(LOG_TAG, "New default key is not the same as old default key."); - for (Stage stage : Stage.getNamedStages()) { - String name = stage.getRepositoryName(); - if (!newKeys.keyBundleForCollectionIsNotDefault(name)) { - // Default key has changed, so this collection has changed. - changedKeys.add(name); - } - } - - return changedKeys; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - // Take the timestamp from the response since it is later than the timestamp from info/collections. - long responseTimestamp = response.normalizedWeaveTimestamp(); - CollectionKeys keys = new CollectionKeys(); - try { - ExtendedJSONObject body = response.jsonObjectBody(); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString()); - } - keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle); - } catch (Exception e) { - session.abort(e, "Invalid keys WBO."); - return; - } - - PersistedCrypto5Keys pck = session.config.persistedCryptoKeys(); - if (!pck.persistedKeysExist()) { - // New keys, and no old keys! Persist keys and server timestamp. - Logger.trace(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified."); - setAndPersist(pck, keys, responseTimestamp); - session.advance(); - return; - } - - // New keys, but we had old keys. Check for differences. - CollectionKeys oldKeys = pck.keys(); - Set<String> changedCollections = collectionsToUpdate(oldKeys, keys); - if (!changedCollections.isEmpty()) { - // New keys, different from old keys. - Logger.trace(LOG_TAG, "Fetched keys are not the same as persisted keys; " + - "setting fetched keys for this session before resetting changed engines."); - setAndPersist(pck, keys, responseTimestamp); - session.resetStagesByName(changedCollections); - session.abort(null, "crypto/keys changed on server."); - return; - } - - // New keys don't differ from old keys; persist timestamp and move on. - Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified."); - session.config.setCollectionKeys(oldKeys); - pck.persistLastModified(response.normalizedWeaveTimestamp()); - session.advance(); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - if (retrying) { - // Should happen very rarely -- this means we uploaded our crypto/keys - // successfully, but failed to re-download. - session.handleHTTPError(response, "Failure while re-downloading already uploaded keys."); - return; - } - - int statusCode = response.getStatusCode(); - if (statusCode == 404) { - Logger.info(LOG_TAG, "Got 404 fetching keys. Fresh starting since keys are missing on server."); - session.freshStart(); - return; - } - session.handleHTTPError(response, "Failure fetching keys: got response status code " + statusCode); - } - - @Override - public void handleRequestError(Exception ex) { - session.abort(ex, "Failure fetching keys."); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java deleted file mode 100644 index 40a474ef4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FennecTabsServerSyncStage.java +++ /dev/null @@ -1,40 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; -import org.mozilla.gecko.sync.repositories.domain.TabsRecordFactory; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -public class FennecTabsServerSyncStage extends ServerSyncStage { - private static final String COLLECTION = "tabs"; - - @Override - protected String getCollection() { - return COLLECTION; - } - - @Override - protected String getEngineName() { - return COLLECTION; - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.TABS_ENGINE_VERSION; - } - - @Override - protected Repository getLocalRepository() { - return new FennecTabsRepository(session.getClientsDelegate()); - } - - @Override - protected RecordFactory getRecordFactory() { - return new TabsRecordFactory(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java deleted file mode 100644 index 088321d5b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoCollectionsStage.java +++ /dev/null @@ -1,44 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public class FetchInfoCollectionsStage extends AbstractNonRepositorySyncStage { - public class StageInfoCollectionsDelegate implements JSONRecordFetchDelegate { - - @Override - public void handleSuccess(ExtendedJSONObject global) { - session.config.infoCollections = new InfoCollections(global); - session.advance(); - } - - @Override - public void handleFailure(SyncStorageResponse response) { - session.handleHTTPError(response, "Failure fetching info/collections."); - } - - @Override - public void handleError(Exception e) { - session.abort(e, "Failure fetching info/collections."); - } - - } - - @Override - public void execute() throws NoSuchStageException { - try { - session.fetchInfoCollections(new StageInfoCollectionsDelegate()); - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI."); - } - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java deleted file mode 100644 index 7f53c2739..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchInfoConfigurationStage.java +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.JSONRecordFetcher; -import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -/** - * Fetches configuration data from info/configurations endpoint. - */ -public class FetchInfoConfigurationStage extends AbstractNonRepositorySyncStage { - private final String configurationURL; - private final AuthHeaderProvider authHeaderProvider; - - public FetchInfoConfigurationStage(final String configurationURL, final AuthHeaderProvider authHeaderProvider) { - super(); - this.configurationURL = configurationURL; - this.authHeaderProvider = authHeaderProvider; - } - - public class StageInfoConfigurationDelegate implements JSONRecordFetchDelegate { - @Override - public void handleSuccess(final ExtendedJSONObject result) { - session.config.infoConfiguration = new InfoConfiguration(result); - session.advance(); - } - - @Override - public void handleFailure(final SyncStorageResponse response) { - // Handle all non-404 failures upstream. - if (response.getStatusCode() != 404) { - session.handleHTTPError(response, "Failure fetching info/configuration"); - return; - } - - // End-point might not be available (404) if server is running an older version. - // We will use default config values in this case. - session.config.infoConfiguration = new InfoConfiguration(); - session.advance(); - } - - @Override - public void handleError(final Exception e) { - session.abort(e, "Failure fetching info/configuration"); - } - } - @Override - public void execute() { - final StageInfoConfigurationDelegate delegate = new StageInfoConfigurationDelegate(); - final JSONRecordFetcher fetcher = new JSONRecordFetcher(configurationURL, authHeaderProvider); - fetcher.fetch(delegate); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java deleted file mode 100644 index b4407b26b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FetchMetaGlobalStage.java +++ /dev/null @@ -1,79 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.MetaGlobal; -import org.mozilla.gecko.sync.PersistedMetaGlobal; -import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -public class FetchMetaGlobalStage extends AbstractNonRepositorySyncStage { - private static final String LOG_TAG = "FetchMetaGlobalStage"; - private static final String META_COLLECTION = "meta"; - - public class StageMetaGlobalDelegate implements MetaGlobalDelegate { - - private final GlobalSession session; - public StageMetaGlobalDelegate(GlobalSession session) { - this.session = session; - } - - @Override - public void handleSuccess(MetaGlobal global, SyncStorageResponse response) { - Logger.trace(LOG_TAG, "Persisting fetched meta/global and last modified."); - PersistedMetaGlobal pmg = session.config.persistedMetaGlobal(); - pmg.persistMetaGlobal(global); - // Take the timestamp from the response since it is later than the timestamp from info/collections. - pmg.persistLastModified(response.normalizedWeaveTimestamp()); - - session.processMetaGlobal(global); - } - - @Override - public void handleFailure(SyncStorageResponse response) { - session.handleHTTPError(response, "Failure fetching meta/global."); - } - - @Override - public void handleError(Exception e) { - session.abort(e, "Failure fetching meta/global."); - } - - @Override - public void handleMissing(MetaGlobal global, SyncStorageResponse response) { - session.processMissingMetaGlobal(global); - } - } - - @Override - public void execute() throws NoSuchStageException { - InfoCollections infoCollections = session.config.infoCollections; - if (infoCollections == null) { - session.abort(null, "No info/collections set in FetchMetaGlobalStage."); - return; - } - - long lastModified = session.config.persistedMetaGlobal().lastModified(); - if (!infoCollections.updateNeeded(META_COLLECTION, lastModified)) { - // Try to use our local collection keys for this session. - Logger.info(LOG_TAG, "Trying to use persisted meta/global for this session."); - MetaGlobal global = session.config.persistedMetaGlobal().metaGlobal(session.config.metaURL(), session.getAuthHeaderProvider()); - if (global != null) { - Logger.info(LOG_TAG, "Using persisted meta/global for this session."); - session.processMetaGlobal(global); // Calls session.advance(). - return; - } - Logger.info(LOG_TAG, "Failed to use persisted meta/global for this session."); - } - - // We need an update: fetch or upload meta/global as necessary. - Logger.info(LOG_TAG, "Fetching fresh meta/global for this session."); - MetaGlobal global = new MetaGlobal(session.config.metaURL(), session.getAuthHeaderProvider()); - global.fetch(new StageMetaGlobalDelegate(session)); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java deleted file mode 100644 index 0a5d974b8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/FormHistoryServerSyncStage.java +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.android.FormHistoryRepositorySession; -import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -public class FormHistoryServerSyncStage extends ServerSyncStage { - - // Eventually this kind of sync stage will be data-driven, - // and all this hard-coding can go away. - private static final String FORM_HISTORY_SORT = "index"; - // Sanity limit. Batch and total limit are the same for now, and will be adjusted - // once buffer and high water mark are in place. See Bug 730142. - private static final long FORM_HISTORY_BATCH_LIMIT = 5000; - private static final long FORM_HISTORY_TOTAL_LIMIT = 5000; - - @Override - protected String getCollection() { - return "forms"; - } - - @Override - protected String getEngineName() { - return "forms"; - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.FORMS_ENGINE_VERSION; - } - - @Override - protected Repository getRemoteRepository() throws URISyntaxException { - String collection = getCollection(); - return new ConstrainedServer11Repository( - collection, - session.config.storageURL(), - session.getAuthHeaderProvider(), - session.config.infoCollections, - session.config.infoConfiguration, - FORM_HISTORY_BATCH_LIMIT, - FORM_HISTORY_TOTAL_LIMIT, - FORM_HISTORY_SORT); - } - - @Override - protected Repository getLocalRepository() { - return new FormHistoryRepositorySession.FormHistoryRepository(); - } - - public class FormHistoryRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - FormHistoryRecord r = new FormHistoryRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } - } - - @Override - protected RecordFactory getRecordFactory() { - return new FormHistoryRecordFactory(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java deleted file mode 100644 index 6dee71f90..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/GlobalSyncStage.java +++ /dev/null @@ -1,93 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; - -import org.mozilla.gecko.sync.GlobalSession; - - -public interface GlobalSyncStage { - public static enum Stage { - idle, // Start state. - checkPreconditions, // Preparation of the basics. TODO: clear status - fetchInfoCollections, // Take a look at timestamps. - fetchInfoConfiguration, // Fetch server upload limits - fetchMetaGlobal, - ensureKeysStage, - /* - ensureSpecialRecords, - updateEngineTimestamps, - */ - syncClientsEngine(SyncClientsEngineStage.STAGE_NAME), - /* - processFirstSyncPref, - processClientCommands, - updateEnabledEngines, - */ - syncTabs("tabs"), - syncPasswords("passwords"), - syncBookmarks("bookmarks"), - syncHistory("history"), - syncFormHistory("forms"), - - uploadMetaGlobal, - completed; - - // Maintain a mapping from names ("bookmarks") to Stage enumerations (syncBookmarks). - private static final Map<String, Stage> named = new HashMap<String, Stage>(); - static { - for (Stage s : EnumSet.allOf(Stage.class)) { - if (s.getRepositoryName() != null) { - named.put(s.getRepositoryName(), s); - } - } - } - - public static Stage byName(final String name) { - if (name == null) { - return null; - } - return named.get(name); - } - - /** - * @return an immutable collection of Stages. - */ - public static Collection<Stage> getNamedStages() { - return Collections.unmodifiableCollection(named.values()); - } - - // Each Stage tracks its repositoryName. - private final String repositoryName; - public String getRepositoryName() { - return repositoryName; - } - - private Stage() { - this.repositoryName = null; - } - - private Stage(final String name) { - this.repositoryName = name; - } - } - - public void execute(GlobalSession session) throws NoSuchStageException; - public void resetLocal(GlobalSession session); - public void wipeLocal(GlobalSession session) throws Exception; - - /** - * What storage version number this engine supports. - * <p> - * Used to generate a fresh meta/global record for upload. - * @return a version number or <code>null</code> to never include this engine in a fresh meta/global record. - */ - public Integer getStorageVersion(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java deleted file mode 100644 index 14c9bb43e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/NoSuchStageException.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -public class NoSuchStageException extends Exception { - private static final long serialVersionUID = 8338484472880746971L; - GlobalSyncStage.Stage stage; - public NoSuchStageException(GlobalSyncStage.Stage stage) { - this.stage = stage; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java deleted file mode 100644 index c781ce2cc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/PasswordsServerSyncStage.java +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession; -import org.mozilla.gecko.sync.repositories.domain.PasswordRecordFactory; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -public class PasswordsServerSyncStage extends ServerSyncStage { - @Override - protected String getCollection() { - return "passwords"; - } - - @Override - protected String getEngineName() { - return "passwords"; - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.PASSWORDS_ENGINE_VERSION; - } - - @Override - protected Repository getLocalRepository() { - return new PasswordsRepositorySession.PasswordsRepository(); - } - - @Override - protected RecordFactory getRecordFactory() { - return new PasswordRecordFactory(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java deleted file mode 100644 index 733c887f0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SafeConstrainedServer11Repository.java +++ /dev/null @@ -1,110 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.InfoCounts; -import org.mozilla.gecko.sync.JSONRecordFetcher; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.Server11RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -/** - * This is a constrained repository -- one which fetches a limited number - * of records -- that additionally refuses to sync if the limit will - * be exceeded on a first sync by the number of records on the server. - * - * You must pass an {@link InfoCounts} instance, which will be interrogated - * in the event of a first sync. - * - * "First sync" means that our sync timestamp is not greater than zero. - */ -public class SafeConstrainedServer11Repository extends ConstrainedServer11Repository { - - // This can be lazily evaluated if we need it. - private final JSONRecordFetcher countFetcher; - - public SafeConstrainedServer11Repository(String collection, - String storageURL, - AuthHeaderProvider authHeaderProvider, - InfoCollections infoCollections, - InfoConfiguration infoConfiguration, - long batchLimit, - long totalLimit, - String sort, - JSONRecordFetcher countFetcher) - throws URISyntaxException { - super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration, - batchLimit, totalLimit, sort); - if (countFetcher == null) { - throw new IllegalArgumentException("countFetcher must not be null"); - } - this.countFetcher = countFetcher; - } - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - delegate.onSessionCreated(new CountCheckingServer11RepositorySession(this, this.getDefaultBatchLimit())); - } - - public class CountCheckingServer11RepositorySession extends Server11RepositorySession { - private static final String LOG_TAG = "CountCheckingServer11RepositorySession"; - - /** - * The session will report no data available if this is a first sync - * and the server has more data available than this limit. - */ - private final long fetchLimit; - - public CountCheckingServer11RepositorySession(Repository repository, long fetchLimit) { - super(repository); - this.fetchLimit = fetchLimit; - } - - @Override - public boolean shouldSkip() { - // If this is a first sync, verify that we aren't going to blow through our limit. - final long lastSyncTimestamp = getLastSyncTimestamp(); - if (lastSyncTimestamp > 0) { - Logger.info(LOG_TAG, "Collection " + collection + " has already had a first sync: " + - "timestamp is " + lastSyncTimestamp + "; " + - "ignoring any updated counts and syncing as usual."); - } else { - Logger.info(LOG_TAG, "Collection " + collection + " is starting a first sync; checking counts."); - - final InfoCounts counts; - try { - // This'll probably be the same object, but best to obey the API. - counts = new InfoCounts(countFetcher.fetchBlocking()); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Skipping " + collection + " until we can fetch counts.", e); - return true; - } - - Integer c = counts.getCount(collection); - if (c == null) { - Logger.info(LOG_TAG, "Fetched counts does not include collection " + collection + "; syncing as usual."); - return false; - } - - Logger.info(LOG_TAG, "First sync for " + collection + ": " + c + " items."); - if (c > fetchLimit) { - Logger.warn(LOG_TAG, "Too many items to sync safely. Skipping."); - return true; - } - } - return super.shouldSkip(); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java deleted file mode 100644 index 733e69da5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java +++ /dev/null @@ -1,627 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import android.content.Context; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.EngineSettings; -import org.mozilla.gecko.sync.GlobalSession; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.MetaGlobalException; -import org.mozilla.gecko.sync.NoCollectionKeysSetException; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.SynchronizerConfiguration; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.WipeServerDelegate; -import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.SyncStorageRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; -import org.mozilla.gecko.sync.repositories.Server11Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer; -import org.mozilla.gecko.sync.synchronizer.Synchronizer; -import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; -import org.mozilla.gecko.sync.synchronizer.SynchronizerSession; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Map; -import java.util.concurrent.ExecutorService; - -/** - * Fetch from a server collection into a local repository, encrypting - * and decrypting along the way. - * - * @author rnewman - * - */ -public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage implements SynchronizerDelegate { - - protected static final String LOG_TAG = "ServerSyncStage"; - - protected long stageStartTimestamp = -1; - protected long stageCompleteTimestamp = -1; - - /** - * Override these in your subclasses. - * - * @return true if this stage should be executed. - * @throws MetaGlobalException - */ - protected boolean isEnabled() throws MetaGlobalException { - EngineSettings engineSettings = null; - try { - engineSettings = getEngineSettings(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Unable to get engine settings for " + this + ": fetching config failed.", e); - // Fall through; null engineSettings will pass below. - } - - // We can be disabled by the server's meta/global record, or malformed in the server's meta/global record, - // or by the user manually in Sync Settings. - // We catch the subclasses of MetaGlobalException to trigger various resets and wipes in execute(). - boolean enabledInMetaGlobal = session.isEngineRemotelyEnabled(this.getEngineName(), engineSettings); - - // Check for manual changes to engines by the user. - checkAndUpdateUserSelectedEngines(enabledInMetaGlobal); - - // Check for changes on the server. - if (!enabledInMetaGlobal) { - Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled by server meta/global."); - return false; - } - - // We can also be disabled just for this sync. - boolean enabledThisSync = session.isEngineLocallyEnabled(this.getEngineName()); // For ServerSyncStage, stage name == engine name. - if (!enabledThisSync) { - Logger.debug(LOG_TAG, "Stage " + this.getEngineName() + " disabled just for this sync."); - } - return enabledThisSync; - } - - /** - * Compares meta/global engine state to user selected engines from Sync - * Settings and throws an exception if they don't match and meta/global needs - * to be updated. - * - * @param enabledInMetaGlobal - * boolean of engine sync state in meta/global - * @throws MetaGlobalException - * if engine sync state has been changed in Sync Settings, with new - * engine sync state. - */ - protected void checkAndUpdateUserSelectedEngines(boolean enabledInMetaGlobal) throws MetaGlobalException { - Map<String, Boolean> selectedEngines = session.config.userSelectedEngines; - String thisEngine = this.getEngineName(); - - if (selectedEngines != null && selectedEngines.containsKey(thisEngine)) { - boolean enabledInSelection = selectedEngines.get(thisEngine); - if (enabledInMetaGlobal != enabledInSelection) { - // Engine enable state has been changed by the user. - Logger.debug(LOG_TAG, "Engine state has been changed by user. Throwing exception."); - throw new MetaGlobalException.MetaGlobalEngineStateChangedException(enabledInSelection); - } - } - } - - protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException { - Integer version = getStorageVersion(); - if (version == null) { - Logger.warn(LOG_TAG, "null storage version for " + this + "; using version 0."); - version = 0; - } - - SynchronizerConfiguration config = this.getConfig(); - if (config == null) { - return new EngineSettings(null, version); - } - return new EngineSettings(config.syncID, version); - } - - protected abstract String getCollection(); - protected abstract String getEngineName(); - protected abstract Repository getLocalRepository(); - protected abstract RecordFactory getRecordFactory(); - - // Override this in subclasses. - protected Repository getRemoteRepository() throws URISyntaxException { - String collection = getCollection(); - return new Server11Repository(collection, - session.config.storageURL(), - session.getAuthHeaderProvider(), - session.config.infoCollections, - session.config.infoConfiguration); - } - - /** - * Return a Crypto5Middleware-wrapped Server11Repository. - * - * @throws NoCollectionKeysSetException - * @throws URISyntaxException - */ - protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException { - String collection = this.getCollection(); - KeyBundle collectionKey = session.keyBundleForCollection(collection); - Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(getRemoteRepository(), collectionKey); - cryptoRepo.recordFactory = getRecordFactory(); - return cryptoRepo; - } - - protected String bundlePrefix() { - return this.getCollection() + "."; - } - - protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException { - return new SynchronizerConfiguration(session.config.getBranch(bundlePrefix())); - } - - protected void persistConfig(SynchronizerConfiguration synchronizerConfiguration) { - synchronizerConfiguration.persist(session.config.getBranch(bundlePrefix())); - } - - public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException { - Repository remote = wrappedServerRepo(); - - Synchronizer synchronizer = new ServerLocalSynchronizer(); - synchronizer.repositoryA = remote; - synchronizer.repositoryB = this.getLocalRepository(); - synchronizer.load(getConfig()); - - return synchronizer; - } - - /** - * Reset timestamps. - */ - @Override - protected void resetLocal() { - resetLocalWithSyncID(null); - } - - /** - * Reset timestamps and possibly set syncID. - * @param syncID if non-null, new syncID to persist. - */ - protected void resetLocalWithSyncID(String syncID) { - // Clear both timestamps. - SynchronizerConfiguration config; - try { - config = this.getConfig(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Unable to reset " + this + ": fetching config failed.", e); - return; - } - - if (syncID != null) { - config.syncID = syncID; - Logger.info(LOG_TAG, "Setting syncID for " + this + " to '" + syncID + "'."); - } - config.localBundle.setTimestamp(0L); - config.remoteBundle.setTimestamp(0L); - persistConfig(config); - Logger.info(LOG_TAG, "Reset timestamps for " + this); - } - - // Not thread-safe. Use with caution. - private class WipeWaiter { - public boolean sessionSucceeded = true; - public boolean wipeSucceeded = true; - public Exception error; - - public void notify(Exception e, boolean sessionSucceeded) { - this.sessionSucceeded = sessionSucceeded; - this.wipeSucceeded = false; - this.error = e; - this.notify(); - } - } - - /** - * Synchronously wipe this stage by instantiating a local repository session - * and wiping that. - * <p> - * Logs and re-throws an exception on failure. - */ - @Override - protected void wipeLocal() throws Exception { - // Reset, then clear data. - this.resetLocal(); - - final WipeWaiter monitor = new WipeWaiter(); - final Context context = session.getContext(); - final Repository r = this.getLocalRepository(); - - final Runnable doWipe = new Runnable() { - @Override - public void run() { - r.createSession(new RepositorySessionCreationDelegate() { - - @Override - public void onSessionCreated(final RepositorySession session) { - try { - session.begin(new RepositorySessionBeginDelegate() { - - @Override - public void onBeginSucceeded(final RepositorySession session) { - session.wipe(new RepositorySessionWipeDelegate() { - @Override - public void onWipeSucceeded() { - try { - session.finish(new RepositorySessionFinishDelegate() { - - @Override - public void onFinishSucceeded(RepositorySession session, - RepositorySessionBundle bundle) { - // Hurrah. - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void onFinishFailed(Exception ex) { - // Assume that no finish => no wipe. - synchronized (monitor) { - monitor.notify(ex, true); - } - } - - @Override - public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) { - return this; - } - }); - } catch (InactiveSessionException e) { - // Cannot happen. Call for safety. - synchronized (monitor) { - monitor.notify(e, true); - } - } - } - - @Override - public void onWipeFailed(Exception ex) { - session.abort(); - synchronized (monitor) { - monitor.notify(ex, true); - } - } - - @Override - public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor) { - return this; - } - }); - } - - @Override - public void onBeginFailed(Exception ex) { - session.abort(); - synchronized (monitor) { - monitor.notify(ex, true); - } - } - - @Override - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) { - return this; - } - }); - } catch (InvalidSessionTransitionException e) { - session.abort(); - synchronized (monitor) { - monitor.notify(e, true); - } - } - } - - @Override - public void onSessionCreateFailed(Exception ex) { - synchronized (monitor) { - monitor.notify(ex, false); - } - } - - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - return this; - } - }, context); - } - }; - - final Thread wiping = new Thread(doWipe); - synchronized (monitor) { - wiping.start(); - try { - monitor.wait(); - } catch (InterruptedException e) { - Logger.error(LOG_TAG, "Wipe interrupted."); - } - } - - if (!monitor.sessionSucceeded) { - Logger.error(LOG_TAG, "Failed to create session for wipe."); - throw monitor.error; - } - - if (!monitor.wipeSucceeded) { - Logger.error(LOG_TAG, "Failed to wipe session."); - throw monitor.error; - } - - Logger.info(LOG_TAG, "Wiping stage complete."); - } - - /** - * Asynchronously wipe collection on server. - */ - protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) { - SyncStorageRequest request; - - try { - request = new SyncStorageRequest(session.config.collectionURI(getCollection())); - } catch (URISyntaxException ex) { - Logger.warn(LOG_TAG, "Invalid URI in wipeServer."); - wipeDelegate.onWipeFailed(ex); - return; - } - - request.delegate = new SyncStorageRequestDelegate() { - - @Override - public String ifUnmodifiedSince() { - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - BaseResource.consumeEntity(response); - resetLocal(); - wipeDelegate.onWiped(response.normalizedWeaveTimestamp()); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - Logger.warn(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer."); - // Process HTTP failures here to pick up backoffs, etc. - session.interpretHTTPFailure(response.httpResponse()); - BaseResource.consumeEntity(response); // The exception thrown should not need the body of the response. - wipeDelegate.onWipeFailed(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(Exception ex) { - Logger.warn(LOG_TAG, "Got exception in wipeServer.", ex); - wipeDelegate.onWipeFailed(ex); - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - }; - - request.delete(); - } - - /** - * Synchronously wipe the server. - * <p> - * Logs and re-throws an exception on failure. - */ - public void wipeServer(final GlobalSession session) throws Exception { - this.session = session; - - final WipeWaiter monitor = new WipeWaiter(); - - final Runnable doWipe = new Runnable() { - @Override - public void run() { - wipeServer(session.getAuthHeaderProvider(), new WipeServerDelegate() { - @Override - public void onWiped(long timestamp) { - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void onWipeFailed(Exception e) { - synchronized (monitor) { - monitor.notify(e, false); - } - } - }); - } - }; - - final Thread wiping = new Thread(doWipe); - synchronized (monitor) { - wiping.start(); - try { - monitor.wait(); - } catch (InterruptedException e) { - Logger.error(LOG_TAG, "Server wipe interrupted."); - } - } - - if (!monitor.wipeSucceeded) { - Logger.error(LOG_TAG, "Failed to wipe server."); - throw monitor.error; - } - - Logger.info(LOG_TAG, "Wiping server complete."); - } - - @Override - public void execute() throws NoSuchStageException { - final String name = getEngineName(); - Logger.debug(LOG_TAG, "Starting execute for " + name); - - stageStartTimestamp = System.currentTimeMillis(); - - try { - if (!this.isEnabled()) { - Logger.info(LOG_TAG, "Skipping stage " + name + "."); - session.advance(); - return; - } - } catch (MetaGlobalException.MetaGlobalMalformedSyncIDException e) { - // Bad engine syncID. This should never happen. Wipe the server. - try { - session.recordForMetaGlobalUpdate(name, new EngineSettings(Utils.generateGuid(), this.getStorageVersion())); - Logger.info(LOG_TAG, "Wiping server because malformed engine sync ID was found in meta/global."); - wipeServer(session); - Logger.info(LOG_TAG, "Wiped server after malformed engine sync ID found in meta/global."); - } catch (Exception ex) { - session.abort(ex, "Failed to wipe server after malformed engine sync ID found in meta/global."); - } - } catch (MetaGlobalException.MetaGlobalMalformedVersionException e) { - // Bad engine version. This should never happen. Wipe the server. - try { - session.recordForMetaGlobalUpdate(name, new EngineSettings(Utils.generateGuid(), this.getStorageVersion())); - Logger.info(LOG_TAG, "Wiping server because malformed engine version was found in meta/global."); - wipeServer(session); - Logger.info(LOG_TAG, "Wiped server after malformed engine version found in meta/global."); - } catch (Exception ex) { - session.abort(ex, "Failed to wipe server after malformed engine version found in meta/global."); - } - } catch (MetaGlobalException.MetaGlobalStaleClientSyncIDException e) { - // Our syncID is wrong. Reset client and take the server syncID. - Logger.warn(LOG_TAG, "Remote engine syncID different from local engine syncID:" + - " resetting local engine and assuming remote engine syncID."); - this.resetLocalWithSyncID(e.serverSyncID); - } catch (MetaGlobalException.MetaGlobalEngineStateChangedException e) { - boolean isEnabled = e.isEnabled; - if (!isEnabled) { - // Engine has been disabled; update meta/global with engine removal for upload. - session.removeEngineFromMetaGlobal(name); - session.config.declinedEngineNames.add(name); - } else { - session.config.declinedEngineNames.remove(name); - // Add engine with new syncID to meta/global for upload. - String newSyncID = Utils.generateGuid(); - session.recordForMetaGlobalUpdate(name, new EngineSettings(newSyncID, this.getStorageVersion())); - // Update SynchronizerConfiguration w/ new engine syncID. - this.resetLocalWithSyncID(newSyncID); - } - try { - // Engine sync status has changed. Wipe server. - Logger.warn(LOG_TAG, "Wiping server because engine sync state changed."); - wipeServer(session); - Logger.warn(LOG_TAG, "Wiped server because engine sync state changed."); - } catch (Exception ex) { - session.abort(ex, "Failed to wipe server after engine sync state changed"); - } - if (!isEnabled) { - Logger.warn(LOG_TAG, "Stage has been disabled. Advancing to next stage."); - session.advance(); - return; - } - } catch (MetaGlobalException e) { - session.abort(e, "Inappropriate meta/global; refusing to execute " + name + " stage."); - return; - } - - Synchronizer synchronizer; - try { - synchronizer = this.getConfiguredSynchronizer(session); - } catch (NoCollectionKeysSetException e) { - session.abort(e, "No CollectionKeys."); - return; - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI syntax for server repository."); - return; - } catch (NonObjectJSONException | IOException e) { - session.abort(e, "Invalid persisted JSON for config."); - return; - } - - Logger.debug(LOG_TAG, "Invoking synchronizer."); - synchronizer.synchronize(session.getContext(), this); - Logger.debug(LOG_TAG, "Reached end of execute."); - } - - /** - * Express the duration taken by this stage as a String, like "0.56 seconds". - * - * @return formatted string. - */ - protected String getStageDurationString() { - return Utils.formatDuration(stageStartTimestamp, stageCompleteTimestamp); - } - - /** - * We synced this engine! Persist timestamps and advance the session. - * - * @param synchronizer the <code>Synchronizer</code> that succeeded. - */ - @Override - public void onSynchronized(Synchronizer synchronizer) { - stageCompleteTimestamp = System.currentTimeMillis(); - Logger.debug(LOG_TAG, "onSynchronized."); - - SynchronizerConfiguration newConfig = synchronizer.save(); - if (newConfig != null) { - persistConfig(newConfig); - } else { - Logger.warn(LOG_TAG, "Didn't get configuration from synchronizer after success."); - } - - final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession(); - int inboundCount = synchronizerSession.getInboundCount(); - int outboundCount = synchronizerSession.getOutboundCount(); - Logger.info(LOG_TAG, "Stage " + getEngineName() + - " received " + inboundCount + " and sent " + outboundCount + - " records in " + getStageDurationString() + "."); - Logger.info(LOG_TAG, "Advancing session."); - session.advance(); - } - - /** - * We failed to sync this engine! Do not persist timestamps (which means that - * the next sync will include this sync's data), but do advance the session - * (if we didn't get a Retry-After header). - * - * @param synchronizer the <code>Synchronizer</code> that failed. - */ - @Override - public void onSynchronizeFailed(Synchronizer synchronizer, - Exception lastException, String reason) { - stageCompleteTimestamp = System.currentTimeMillis(); - Logger.warn(LOG_TAG, "Synchronize failed: " + reason, lastException); - - // This failure could be due to a 503 or a 401 and it could have headers. - // Interrogate the headers but only abort the global session if Retry-After header is set. - if (lastException instanceof HTTPFailureException) { - SyncStorageResponse response = ((HTTPFailureException)lastException).response; - if (response.retryAfterInSeconds() > 0) { - session.handleHTTPError(response, reason); // Calls session.abort(). - return; - } else { - session.interpretHTTPFailure(response.httpResponse()); // Does not call session.abort(). - } - } - - Logger.info(LOG_TAG, "Advancing session even though stage failed (took " + getStageDurationString() + - "). Timestamps not persisted."); - session.advance(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java deleted file mode 100644 index 04d3e7ce2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java +++ /dev/null @@ -1,691 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - -import android.accounts.Account; -import android.content.Context; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.FxAccountClient; -import org.mozilla.gecko.background.fxa.FxAccountClient20; -import org.mozilla.gecko.background.fxa.FxAccountClientException; -import org.mozilla.gecko.fxa.FirefoxAccounts; -import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; -import org.mozilla.gecko.sync.CommandProcessor; -import org.mozilla.gecko.sync.CommandProcessor.Command; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.NoCollectionKeysSetException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.crypto.CryptoException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; -import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate; -import org.mozilla.gecko.sync.net.WBORequestDelegate; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.repositories.domain.ClientRecordFactory; -import org.mozilla.gecko.sync.repositories.domain.VersionConstants; - -import ch.boye.httpclientandroidlib.HttpStatus; - -public class SyncClientsEngineStage extends AbstractSessionManagingSyncStage { - private static final String LOG_TAG = "SyncClientsEngineStage"; - - public static final String COLLECTION_NAME = "clients"; - public static final String STAGE_NAME = COLLECTION_NAME; - public static final int CLIENTS_TTL_REFRESH = 604800000; // 7 days in milliseconds. - public static final int MAX_UPLOAD_FAILURE_COUNT = 5; - public static final long NOTIFY_TAB_SENT_TTL_SECS = TimeUnit.SECONDS.convert(1L, TimeUnit.HOURS); // 1 hour - - protected final ClientRecordFactory factory = new ClientRecordFactory(); - protected ClientUploadDelegate clientUploadDelegate; - protected ClientDownloadDelegate clientDownloadDelegate; - - // Be sure to use this safely via getClientsDatabaseAccessor/closeDataAccessor. - protected ClientsDatabaseAccessor db; - - protected volatile boolean shouldWipe; - protected volatile boolean shouldUploadLocalRecord; // Set if, e.g., we received commands or need to refresh our version. - protected final AtomicInteger uploadAttemptsCount = new AtomicInteger(); - protected final List<ClientRecord> modifiedClientsToUpload = new ArrayList<ClientRecord>(); - - protected int getClientsCount() { - return getClientsDatabaseAccessor().clientsCount(); - } - - protected synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() { - if (db == null) { - db = new ClientsDatabaseAccessor(session.getContext()); - } - return db; - } - - protected synchronized void closeDataAccessor() { - if (db == null) { - return; - } - db.close(); - db = null; - } - - /** - * The following two delegates, ClientDownloadDelegate and ClientUploadDelegate - * are both triggered in a chain, starting when execute() calls - * downloadClientRecords(). - * - * Client records are downloaded using a get() request. Upon success of the - * get() request, the local client record is uploaded. - * - * @author Marina Samuel - * - */ - public class ClientDownloadDelegate extends WBOCollectionRequestDelegate { - - // We use this on each WBO, so lift it out. - final ClientsDataDelegate clientsDelegate = session.getClientsDelegate(); - boolean localAccountGUIDDownloaded = false; - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return session.getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - // TODO last client download time? - return null; - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - - // Hang onto the server's last modified timestamp to use - // in X-If-Unmodified-Since for upload. - session.config.persistServerClientsTimestamp(response.normalizedWeaveTimestamp()); - BaseResource.consumeEntity(response); - - // Wipe the clients table if it still hasn't been wiped but needs to be. - wipeAndStore(null); - - // If we successfully downloaded all records but ours was not one of them - // then reset the timestamp. - if (!localAccountGUIDDownloaded) { - Logger.info(LOG_TAG, "Local client GUID does not exist on the server. Upload timestamp will be reset."); - session.config.persistServerClientRecordTimestamp(0); - } - localAccountGUIDDownloaded = false; - - final int clientsCount; - try { - clientsCount = getClientsCount(); - } finally { - // Close the database to clear cached readableDatabase/writableDatabase - // after we've completed our last transaction (db.store()). - closeDataAccessor(); - } - - Logger.debug(LOG_TAG, "Database contains " + clientsCount + " clients."); - Logger.debug(LOG_TAG, "Server response asserts " + response.weaveRecords() + " records."); - - // TODO: persist the response timestamp to know whether to download next time (Bug 726055). - clientUploadDelegate = new ClientUploadDelegate(); - clientsDelegate.setClientsCount(clientsCount); - - // If we upload remote records, checkAndUpload() will be called upon - // upload success in the delegate. Otherwise call checkAndUpload() now. - if (modifiedClientsToUpload.size() > 0) { - // modifiedClientsToUpload is cleared in uploadRemoteRecords, save what we need here - final List<String> devicesToNotify = new ArrayList<>(); - for (ClientRecord record : modifiedClientsToUpload) { - if (!TextUtils.isEmpty(record.fxaDeviceId)) { - devicesToNotify.add(record.fxaDeviceId); - } - } - - // This method is synchronous, there's no risk of notifying the clients - // before we actually uploaded the records - uploadRemoteRecords(); - - // Notify the clients who got their record written - notifyClients(devicesToNotify); - - return; - } - checkAndUpload(); - } - - private void notifyClients(final List<String> devicesToNotify) { - final ExecutorService executor = Executors.newSingleThreadExecutor(); - final Context context = session.getContext(); - final Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account == null) { - Log.e(LOG_TAG, "Can't notify other clients: no account"); - return; - } - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - final ExtendedJSONObject payload = createNotifyDevicesPayload(); - - final byte[] sessionToken; - try { - sessionToken = fxAccount.getSessionToken(); - } catch (AndroidFxAccount.InvalidFxAState invalidFxAState) { - Log.e(LOG_TAG, "Could not get session token", invalidFxAState); - return; - } - - // API doc : https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountdevicesnotify - final FxAccountClient fxAccountClient = new FxAccountClient20(fxAccount.getAccountServerURI(), executor); - fxAccountClient.notifyDevices(sessionToken, devicesToNotify, payload, NOTIFY_TAB_SENT_TTL_SECS, new FxAccountClient20.RequestDelegate<ExtendedJSONObject>() { - @Override - public void handleError(Exception e) { - Log.e(LOG_TAG, "Error while notifying devices", e); - } - - @Override - public void handleFailure(FxAccountClientException.FxAccountClientRemoteException e) { - Log.e(LOG_TAG, "Error while notifying devices", e); - } - - @Override - public void handleSuccess(ExtendedJSONObject result) { - Log.i(LOG_TAG, devicesToNotify.size() + " devices notified"); - } - }); - } - - @NonNull - @SuppressWarnings("unchecked") - private ExtendedJSONObject createNotifyDevicesPayload() { - final ExtendedJSONObject payload = new ExtendedJSONObject(); - payload.put("version", 1); - payload.put("command", "sync:collection_changed"); - final ExtendedJSONObject data = new ExtendedJSONObject(); - final JSONArray collections = new JSONArray(); - collections.add("clients"); - data.put("collections", collections); - payload.put("data", data); - return payload; - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - BaseResource.consumeEntity(response); // We don't need the response at all, and any exception handling shouldn't need the response body. - localAccountGUIDDownloaded = false; - - try { - Logger.info(LOG_TAG, "Client upload failed. Aborting sync."); - session.abort(new HTTPFailureException(response), "Client download failed."); - } finally { - // Close the database upon failure. - closeDataAccessor(); - } - } - - @Override - public void handleRequestError(Exception ex) { - localAccountGUIDDownloaded = false; - try { - Logger.info(LOG_TAG, "Client upload error. Aborting sync."); - session.abort(ex, "Failure fetching client record."); - } finally { - // Close the database upon error. - closeDataAccessor(); - } - } - - @Override - public void handleWBO(CryptoRecord record) { - ClientRecord r; - try { - r = (ClientRecord) factory.createRecord(record.decrypt()); - if (clientsDelegate.isLocalGUID(r.guid)) { - Logger.info(LOG_TAG, "Local client GUID exists on server and was downloaded."); - localAccountGUIDDownloaded = true; - handleDownloadedLocalRecord(r); - } else { - // Only need to store record if it isn't our local one. - wipeAndStore(r); - addCommands(r); - } - RepoUtils.logClient(r); - } catch (Exception e) { - session.abort(e, "Exception handling client WBO."); - return; - } - } - - @Override - public KeyBundle keyBundle() { - try { - return session.keyBundleForCollection(COLLECTION_NAME); - } catch (NoCollectionKeysSetException e) { - return null; - } - } - } - - public class ClientUploadDelegate extends WBORequestDelegate { - protected static final String LOG_TAG = "ClientUploadDelegate"; - public Long currentlyUploadingRecordTimestamp; - public boolean currentlyUploadingLocalRecord; - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return session.getAuthHeaderProvider(); - } - - private void setUploadDetails(boolean isLocalRecord) { - // Use the timestamp for the whole collection per Sync storage 1.1 spec. - currentlyUploadingRecordTimestamp = session.config.getPersistedServerClientsTimestamp(); - currentlyUploadingLocalRecord = isLocalRecord; - } - - @Override - public String ifUnmodifiedSince() { - Long timestampInMilliseconds = currentlyUploadingRecordTimestamp; - - // It's the first upload so we don't care about X-If-Unmodified-Since. - if (timestampInMilliseconds <= 0) { - return null; - } - - return Utils.millisecondsToDecimalSecondsString(timestampInMilliseconds); - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - Logger.debug(LOG_TAG, "Upload succeeded."); - uploadAttemptsCount.set(0); - - // X-Weave-Timestamp is the modified time of uploaded records. - // Always persist this. - final long responseTimestamp = response.normalizedWeaveTimestamp(); - Logger.trace(LOG_TAG, "Timestamp from header is: " + responseTimestamp); - - if (responseTimestamp == -1) { - final String message = "Response did not contain a valid timestamp."; - session.abort(new RuntimeException(message), message); - return; - } - - BaseResource.consumeEntity(response); - session.config.persistServerClientsTimestamp(responseTimestamp); - - // If we're not uploading our record, we're done here; just - // clean up and finish. - if (!currentlyUploadingLocalRecord) { - // TODO: check failed uploads in body. - clearRecordsToUpload(); - checkAndUpload(); - return; - } - - // If we're processing our record, we have a little more cleanup - // to do. - shouldUploadLocalRecord = false; - session.config.persistServerClientRecordTimestamp(responseTimestamp); - session.advance(); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - int statusCode = response.getStatusCode(); - - // If upload failed because of `ifUnmodifiedSince` then there are new - // commands uploaded to our record. We must download and process them first. - if (!shouldUploadLocalRecord || - statusCode == HttpStatus.SC_PRECONDITION_FAILED || - uploadAttemptsCount.incrementAndGet() > MAX_UPLOAD_FAILURE_COUNT) { - - Logger.debug(LOG_TAG, "Client upload failed. Aborting sync."); - if (!currentlyUploadingLocalRecord) { - modifiedClientsToUpload.clear(); // These will be redownloaded. - } - BaseResource.consumeEntity(response); // The exception thrown should need the response body. - session.abort(new HTTPFailureException(response), "Client upload failed."); - return; - } - Logger.trace(LOG_TAG, "Retrying upload…"); - // Preconditions: - // shouldUploadLocalRecord == true && - // statusCode != 412 && - // uploadAttemptCount < MAX_UPLOAD_FAILURE_COUNT - checkAndUpload(); - } - - @Override - public void handleRequestError(Exception ex) { - Logger.info(LOG_TAG, "Client upload error. Aborting sync."); - session.abort(ex, "Client upload failed."); - } - - @Override - public KeyBundle keyBundle() { - try { - return session.keyBundleForCollection(COLLECTION_NAME); - } catch (NoCollectionKeysSetException e) { - return null; - } - } - } - - @Override - public void execute() throws NoSuchStageException { - // We can be disabled just for this sync. - boolean enabledThisSync = session.isEngineLocallyEnabled(STAGE_NAME); - if (!enabledThisSync) { - // These log messages look best when they match the messages in ServerSyncStage. - Logger.debug(LOG_TAG, "Stage " + STAGE_NAME + " disabled just for this sync."); - Logger.info(LOG_TAG, "Skipping stage " + STAGE_NAME + "."); - session.advance(); - return; - } - - if (shouldDownload()) { - downloadClientRecords(); // Will kick off upload, too… - } else { - // Upload if necessary. - } - } - - @Override - protected void resetLocal() { - // Clear timestamps and local data. - session.config.persistServerClientRecordTimestamp(0L); // TODO: roll these into one. - session.config.persistServerClientsTimestamp(0L); - - session.getClientsDelegate().setClientsCount(0); - try { - getClientsDatabaseAccessor().wipeDB(); - } finally { - closeDataAccessor(); - } - } - - @Override - protected void wipeLocal() throws Exception { - // Nothing more to do. - this.resetLocal(); - } - - @Override - public Integer getStorageVersion() { - return VersionConstants.CLIENTS_ENGINE_VERSION; - } - - protected String getLocalClientVersion() { - return AppConstants.MOZ_APP_VERSION; - } - - @SuppressWarnings("unchecked") - protected JSONArray getLocalClientProtocols() { - final JSONArray protocols = new JSONArray(); - protocols.add(ClientRecord.PROTOCOL_LEGACY_SYNC); - protocols.add(ClientRecord.PROTOCOL_FXA_SYNC); - return protocols; - } - - protected ClientRecord newLocalClientRecord(ClientsDataDelegate delegate) { - final String ourGUID = delegate.getAccountGUID(); - final String ourName = delegate.getClientName(); - - ClientRecord r = new ClientRecord(ourGUID); - r.name = ourName; - r.version = getLocalClientVersion(); - r.protocols = getLocalClientProtocols(); - - r.os = "Android"; - r.application = AppConstants.MOZ_APP_DISPLAYNAME; - r.appPackage = AppConstants.ANDROID_PACKAGE_NAME; - r.device = android.os.Build.MODEL; - r.formfactor = delegate.getFormFactor(); - - Context context = session.getContext(); - final Account account = FirefoxAccounts.getFirefoxAccount(context); - if (account != null) { - final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account); - final String deviceId = fxAccount.getDeviceId(); - if (!TextUtils.isEmpty(deviceId)) { - r.fxaDeviceId = deviceId; - } - } - - return r; - } - - // TODO: Bug 726055 - More considered handling of when to sync. - protected boolean shouldDownload() { - // Ask info/collections whether a download is needed. - return true; - } - - protected boolean shouldUpload() { - if (shouldUploadLocalRecord) { - return true; - } - - long lastUpload = session.config.getPersistedServerClientRecordTimestamp(); // Defaults to 0. - if (lastUpload == 0) { - return true; - } - - if (session.getClientsDelegate().getLastModifiedTimestamp() > lastUpload) { - // Something's changed locally since we last uploaded. - return true; - } - - // Note the opportunity for clock drift problems here. - // TODO: if we track download times, we can use the timestamp of most - // recent download response instead of the current time. - long now = System.currentTimeMillis(); - long age = now - lastUpload; - return age >= CLIENTS_TTL_REFRESH; - } - - protected void handleDownloadedLocalRecord(ClientRecord r) { - session.config.persistServerClientRecordTimestamp(r.lastModified); - - if (!getLocalClientVersion().equals(r.version) || - !getLocalClientProtocols().equals(r.protocols)) { - shouldUploadLocalRecord = true; - } - processCommands(r.commands); - } - - protected void processCommands(JSONArray commands) { - if (commands == null || - commands.size() == 0) { - return; - } - - shouldUploadLocalRecord = true; - CommandProcessor processor = CommandProcessor.getProcessor(); - - for (Object o : commands) { - processor.processCommand(session, new ExtendedJSONObject((JSONObject) o)); - } - } - - @SuppressWarnings("unchecked") - protected void addCommands(ClientRecord record) throws NullCursorException { - Logger.trace(LOG_TAG, "Adding commands to " + record.guid); - List<Command> commands = db.fetchCommandsForClient(record.guid); - - if (commands == null || commands.size() == 0) { - Logger.trace(LOG_TAG, "No commands to add."); - return; - } - - for (Command command : commands) { - JSONObject jsonCommand = command.asJSONObject(); - if (record.commands == null) { - record.commands = new JSONArray(); - } - record.commands.add(jsonCommand); - } - modifiedClientsToUpload.add(record); - } - - @SuppressWarnings("unchecked") - protected void uploadRemoteRecords() { - Logger.trace(LOG_TAG, "In uploadRemoteRecords. Uploading " + modifiedClientsToUpload.size() + " records" ); - - for (ClientRecord r : modifiedClientsToUpload) { - Logger.trace(LOG_TAG, ">> Uploading record " + r.guid + ": " + r.name); - } - - if (modifiedClientsToUpload.size() == 1) { - ClientRecord record = modifiedClientsToUpload.get(0); - Logger.debug(LOG_TAG, "Only 1 remote record to upload."); - Logger.debug(LOG_TAG, "Record last modified: " + record.lastModified); - CryptoRecord cryptoRecord = encryptClientRecord(record); - if (cryptoRecord != null) { - clientUploadDelegate.setUploadDetails(false); - this.uploadClientRecord(cryptoRecord); - } - return; - } - - JSONArray cryptoRecords = new JSONArray(); - for (ClientRecord record : modifiedClientsToUpload) { - Logger.trace(LOG_TAG, "Record " + record.guid + " is being uploaded" ); - - CryptoRecord cryptoRecord = encryptClientRecord(record); - cryptoRecords.add(cryptoRecord.toJSONObject()); - } - Logger.debug(LOG_TAG, "Uploading records: " + cryptoRecords.size()); - clientUploadDelegate.setUploadDetails(false); - this.uploadClientRecords(cryptoRecords); - } - - protected void checkAndUpload() { - if (!shouldUpload()) { - Logger.debug(LOG_TAG, "Not uploading client record."); - session.advance(); - return; - } - - final ClientRecord localClient = newLocalClientRecord(session.getClientsDelegate()); - clientUploadDelegate.setUploadDetails(true); - CryptoRecord cryptoRecord = encryptClientRecord(localClient); - if (cryptoRecord != null) { - this.uploadClientRecord(cryptoRecord); - } - } - - protected CryptoRecord encryptClientRecord(ClientRecord recordToUpload) { - // Generate CryptoRecord from ClientRecord to upload. - final String encryptionFailure = "Couldn't encrypt new client record."; - - try { - CryptoRecord cryptoRecord = recordToUpload.getEnvelope(); - cryptoRecord.keyBundle = clientUploadDelegate.keyBundle(); - if (cryptoRecord.keyBundle == null) { - session.abort(new NoCollectionKeysSetException(), "No collection keys set."); - return null; - } - return cryptoRecord.encrypt(); - } catch (UnsupportedEncodingException e) { - session.abort(e, encryptionFailure + " Unsupported encoding."); - } catch (CryptoException e) { - session.abort(e, encryptionFailure); - } - return null; - } - - public void clearRecordsToUpload() { - try { - getClientsDatabaseAccessor().wipeCommandsTable(); - modifiedClientsToUpload.clear(); - } finally { - closeDataAccessor(); - } - } - - protected void downloadClientRecords() { - shouldWipe = true; - clientDownloadDelegate = makeClientDownloadDelegate(); - - try { - final URI getURI = session.config.collectionURI(COLLECTION_NAME, true); - final SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(getURI); - request.delegate = clientDownloadDelegate; - - Logger.trace(LOG_TAG, "Downloading client records."); - request.get(); - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI."); - } - } - - protected void uploadClientRecords(JSONArray records) { - Logger.trace(LOG_TAG, "Uploading " + records.size() + " client records."); - try { - final URI postURI = session.config.collectionURI(COLLECTION_NAME, false); - final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI); - request.delegate = clientUploadDelegate; - request.post(records); - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI."); - } catch (Exception e) { - session.abort(e, "Unable to parse body."); - } - } - - /** - * Upload a client record via HTTP POST to the parent collection. - */ - protected void uploadClientRecord(CryptoRecord record) { - Logger.debug(LOG_TAG, "Uploading client record " + record.guid); - try { - final URI postURI = session.config.collectionURI(COLLECTION_NAME); - final SyncStorageRecordRequest request = new SyncStorageRecordRequest(postURI); - request.delegate = clientUploadDelegate; - request.post(record); - } catch (URISyntaxException e) { - session.abort(e, "Invalid URI."); - } - } - - protected ClientDownloadDelegate makeClientDownloadDelegate() { - return new ClientDownloadDelegate(); - } - - protected void wipeAndStore(ClientRecord record) { - final ClientsDatabaseAccessor db = getClientsDatabaseAccessor(); - if (shouldWipe) { - db.wipeClientsTable(); - shouldWipe = false; - } - if (record != null) { - db.store(record); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java deleted file mode 100644 index 77846c212..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/UploadMetaGlobalStage.java +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.stage; - - -public class UploadMetaGlobalStage extends AbstractNonRepositorySyncStage { - public static final String LOG_TAG = "UploadMGStage"; - - @Override - public void execute() throws NoSuchStageException { - if (session.hasUpdatedMetaGlobal()) { - session.uploadUpdatedMetaGlobal(); - } - session.advance(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java deleted file mode 100644 index 9b1ef3e85..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ConcurrentRecordConsumer.java +++ /dev/null @@ -1,122 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.domain.Record; - -/** - * Consume records from a queue inside a RecordsChannel, as fast as we can. - * TODO: rewrite this in terms of an ExecutorService and a CompletionService. - * See Bug 713483. - * - * @author rnewman - * - */ -class ConcurrentRecordConsumer extends RecordConsumer { - private static final String LOG_TAG = "CRecordConsumer"; - - /** - * When this is true and all records have been processed, the consumer - * will notify its delegate. - */ - protected boolean allRecordsQueued = false; - private long counter = 0; - - public ConcurrentRecordConsumer(RecordsConsumerDelegate delegate) { - this.delegate = delegate; - } - - private final Object monitor = new Object(); - @Override - public void doNotify() { - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void queueFilled() { - Logger.debug(LOG_TAG, "Queue filled."); - synchronized (monitor) { - this.allRecordsQueued = true; - monitor.notify(); - } - } - - @Override - public void halt() { - synchronized (monitor) { - this.stopImmediately = true; - monitor.notify(); - } - } - - private final Object countMonitor = new Object(); - @Override - public void stored() { - Logger.trace(LOG_TAG, "Record stored. Notifying."); - synchronized (countMonitor) { - counter++; - } - } - - private void consumerIsDone() { - Logger.debug(LOG_TAG, "Consumer is done. Processed " + counter + ((counter == 1) ? " record." : " records.")); - delegate.consumerIsDone(!allRecordsQueued); - } - - @Override - public void run() { - Record record; - - while (true) { - // The queue is concurrent-safe. - while ((record = delegate.getQueue().poll()) != null) { - synchronized (monitor) { - Logger.trace(LOG_TAG, "run() took monitor."); - if (stopImmediately) { - Logger.debug(LOG_TAG, "Stopping immediately. Clearing queue."); - delegate.getQueue().clear(); - Logger.debug(LOG_TAG, "Notifying consumer."); - consumerIsDone(); - return; - } - Logger.debug(LOG_TAG, "run() dropped monitor."); - } - - Logger.trace(LOG_TAG, "Storing record with guid " + record.guid + "."); - try { - delegate.store(record); - } catch (Exception e) { - // TODO: Bug 709371: track records that failed to apply. - Logger.error(LOG_TAG, "Caught error in store.", e); - } - Logger.trace(LOG_TAG, "Done with record."); - } - synchronized (monitor) { - Logger.trace(LOG_TAG, "run() took monitor."); - - if (allRecordsQueued) { - Logger.debug(LOG_TAG, "Done with records and no more to come. Notifying consumerIsDone."); - consumerIsDone(); - return; - } - if (stopImmediately) { - Logger.debug(LOG_TAG, "Done with records and told to stop immediately. Notifying consumerIsDone."); - consumerIsDone(); - return; - } - try { - Logger.debug(LOG_TAG, "Not told to stop but no records. Waiting."); - monitor.wait(10000); - } catch (InterruptedException e) { - // TODO - } - Logger.trace(LOG_TAG, "run() dropped monitor."); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java deleted file mode 100644 index 35e57d9c2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordConsumer.java +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -public abstract class RecordConsumer implements Runnable { - - public abstract void stored(); - - /** - * There are no more store items to arrive at the delegate. - * When you're done, take care of finishing up. - */ - public abstract void queueFilled(); - public abstract void halt(); - - public abstract void doNotify(); - - protected boolean stopImmediately = false; - protected RecordsConsumerDelegate delegate; - - public RecordConsumer() { - super(); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java deleted file mode 100644 index f929cdc75..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannel.java +++ /dev/null @@ -1,292 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -/** - * Pulls records from `source`, applying them to `sink`. - * Notifies its delegate of errors and completion. - * - * All stores (initiated by a fetch) must have been completed before storeDone - * is invoked on the sink. This is to avoid the existing stored items being - * considered as the total set, with onStoreCompleted being called when they're - * done: - * - * store(A) store(B) - * store(C) storeDone() - * store(A) finishes. Store job begins. - * store(C) finishes. Store job begins. - * storeDone() finishes. - * Storing of A complete. - * Storing of C complete. - * We're done! Call onStoreCompleted. - * store(B) finishes... uh oh. - * - * In other words, storeDone must be gated on the synchronous invocation of every store. - * - * Similarly, we require that every store callback have returned before onStoreCompleted is invoked. - * - * This whole set of guarantees should be achievable thusly: - * - * * The fetch process must run in a single thread, and invoke store() - * synchronously. After processing every incoming record, storeDone is called, - * setting a flag. - * If the fetch cannot be implicitly queued, it must be explicitly queued. - * In this implementation, we assume that fetch callbacks are strictly ordered in this way. - * - * * The store process must be (implicitly or explicitly) queued. When the - * queue empties, the consumer checks the storeDone flag. If it's set, and the - * queue is exhausted, invoke onStoreCompleted. - * - * RecordsChannel exists to enforce this ordering of operations. - * - * @author rnewman - * - */ -public class RecordsChannel implements - RepositorySessionFetchRecordsDelegate, - RepositorySessionStoreDelegate, - RecordsConsumerDelegate, - RepositorySessionBeginDelegate { - - private static final String LOG_TAG = "RecordsChannel"; - public RepositorySession source; - public RepositorySession sink; - private final RecordsChannelDelegate delegate; - private long fetchEnd = -1; - - protected final AtomicInteger numFetched = new AtomicInteger(); - protected final AtomicInteger numFetchFailed = new AtomicInteger(); - protected final AtomicInteger numStored = new AtomicInteger(); - protected final AtomicInteger numStoreFailed = new AtomicInteger(); - - public RecordsChannel(RepositorySession source, RepositorySession sink, RecordsChannelDelegate delegate) { - this.source = source; - this.sink = sink; - this.delegate = delegate; - } - - /* - * We push fetched records into a queue. - * A separate thread is waiting for us to notify it of work to do. - * When we tell it to stop, it'll stop. We do that when the fetch - * is completed. - * When it stops, we tell the sink that there are no more records, - * and wait for the sink to tell us that storing is done. - * Then we notify our delegate of completion. - */ - private RecordConsumer consumer; - private boolean waitingForQueueDone = false; - private final ConcurrentLinkedQueue<Record> toProcess = new ConcurrentLinkedQueue<Record>(); - - @Override - public ConcurrentLinkedQueue<Record> getQueue() { - return toProcess; - } - - protected boolean isReady() { - return source.isActive() && sink.isActive(); - } - - /** - * Get the number of records fetched so far. - * - * @return number of fetches. - */ - public int getFetchCount() { - return numFetched.get(); - } - - /** - * Get the number of fetch failures recorded so far. - * - * @return number of fetch failures. - */ - public int getFetchFailureCount() { - return numFetchFailed.get(); - } - - /** - * Get the number of store attempts (successful or not) so far. - * - * @return number of stores attempted. - */ - public int getStoreCount() { - return numStored.get(); - } - - /** - * Get the number of store failures recorded so far. - * - * @return number of store failures. - */ - public int getStoreFailureCount() { - return numStoreFailed.get(); - } - - /** - * Start records flowing through the channel. - */ - public void flow() { - if (!isReady()) { - RepositorySession failed = source; - if (source.isActive()) { - failed = sink; - } - this.delegate.onFlowBeginFailed(this, new SessionNotBegunException(failed)); - return; - } - - if (!source.dataAvailable()) { - Logger.info(LOG_TAG, "No data available: short-circuiting flow from source " + source); - long now = System.currentTimeMillis(); - this.delegate.onFlowCompleted(this, now, now); - return; - } - - sink.setStoreDelegate(this); - numFetched.set(0); - numFetchFailed.set(0); - numStored.set(0); - numStoreFailed.set(0); - // Start a consumer thread. - this.consumer = new ConcurrentRecordConsumer(this); - ThreadPool.run(this.consumer); - waitingForQueueDone = true; - source.fetchSince(source.getLastSyncTimestamp(), this); - } - - /** - * Begin both sessions, invoking flow() when done. - * @throws InvalidSessionTransitionException - */ - public void beginAndFlow() throws InvalidSessionTransitionException { - Logger.trace(LOG_TAG, "Beginning source."); - source.begin(this); - } - - @Override - public void store(Record record) { - numStored.incrementAndGet(); - try { - sink.store(record); - } catch (NoStoreDelegateException e) { - Logger.error(LOG_TAG, "Got NoStoreDelegateException in RecordsChannel.store(). This should not occur. Aborting.", e); - delegate.onFlowStoreFailed(this, e, record.guid); - } - } - - @Override - public void onFetchFailed(Exception ex, Record record) { - Logger.warn(LOG_TAG, "onFetchFailed. Calling for immediate stop.", ex); - numFetchFailed.incrementAndGet(); - this.consumer.halt(); - delegate.onFlowFetchFailed(this, ex); - } - - @Override - public void onFetchedRecord(Record record) { - numFetched.incrementAndGet(); - this.toProcess.add(record); - this.consumer.doNotify(); - } - - @Override - public void onFetchCompleted(final long fetchEnd) { - Logger.trace(LOG_TAG, "onFetchCompleted. Stopping consumer once stores are done."); - Logger.trace(LOG_TAG, "Fetch timestamp is " + fetchEnd); - this.fetchEnd = fetchEnd; - this.consumer.queueFilled(); - } - - @Override - public void onRecordStoreFailed(Exception ex, String recordGuid) { - Logger.trace(LOG_TAG, "Failed to store record with guid " + recordGuid); - numStoreFailed.incrementAndGet(); - this.consumer.stored(); - delegate.onFlowStoreFailed(this, ex, recordGuid); - // TODO: abort? - } - - @Override - public void onRecordStoreSucceeded(String guid) { - Logger.trace(LOG_TAG, "Stored record with guid " + guid); - this.consumer.stored(); - } - - - @Override - public void consumerIsDone(boolean allRecordsQueued) { - Logger.trace(LOG_TAG, "Consumer is done. Are we waiting for it? " + waitingForQueueDone); - if (waitingForQueueDone) { - waitingForQueueDone = false; - this.sink.storeDone(); // Now we'll be waiting for onStoreCompleted. - } - } - - @Override - public void onStoreCompleted(long storeEnd) { - Logger.trace(LOG_TAG, "onStoreCompleted. Notifying delegate of onFlowCompleted. " + - "Fetch end is " + fetchEnd + ", store end is " + storeEnd); - // TODO: synchronize on consumer callback? - delegate.onFlowCompleted(this, fetchEnd, storeEnd); - } - - @Override - public void onBeginFailed(Exception ex) { - delegate.onFlowBeginFailed(this, ex); - } - - @Override - public void onBeginSucceeded(RepositorySession session) { - if (session == source) { - Logger.trace(LOG_TAG, "Source session began. Beginning sink session."); - try { - sink.begin(this); - } catch (InvalidSessionTransitionException e) { - onBeginFailed(e); - return; - } - } - if (session == sink) { - Logger.trace(LOG_TAG, "Sink session began. Beginning flow."); - this.flow(); - return; - } - - // TODO: error! - } - - @Override - public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) { - return new DeferredRepositorySessionStoreDelegate(this, executor); - } - - @Override - public RepositorySessionBeginDelegate deferredBeginDelegate(final ExecutorService executor) { - return new DeferredRepositorySessionBeginDelegate(this, executor); - } - - @Override - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) { - // Lie outright. We know that all of our fetch methods are safe. - return this; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java deleted file mode 100644 index 8daeb7ad5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsChannelDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -public interface RecordsChannelDelegate { - public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd); - public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex); - public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex); - public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid); - public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java deleted file mode 100644 index a00abf848..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/RecordsConsumerDelegate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -interface RecordsConsumerDelegate { - public abstract ConcurrentLinkedQueue<Record> getQueue(); - - /** - * Called when no more items will be processed. - * If forced is true, the consumer is terminating because it was told to halt; - * not all items will necessarily have been processed. - * If forced is false, the consumer has invoked store and received an onStoreCompleted callback. - * @param forced - */ - public abstract void consumerIsDone(boolean forced); - public abstract void store(Record record); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java deleted file mode 100644 index 6ee44ea2b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SerialRecordConsumer.java +++ /dev/null @@ -1,131 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.domain.Record; - -/** - * Consume records from a queue inside a RecordsChannel, storing them serially. - * @author rnewman - * - */ -class SerialRecordConsumer extends RecordConsumer { - private static final String LOG_TAG = "SerialRecordConsumer"; - protected boolean stopEventually = false; - private volatile long counter = 0; - - public SerialRecordConsumer(RecordsConsumerDelegate delegate) { - this.delegate = delegate; - } - - private final Object monitor = new Object(); - @Override - public void doNotify() { - synchronized (monitor) { - monitor.notify(); - } - } - - @Override - public void queueFilled() { - Logger.debug(LOG_TAG, "Queue filled."); - synchronized (monitor) { - this.stopEventually = true; - monitor.notify(); - } - } - - @Override - public void halt() { - Logger.debug(LOG_TAG, "Halting."); - synchronized (monitor) { - this.stopEventually = true; - this.stopImmediately = true; - monitor.notify(); - } - } - - private final Object storeSerializer = new Object(); - @Override - public void stored() { - Logger.debug(LOG_TAG, "Record stored. Notifying."); - synchronized (storeSerializer) { - Logger.debug(LOG_TAG, "stored() took storeSerializer."); - counter++; - storeSerializer.notify(); - Logger.debug(LOG_TAG, "stored() dropped storeSerializer."); - } - } - private void storeSerially(Record record) { - Logger.debug(LOG_TAG, "New record to store."); - synchronized (storeSerializer) { - Logger.debug(LOG_TAG, "storeSerially() took storeSerializer."); - Logger.debug(LOG_TAG, "Storing..."); - try { - this.delegate.store(record); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception in store. Not waiting.", e); - return; // So we don't block for a stored() that never comes. - } - try { - Logger.debug(LOG_TAG, "Waiting..."); - storeSerializer.wait(); - } catch (InterruptedException e) { - // TODO - } - Logger.debug(LOG_TAG, "storeSerially() dropped storeSerializer."); - } - } - - private void consumerIsDone() { - long counterNow = this.counter; - Logger.info(LOG_TAG, "Consumer is done. Processed " + counterNow + ((counterNow == 1) ? " record." : " records.")); - delegate.consumerIsDone(stopImmediately); - } - - @Override - public void run() { - while (true) { - synchronized (monitor) { - Logger.debug(LOG_TAG, "run() took monitor."); - if (stopImmediately) { - Logger.debug(LOG_TAG, "Stopping immediately. Clearing queue."); - delegate.getQueue().clear(); - Logger.debug(LOG_TAG, "Notifying consumer."); - consumerIsDone(); - return; - } - Logger.debug(LOG_TAG, "run() dropped monitor."); - } - // The queue is concurrent-safe. - while (!delegate.getQueue().isEmpty()) { - Logger.debug(LOG_TAG, "Grabbing record..."); - Record record = delegate.getQueue().remove(); - // Block here, allowing us to process records - // serially. - Logger.debug(LOG_TAG, "Invoking storeSerially..."); - this.storeSerially(record); - Logger.debug(LOG_TAG, "Done with record."); - } - synchronized (monitor) { - Logger.debug(LOG_TAG, "run() took monitor."); - - if (stopEventually) { - Logger.debug(LOG_TAG, "Done with records and told to stop. Notifying consumer."); - consumerIsDone(); - return; - } - try { - Logger.debug(LOG_TAG, "Not told to stop but no records. Waiting."); - monitor.wait(10000); - } catch (InterruptedException e) { - // TODO - } - Logger.debug(LOG_TAG, "run() dropped monitor."); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java deleted file mode 100644 index ac4f48789..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizer.java +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -/** - * A <code>SynchronizerSession</code> designed to be used between a remote - * server and a local repository. - * <p> - * See <code>ServerLocalSynchronizerSession</code> for error handling details. - */ -public class ServerLocalSynchronizer extends Synchronizer { - @Override - public SynchronizerSession newSynchronizerSession() { - return new ServerLocalSynchronizerSession(this, this); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java deleted file mode 100644 index dc9eb01a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/ServerLocalSynchronizerSession.java +++ /dev/null @@ -1,78 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.FetchFailedException; -import org.mozilla.gecko.sync.repositories.StoreFailedException; - -/** - * A <code>SynchronizerSession</code> designed to be used between a remote - * server and a local repository. - * <p> - * Handles failure cases as follows (in the order they will occur during a sync): - * <ul> - * <li>Remote fetch failures abort.</li> - * <li>Local store failures are ignored.</li> - * <li>Local fetch failures abort.</li> - * <li>Remote store failures abort.</li> - * </ul> - */ -public class ServerLocalSynchronizerSession extends SynchronizerSession { - protected static final String LOG_TAG = "ServLocSynchronizerSess"; - - public ServerLocalSynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) { - super(synchronizer, delegate); - } - - @Override - public void onFirstFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - // Fetch failures always abort. - int numRemoteFetchFailed = recordsChannel.getFetchFailureCount(); - if (numRemoteFetchFailed > 0) { - final String message = "Got " + numRemoteFetchFailed + " failures fetching remote records!"; - Logger.warn(LOG_TAG, message + " Aborting session."); - delegate.onSynchronizeFailed(this, new FetchFailedException(), message); - return; - } - Logger.trace(LOG_TAG, "No failures fetching remote records."); - - // Local store failures are ignored. - int numLocalStoreFailed = recordsChannel.getStoreFailureCount(); - if (numLocalStoreFailed > 0) { - final String message = "Got " + numLocalStoreFailed + " failures storing local records!"; - Logger.warn(LOG_TAG, message + " Ignoring local store failures and continuing synchronizer session."); - } else { - Logger.trace(LOG_TAG, "No failures storing local records."); - } - - super.onFirstFlowCompleted(recordsChannel, fetchEnd, storeEnd); - } - - @Override - public void onSecondFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - // Fetch failures always abort. - int numLocalFetchFailed = recordsChannel.getFetchFailureCount(); - if (numLocalFetchFailed > 0) { - final String message = "Got " + numLocalFetchFailed + " failures fetching local records!"; - Logger.warn(LOG_TAG, message + " Aborting session."); - delegate.onSynchronizeFailed(this, new FetchFailedException(), message); - return; - } - Logger.trace(LOG_TAG, "No failures fetching local records."); - - // Remote store failures abort! - int numRemoteStoreFailed = recordsChannel.getStoreFailureCount(); - if (numRemoteStoreFailed > 0) { - final String message = "Got " + numRemoteStoreFailed + " failures storing remote records!"; - Logger.warn(LOG_TAG, message + " Aborting session."); - delegate.onSynchronizeFailed(this, new StoreFailedException(), message); - return; - } - Logger.trace(LOG_TAG, "No failures storing remote records."); - - super.onSecondFlowCompleted(recordsChannel, fetchEnd, storeEnd); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java deleted file mode 100644 index 20c7fcd56..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SessionNotBegunException.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.sync.SyncException; -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public class SessionNotBegunException extends SyncException { - - public RepositorySession failed; - - public SessionNotBegunException(RepositorySession failed) { - this.failed = failed; - } - - private static final long serialVersionUID = -4565241449897072841L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java deleted file mode 100644 index cc15b35a9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/Synchronizer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.SynchronizerConfiguration; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; - -import android.content.Context; - -/** - * I perform a sync. - * - * Initialize me by calling `load` with a SynchronizerConfiguration. - * - * Start synchronizing by calling `synchronize` with a SynchronizerDelegate. I - * provide coarse-grained feedback by calling my delegate's callback methods. - * - * I always call exactly one of my delegate's `onSynchronized` or - * `onSynchronizeFailed` callback methods. In addition, I call - * `onSynchronizeAborted` before `onSynchronizeFailed` when I encounter a fetch, - * store, or session error while synchronizing. - * - * After synchronizing, call `save` to get back a SynchronizerConfiguration with - * updated bundle information. - */ -public class Synchronizer implements SynchronizerSessionDelegate { - public static final String LOG_TAG = "SyncDelSDelegate"; - - protected String configSyncID; // Used to pass syncID from load() back into save(). - - protected SynchronizerDelegate synchronizerDelegate; - - protected SynchronizerSession session = null; - - public SynchronizerSession getSynchronizerSession() { - return session; - } - - @Override - public void onInitialized(SynchronizerSession session) { - session.synchronize(); - } - - @Override - public void onSynchronized(SynchronizerSession synchronizerSession) { - Logger.debug(LOG_TAG, "Got onSynchronized."); - Logger.debug(LOG_TAG, "Notifying SynchronizerDelegate."); - this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer()); - } - - @Override - public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) { - Logger.debug(LOG_TAG, "Got onSynchronizeSkipped."); - Logger.debug(LOG_TAG, "Notifying SynchronizerDelegate as if on success."); - this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer()); - } - - @Override - public void onSynchronizeFailed(SynchronizerSession session, - Exception lastException, String reason) { - this.synchronizerDelegate.onSynchronizeFailed(session.getSynchronizer(), lastException, reason); - } - - public Repository repositoryA; - public Repository repositoryB; - public RepositorySessionBundle bundleA; - public RepositorySessionBundle bundleB; - - /** - * Fetch a synchronizer session appropriate for this <code>Synchronizer</code> - */ - protected SynchronizerSession newSynchronizerSession() { - return new SynchronizerSession(this, this); - } - - /** - * Start synchronizing, calling delegate's callback methods. - */ - public void synchronize(Context context, SynchronizerDelegate delegate) { - this.synchronizerDelegate = delegate; - this.session = newSynchronizerSession(); - this.session.init(context, bundleA, bundleB); - } - - public SynchronizerConfiguration save() { - return new SynchronizerConfiguration(configSyncID, bundleA, bundleB); - } - - /** - * Set my repository session bundles from a SynchronizerConfiguration. - * - * This method is not thread-safe. - * - * @param config - */ - public void load(SynchronizerConfiguration config) { - bundleA = config.remoteBundle; - bundleB = config.localBundle; - configSyncID = config.syncID; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java deleted file mode 100644 index a290188ab..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -public interface SynchronizerDelegate { - public void onSynchronized(Synchronizer synchronizer); - public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java deleted file mode 100644 index c4d244b4c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSession.java +++ /dev/null @@ -1,425 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; -import org.mozilla.gecko.sync.repositories.delegates.DeferrableRepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; - -import android.content.Context; - -/** - * I coordinate the moving parts of a sync started by - * {@link Synchronizer#synchronize}. - * - * I flow records twice: first from A to B, and then from B to A. I provide - * fine-grained feedback by calling my delegate's callback methods. - * - * Initialize me by creating me with a Synchronizer and a - * SynchronizerSessionDelegate. Kick things off by calling `init` with two - * RepositorySessionBundles, and then call `synchronize` in your `onInitialized` - * callback. - * - * I always call exactly one of my delegate's `onInitialized` or - * `onSessionError` callback methods from `init`. - * - * I call my delegate's `onSynchronizeSkipped` callback method if there is no - * data to be synchronized in `synchronize`. - * - * In addition, I call `onFetchError`, `onStoreError`, and `onSessionError` when - * I encounter a fetch, store, or session error while synchronizing. - * - * Typically my delegate will call `abort` in its error callbacks, which will - * call my delegate's `onSynchronizeAborted` method and halt the sync. - * - * I always call exactly one of my delegate's `onSynchronized` or - * `onSynchronizeFailed` callback methods if I have not seen an error. - */ -public class SynchronizerSession -extends DeferrableRepositorySessionCreationDelegate -implements RecordsChannelDelegate, - RepositorySessionFinishDelegate { - - protected static final String LOG_TAG = "SynchronizerSession"; - protected Synchronizer synchronizer; - protected SynchronizerSessionDelegate delegate; - protected Context context; - - /* - * Computed during init. - */ - private RepositorySession sessionA; - private RepositorySession sessionB; - private RepositorySessionBundle bundleA; - private RepositorySessionBundle bundleB; - - // Bug 726054: just like desktop, we track our last interaction with the server, - // not the last record timestamp that we fetched. This ensures that we don't re- - // download the records we just uploaded, at the cost of skipping any records - // that a concurrently syncing client has uploaded. - private long pendingATimestamp = -1; - private long pendingBTimestamp = -1; - private long storeEndATimestamp = -1; - private long storeEndBTimestamp = -1; - private boolean flowAToBCompleted = false; - private boolean flowBToACompleted = false; - - protected final AtomicInteger numInboundRecords = new AtomicInteger(-1); - protected final AtomicInteger numOutboundRecords = new AtomicInteger(-1); - - /* - * Public API: constructor, init, synchronize. - */ - public SynchronizerSession(Synchronizer synchronizer, SynchronizerSessionDelegate delegate) { - this.setSynchronizer(synchronizer); - this.delegate = delegate; - } - - public Synchronizer getSynchronizer() { - return synchronizer; - } - - public void setSynchronizer(Synchronizer synchronizer) { - this.synchronizer = synchronizer; - } - - public void init(Context context, RepositorySessionBundle bundleA, RepositorySessionBundle bundleB) { - this.context = context; - this.bundleA = bundleA; - this.bundleB = bundleB; - // Begin sessionA and sessionB, call onInitialized in callbacks. - this.getSynchronizer().repositoryA.createSession(this, context); - } - - /** - * Get the number of records fetched from the first repository (usually the - * server, hence inbound). - * <p> - * Valid only after first flow has completed. - * - * @return number of records, or -1 if not valid. - */ - public int getInboundCount() { - return numInboundRecords.get(); - } - - /** - * Get the number of records fetched from the second repository (usually the - * local store, hence outbound). - * <p> - * Valid only after second flow has completed. - * - * @return number of records, or -1 if not valid. - */ - public int getOutboundCount() { - return numOutboundRecords.get(); - } - - // These are accessed by `abort` and `synchronize`, both of which are synchronized. - // Guarded by `this`. - protected RecordsChannel channelAToB; - protected RecordsChannel channelBToA; - - /** - * Please don't call this until you've been notified with onInitialized. - */ - public synchronized void synchronize() { - numInboundRecords.set(-1); - numOutboundRecords.set(-1); - - // First thing: decide whether we should. - if (sessionA.shouldSkip() || - sessionB.shouldSkip()) { - Logger.info(LOG_TAG, "Session requested skip. Short-circuiting sync."); - sessionA.abort(); - sessionB.abort(); - this.delegate.onSynchronizeSkipped(this); - return; - } - - final SynchronizerSession session = this; - - // TODO: failed record handling. - - // This is the *second* record channel to flow. - // I, SynchronizerSession, am the delegate for the *second* flow. - channelBToA = new RecordsChannel(this.sessionB, this.sessionA, this); - - // This is the delegate for the *first* flow. - RecordsChannelDelegate channelAToBDelegate = new RecordsChannelDelegate() { - @Override - public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - session.onFirstFlowCompleted(recordsChannel, fetchEnd, storeEnd); - } - - @Override - public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "First RecordsChannel onFlowBeginFailed. Logging session error.", ex); - session.delegate.onSynchronizeFailed(session, ex, "Failed to begin first flow."); - } - - @Override - public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "First RecordsChannel onFlowFetchFailed. Logging remote fetch error.", ex); - } - - @Override - public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) { - Logger.warn(LOG_TAG, "First RecordsChannel onFlowStoreFailed. Logging local store error.", ex); - } - - @Override - public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "First RecordsChannel onFlowFinishedFailed. Logging session error.", ex); - session.delegate.onSynchronizeFailed(session, ex, "Failed to finish first flow."); - } - }; - - // This is the *first* channel to flow. - channelAToB = new RecordsChannel(this.sessionA, this.sessionB, channelAToBDelegate); - - Logger.trace(LOG_TAG, "Starting A to B flow. Channel is " + channelAToB); - try { - channelAToB.beginAndFlow(); - } catch (InvalidSessionTransitionException e) { - onFlowBeginFailed(channelAToB, e); - } - } - - /** - * Called after the first flow completes. - * <p> - * By default, any fetch and store failures are ignored. - * @param recordsChannel the <code>RecordsChannel</code> (for error testing). - * @param fetchEnd timestamp when fetches completed. - * @param storeEnd timestamp when stores completed. - */ - public void onFirstFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - Logger.trace(LOG_TAG, "First RecordsChannel onFlowCompleted."); - Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Starting next."); - pendingATimestamp = fetchEnd; - storeEndBTimestamp = storeEnd; - numInboundRecords.set(recordsChannel.getFetchCount()); - flowAToBCompleted = true; - channelBToA.flow(); - } - - /** - * Called after the second flow completes. - * <p> - * By default, any fetch and store failures are ignored. - * @param recordsChannel the <code>RecordsChannel</code> (for error testing). - * @param fetchEnd timestamp when fetches completed. - * @param storeEnd timestamp when stores completed. - */ - public void onSecondFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - Logger.trace(LOG_TAG, "Second RecordsChannel onFlowCompleted."); - Logger.debug(LOG_TAG, "Fetch end is " + fetchEnd + ". Store end is " + storeEnd + ". Finishing."); - - pendingBTimestamp = fetchEnd; - storeEndATimestamp = storeEnd; - numOutboundRecords.set(recordsChannel.getFetchCount()); - flowBToACompleted = true; - - // Finish the two sessions. - try { - this.sessionA.finish(this); - } catch (InactiveSessionException e) { - this.onFinishFailed(e); - return; - } - } - - @Override - public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) { - onSecondFlowCompleted(recordsChannel, fetchEnd, storeEnd); - } - - @Override - public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "Second RecordsChannel onFlowBeginFailed. Logging session error.", ex); - this.delegate.onSynchronizeFailed(this, ex, "Failed to begin second flow."); - } - - @Override - public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "Second RecordsChannel onFlowFetchFailed. Logging local fetch error.", ex); - } - - @Override - public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) { - Logger.warn(LOG_TAG, "Second RecordsChannel onFlowStoreFailed. Logging remote store error.", ex); - } - - @Override - public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) { - Logger.warn(LOG_TAG, "Second RecordsChannel onFlowFinishedFailed. Logging session error.", ex); - this.delegate.onSynchronizeFailed(this, ex, "Failed to finish second flow."); - } - - /* - * RepositorySessionCreationDelegate methods. - */ - - /** - * I could be called twice: once for sessionA and once for sessionB. - * - * I try to clean up sessionA if it is not null, since the creation of - * sessionB must have failed. - */ - @Override - public void onSessionCreateFailed(Exception ex) { - // Attempt to finish the first session, if the second is the one that failed. - if (this.sessionA != null) { - try { - // We no longer need a reference to our context. - this.context = null; - this.sessionA.finish(this); - } catch (Exception e) { - // Never mind; best-effort finish. - } - } - // We no longer need a reference to our context. - this.context = null; - this.delegate.onSynchronizeFailed(this, ex, "Failed to create session"); - } - - /** - * I should be called twice: first for sessionA and second for sessionB. - * - * If I am called for sessionB, I call my delegate's `onInitialized` callback - * method because my repository sessions are correctly initialized. - */ - // TODO: some of this "finish and clean up" code can be refactored out. - @Override - public void onSessionCreated(RepositorySession session) { - if (session == null || - this.sessionA == session) { - // TODO: clean up sessionA. - this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(session), "Failed to create session."); - return; - } - if (this.sessionA == null) { - this.sessionA = session; - - // Unbundle. - try { - this.sessionA.unbundle(this.bundleA); - } catch (Exception e) { - this.delegate.onSynchronizeFailed(this, new UnbundleError(e, sessionA), "Failed to unbundle first session."); - // TODO: abort - return; - } - this.getSynchronizer().repositoryB.createSession(this, this.context); - return; - } - if (this.sessionB == null) { - this.sessionB = session; - // We no longer need a reference to our context. - this.context = null; - - // Unbundle. We unbundled sessionA when that session was created. - try { - this.sessionB.unbundle(this.bundleB); - } catch (Exception e) { - this.delegate.onSynchronizeFailed(this, new UnbundleError(e, sessionA), "Failed to unbundle second session."); - return; - } - - this.delegate.onInitialized(this); - return; - } - // TODO: need a way to make sure we don't call any more delegate methods. - this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(session), "Failed to create session."); - } - - /* - * RepositorySessionFinishDelegate methods. - */ - - /** - * I could be called twice: once for sessionA and once for sessionB. - * - * If sessionB couldn't be created, I don't fail again. - */ - @Override - public void onFinishFailed(Exception ex) { - if (this.sessionB == null) { - // Ah, it was a problem cleaning up. Never mind. - Logger.warn(LOG_TAG, "Got exception cleaning up first after second session creation failed.", ex); - return; - } - String session = (this.sessionA == null) ? "B" : "A"; - this.delegate.onSynchronizeFailed(this, ex, "Finish of session " + session + " failed."); - } - - /** - * I should be called twice: first for sessionA and second for sessionB. - * - * If I am called for sessionA, I try to finish sessionB. - * - * If I am called for sessionB, I call my delegate's `onSynchronized` callback - * method because my flows should have completed. - */ - @Override - public void onFinishSucceeded(RepositorySession session, - RepositorySessionBundle bundle) { - Logger.debug(LOG_TAG, "onFinishSucceeded. Flows? " + flowAToBCompleted + ", " + flowBToACompleted); - - if (session == sessionA) { - if (flowAToBCompleted) { - Logger.debug(LOG_TAG, "onFinishSucceeded: bumping session A's timestamp to " + pendingATimestamp + " or " + storeEndATimestamp); - bundle.bumpTimestamp(Math.max(pendingATimestamp, storeEndATimestamp)); - this.synchronizer.bundleA = bundle; - } else { - // Should not happen! - this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(sessionA), "Failed to finish first session."); - return; - } - if (this.sessionB != null) { - Logger.trace(LOG_TAG, "Finishing session B."); - // On to the next. - try { - this.sessionB.finish(this); - } catch (InactiveSessionException e) { - this.onFinishFailed(e); - return; - } - } - } else if (session == sessionB) { - if (flowBToACompleted) { - Logger.debug(LOG_TAG, "onFinishSucceeded: bumping session B's timestamp to " + pendingBTimestamp + " or " + storeEndBTimestamp); - bundle.bumpTimestamp(Math.max(pendingBTimestamp, storeEndBTimestamp)); - this.synchronizer.bundleB = bundle; - Logger.trace(LOG_TAG, "Notifying delegate.onSynchronized."); - this.delegate.onSynchronized(this); - } else { - // Should not happen! - this.delegate.onSynchronizeFailed(this, new UnexpectedSessionException(sessionB), "Failed to finish second session."); - return; - } - } else { - // TODO: hurrrrrr... - } - - if (this.sessionB == null) { - this.sessionA = null; // We're done. - } - } - - @Override - public RepositorySessionFinishDelegate deferredFinishDelegate(final ExecutorService executor) { - return new DeferredRepositorySessionFinishDelegate(this, executor); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java deleted file mode 100644 index 1d55274e8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/SynchronizerSessionDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -public interface SynchronizerSessionDelegate { - public void onInitialized(SynchronizerSession session); - - public void onSynchronized(SynchronizerSession session); - public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason); - public void onSynchronizeSkipped(SynchronizerSession synchronizerSession); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java deleted file mode 100644 index fea779636..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnbundleError.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.sync.SyncException; -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public class UnbundleError extends SyncException { - private static final long serialVersionUID = -8709503281041697522L; - - public RepositorySession failedSession; - - public UnbundleError(Exception e, RepositorySession session) { - super(e); - this.failedSession = session; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java deleted file mode 100644 index 0237b884b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/synchronizer/UnexpectedSessionException.java +++ /dev/null @@ -1,26 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.synchronizer; - -import org.mozilla.gecko.sync.SyncException; -import org.mozilla.gecko.sync.repositories.RepositorySession; - -/** - * An exception class that indicates that a session was passed - * to a begin callback and wasn't expected. - * - * This shouldn't occur. - * - * @author rnewman - * - */ -public class UnexpectedSessionException extends SyncException { - private static final long serialVersionUID = 949010933527484721L; - public RepositorySession session; - - public UnexpectedSessionException(RepositorySession session) { - this.session = session; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java deleted file mode 100644 index e3e134fe5..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/telemetry/TelemetryContract.java +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.telemetry; - -public class TelemetryContract { - /** - * We are a Sync 1.1 (legacy) client, and we downloaded a migration sentinel. - */ - public static final String SYNC11_MIGRATION_SENTINELS_SEEN = "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN"; - - /** - * We are a Sync 1.1 (legacy) client and we have downloaded a migration - * sentinel, but there was an error creating a Firefox Account from that - * sentinel. - * <p> - * We have logged the error and are ignoring that sentinel. - */ - public static final String SYNC11_MIGRATIONS_FAILED = "FENNEC_SYNC11_MIGRATIONS_FAILED"; - - /** - * We are a Sync 1.1 (legacy) client and we have downloaded a migration - * sentinel, and there was no reported error creating a Firefox Account from - * that sentinel. - * <p> - * We have created a Firefox Account corresponding to the sentinel and have - * queued the existing Old Sync account for removal. - */ - public static final String SYNC11_MIGRATIONS_SUCCEEDED = "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED"; - - /** - * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from - * Sync 1.1. We have presented the user the "complete upgrade" notification. - * <p> - * We will offer every time a sync is triggered, including when a notification - * is already pending. - */ - public static final String SYNC11_MIGRATION_NOTIFICATIONS_OFFERED = "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED"; - - /** - * We are (now) a Sync 1.5 (Firefox Accounts-based) client that migrated from - * Sync 1.1. We have presented the user the "complete upgrade" notification - * and they have successfully completed the upgrade process by entering their - * Firefox Account credentials. - */ - public static final String SYNC11_MIGRATIONS_COMPLETED = "FENNEC_SYNC11_MIGRATIONS_COMPLETED"; - - public static final String SYNC_STARTED = "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED"; - - public static final String SYNC_COMPLETED = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED"; - - public static final String SYNC_FAILED = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED"; - - public static final String SYNC_FAILED_BACKOFF = "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF"; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java deleted file mode 100644 index 9ee014dcb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java +++ /dev/null @@ -1,330 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.tokenserver; - -import java.io.IOException; -import java.net.URI; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; - -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.fxa.SkewHandler; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.BaseResource; -import org.mozilla.gecko.sync.net.BaseResourceDelegate; -import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException; -import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException; -import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException; -import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException; -import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException; - -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import ch.boye.httpclientandroidlib.message.BasicHeader; - -/** - * HTTP client for interacting with the Mozilla Services Token Server API v1.0, - * as documented at - * <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>. - * <p> - * A token server accepts some authorization credential and returns a different - * authorization credential. Usually, it used to exchange a public-key - * authorization token that is expensive to validate for a symmetric-key - * authorization that is cheap to validate. For example, we might exchange a - * BrowserID assertion for a HAWK id and key pair. - */ -public class TokenServerClient { - protected static final String LOG_TAG = "TokenServerClient"; - - public static final String JSON_KEY_API_ENDPOINT = "api_endpoint"; - public static final String JSON_KEY_CONDITION_URLS = "condition_urls"; - public static final String JSON_KEY_DURATION = "duration"; - public static final String JSON_KEY_ERRORS = "errors"; - public static final String JSON_KEY_ID = "id"; - public static final String JSON_KEY_KEY = "key"; - public static final String JSON_KEY_UID = "uid"; - - public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted"; - public static final String HEADER_CLIENT_STATE = "X-Client-State"; - - protected final Executor executor; - protected final URI uri; - - public TokenServerClient(URI uri, Executor executor) { - if (uri == null) { - throw new IllegalArgumentException("uri must not be null"); - } - if (executor == null) { - throw new IllegalArgumentException("executor must not be null"); - } - this.uri = uri; - this.executor = executor; - } - - protected void invokeHandleSuccess(final TokenServerClientDelegate delegate, final TokenServerToken token) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleSuccess(token); - } - }); - } - - protected void invokeHandleFailure(final TokenServerClientDelegate delegate, final TokenServerException e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(e); - } - }); - } - - /** - * Notify the delegate that some kind of backoff header (X-Backoff, - * X-Weave-Backoff, Retry-After) was received and should be acted upon. - * - * This method is non-terminal, and will be followed by a separate - * <code>invoke*</code> call. - * - * @param delegate - * the delegate to inform. - * @param backoffSeconds - * the number of seconds for which the system should wait before - * making another token server request to this server. - */ - protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleBackoff(backoffSeconds); - } - }); - } - - protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); - } - - public TokenServerToken processResponse(SyncResponse res) throws TokenServerException { - int statusCode = res.getStatusCode(); - - Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + "."); - - // Responses should *always* be JSON, even in the case of 4xx and 5xx - // errors. If we don't see JSON, the server is likely very unhappy. - final Header contentType = res.getContentType(); - if (contentType == null) { - throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type."); - } - - final String type = contentType.getValue(); - if (!type.equals("application/json") && - !type.startsWith("application/json;")) { - Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " + - contentType + ". Misconfigured server?"); - throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type."); - } - - // Responses should *always* be a valid JSON object. - // It turns out that right now they're not always, but that's a server bug... - ExtendedJSONObject result; - try { - result = res.jsonObjectBody(); - } catch (Exception e) { - Logger.debug(LOG_TAG, "Malformed token response.", e); - throw new TokenServerMalformedResponseException(null, e); - } - - // The service shouldn't have any 3xx, so we don't need to handle those. - if (res.getStatusCode() != 200) { - // We should have a (Cornice) error report in the JSON. We log that to - // help with debugging. - List<ExtendedJSONObject> errorList = new ArrayList<ExtendedJSONObject>(); - - if (result.containsKey(JSON_KEY_ERRORS)) { - try { - for (Object error : result.getArray(JSON_KEY_ERRORS)) { - Logger.warn(LOG_TAG, "" + error); - - if (error instanceof JSONObject) { - errorList.add(new ExtendedJSONObject((JSONObject) error)); - } - } - } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "Got non-JSON array '" + JSON_KEY_ERRORS + "'.", e); - } - } - - if (statusCode == 400) { - throw new TokenServerMalformedRequestException(errorList, result.toJSONString()); - } - - if (statusCode == 401) { - throw new TokenServerInvalidCredentialsException(errorList, result.toJSONString()); - } - - // 403 should represent a "condition acceptance needed" response. - // - // The extra validation of "urls" is important. We don't want to signal - // conditions required unless we are absolutely sure that is what the - // server is asking for. - if (statusCode == 403) { - // Bug 792674 and Bug 783598: make this testing simpler. For now, we - // check that errors is an array, and take any condition_urls from the - // first element. - - try { - if (errorList == null || errorList.isEmpty()) { - throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields."); - } - - ExtendedJSONObject error = errorList.get(0); - - ExtendedJSONObject condition_urls = error.getObject(JSON_KEY_CONDITION_URLS); - if (condition_urls != null) { - throw new TokenServerConditionsRequiredException(condition_urls); - } - } catch (NonObjectJSONException e) { - Logger.warn(LOG_TAG, "Got non-JSON error object."); - } - - throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields."); - } - - if (statusCode == 404) { - throw new TokenServerUnknownServiceException(errorList); - } - - // We shouldn't ever get here... - throw new TokenServerException(errorList); - } - - try { - result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT }, String.class); - result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_UID }, Long.class); - } catch (BadRequiredFieldJSONException e ) { - throw new TokenServerMalformedResponseException(null, e); - } - - Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID)); - - return new TokenServerToken(result.getString(JSON_KEY_ID), - result.getString(JSON_KEY_KEY), - result.get(JSON_KEY_UID).toString(), - result.getString(JSON_KEY_API_ENDPOINT)); - } - - public static class TokenFetchResourceDelegate extends BaseResourceDelegate { - private final TokenServerClient client; - private final TokenServerClientDelegate delegate; - private final String assertion; - private final String clientState; - private final BaseResource resource; - private final boolean conditionsAccepted; - - public TokenFetchResourceDelegate(TokenServerClient client, - BaseResource resource, - TokenServerClientDelegate delegate, - String assertion, String clientState, - boolean conditionsAccepted) { - super(resource); - this.client = client; - this.delegate = delegate; - this.assertion = assertion; - this.clientState = clientState; - this.resource = resource; - this.conditionsAccepted = conditionsAccepted; - } - - @Override - public String getUserAgent() { - return delegate.getUserAgent(); - } - - @Override - public void handleHttpResponse(HttpResponse response) { - // Skew. - SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource); - skewHandler.updateSkew(response, System.currentTimeMillis()); - - // Extract backoff regardless of whether this was an error response, and - // Retry-After for 503 responses. The error will be handled elsewhere.) - SyncResponse res = new SyncResponse(response); - final boolean includeRetryAfter = res.getStatusCode() == 503; - int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter); - if (backoffInSeconds > -1) { - client.notifyBackoff(delegate, backoffInSeconds); - } - - try { - TokenServerToken token = client.processResponse(res); - client.invokeHandleSuccess(delegate, token); - } catch (TokenServerException e) { - client.invokeHandleFailure(delegate, e); - } - } - - @Override - public void handleTransportException(GeneralSecurityException e) { - client.invokeHandleError(delegate, e); - } - - @Override - public void handleHttpProtocolException(ClientProtocolException e) { - client.invokeHandleError(delegate, e); - } - - @Override - public void handleHttpIOException(IOException e) { - client.invokeHandleError(delegate, e); - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return new BrowserIDAuthHeaderProvider(assertion); - } - - @Override - public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { - String host = request.getURI().getHost(); - request.setHeader(new BasicHeader(HttpHeaders.HOST, host)); - if (clientState != null) { - request.setHeader(new BasicHeader(HEADER_CLIENT_STATE, clientState)); - } - if (conditionsAccepted) { - request.addHeader(HEADER_CONDITIONS_ACCEPTED, "1"); - } - } - } - - public void getTokenFromBrowserIDAssertion(final String assertion, - final boolean conditionsAccepted, - final String clientState, - final TokenServerClientDelegate delegate) { - final BaseResource resource = new BaseResource(this.uri); - resource.delegate = new TokenFetchResourceDelegate(this, resource, delegate, - assertion, clientState, - conditionsAccepted); - resource.get(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java deleted file mode 100644 index e1dfe2422..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClientDelegate.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.tokenserver; - - -public interface TokenServerClientDelegate { - void handleSuccess(TokenServerToken token); - void handleFailure(TokenServerException e); - void handleError(Exception e); - - /** - * Might be called multiple times, in addition to the other terminating handler methods. - */ - void handleBackoff(int backoffSeconds); - - public String getUserAgent(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java deleted file mode 100644 index 099e51867..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerException.java +++ /dev/null @@ -1,89 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.tokenserver; - -import java.util.List; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -public class TokenServerException extends Exception { - private static final long serialVersionUID = 7185692034925819696L; - - public final List<ExtendedJSONObject> errors; - - public TokenServerException(List<ExtendedJSONObject> errors) { - super(); - this.errors = errors; - } - - public TokenServerException(List<ExtendedJSONObject> errors, String string) { - super(string); - this.errors = errors; - } - - public TokenServerException(List<ExtendedJSONObject> errors, Throwable e) { - super(e); - this.errors = errors; - } - - public static class TokenServerConditionsRequiredException extends TokenServerException { - private static final long serialVersionUID = 7578072663150608399L; - - public final ExtendedJSONObject conditionUrls; - - public TokenServerConditionsRequiredException(ExtendedJSONObject urls) { - super(null); - this.conditionUrls = urls; - } - } - - public static class TokenServerInvalidCredentialsException extends TokenServerException { - private static final long serialVersionUID = 7578072663150608398L; - - public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors) { - super(errors); - } - - public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors, String message) { - super(errors, message); - } - } - - public static class TokenServerUnknownServiceException extends TokenServerException { - private static final long serialVersionUID = 7578072663150608397L; - - public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors) { - super(errors); - } - - public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors, String message) { - super(errors, message); - } - } - - public static class TokenServerMalformedRequestException extends TokenServerException { - private static final long serialVersionUID = 7578072663150608396L; - - public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors) { - super(errors); - } - - public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors, String message) { - super(errors, message); - } - } - - public static class TokenServerMalformedResponseException extends TokenServerException { - private static final long serialVersionUID = 7578072663150608395L; - - public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, String message) { - super(errors, message); - } - - public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, Throwable e) { - super(errors, e); - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java deleted file mode 100644 index 916586cdc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerToken.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.tokenserver; - -public class TokenServerToken { - public final String id; - public final String key; - public final String uid; - public final String endpoint; - - public TokenServerToken(String id, String key, String uid, String endpoint) { - this.id = id; - this.key = key; - this.uid = uid; - this.endpoint = endpoint; - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java b/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java deleted file mode 100644 index ebb50f765..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/util/PRNGFixes.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * This software is provided 'as-is', without any express or implied - * warranty. In no event will Google be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, as long as the origin is not misrepresented. - */ - -package org.mozilla.gecko.util; - -import android.os.Build; -import android.os.Process; -import android.util.Log; - -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.SecureRandomSpi; -import java.security.Security; - -/** - * Fixes for the output of the default PRNG having low entropy. - * - * The fixes need to be applied via {@link #apply()} before any use of Java - * Cryptography Architecture primitives. A good place to invoke them is in the - * application's {@code onCreate}. - */ -public final class PRNGFixes { - private static final long serialVersionUID = -687331492884005033L; - - private static final int VERSION_CODE_JELLY_BEAN = 16; - private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; - private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = - getBuildFingerprintAndDeviceSerial(); - - /** Hidden constructor to prevent instantiation. */ - private PRNGFixes() {} - - /** - * Applies all fixes. - * - * @throws SecurityException if a fix is needed but could not be applied. - */ - public static void apply() { - applyOpenSSLFix(); - installLinuxPRNGSecureRandom(); - } - - /** - * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the - * fix is not needed. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void applyOpenSSLFix() throws SecurityException { - if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) - || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { - // No need to apply the fix - return; - } - - try { - // Mix in the device- and invocation-specific seed. - Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_seed", byte[].class) - .invoke(null, generateSeed()); - - // Mix output of Linux PRNG into OpenSSL's PRNG - int bytesRead = (Integer) Class.forName( - "org.apache.harmony.xnet.provider.jsse.NativeCrypto") - .getMethod("RAND_load_file", String.class, long.class) - .invoke(null, "/dev/urandom", 1024); - if (bytesRead != 1024) { - throw new IOException( - "Unexpected number of bytes read from Linux PRNG: " - + bytesRead); - } - } catch (Exception e) { - throw new SecurityException("Failed to seed OpenSSL PRNG", e); - } - } - - /** - * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the - * default. Does nothing if the implementation is already the default or if - * there is not need to install the implementation. - * - * @throws SecurityException if the fix is needed but could not be applied. - */ - private static void installLinuxPRNGSecureRandom() - throws SecurityException { - if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { - // No need to apply the fix - return; - } - - // Install a Linux PRNG-based SecureRandom implementation as the - // default, if not yet installed. - Provider[] secureRandomProviders = - Security.getProviders("SecureRandom.SHA1PRNG"); - if ((secureRandomProviders == null) - || (secureRandomProviders.length < 1) - || (!LinuxPRNGSecureRandomProvider.class.equals( - secureRandomProviders[0].getClass()))) { - Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); - } - - // Assert that new SecureRandom() and - // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed - // by the Linux PRNG-based SecureRandom implementation. - SecureRandom rng1 = new SecureRandom(); - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng1.getProvider().getClass())) { - throw new SecurityException( - "new SecureRandom() backed by wrong Provider: " - + rng1.getProvider().getClass()); - } - - SecureRandom rng2; - try { - rng2 = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new SecurityException("SHA1PRNG not available", e); - } - if (!LinuxPRNGSecureRandomProvider.class.equals( - rng2.getProvider().getClass())) { - throw new SecurityException( - "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" - + " Provider: " + rng2.getProvider().getClass()); - } - } - - /** - * {@code Provider} of {@code SecureRandom} engines which pass through - * all requests to the Linux PRNG. - */ - private static class LinuxPRNGSecureRandomProvider extends Provider { - private static final long serialVersionUID = -686731492884005033L; - - public LinuxPRNGSecureRandomProvider() { - super("LinuxPRNG", - 1.0, - "A Linux-specific random number provider that uses" - + " /dev/urandom"); - // Although /dev/urandom is not a SHA-1 PRNG, some apps - // explicitly request a SHA1PRNG SecureRandom and we thus need to - // prevent them from getting the default implementation whose output - // may have low entropy. - put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); - put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); - } - } - - /** - * {@link SecureRandomSpi} which passes all requests to the Linux PRNG - * ({@code /dev/urandom}). - */ - public static class LinuxPRNGSecureRandom extends SecureRandomSpi { - private static final long serialVersionUID = -696231492884005033L; - - /* - * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed - * are passed through to the Linux PRNG (/dev/urandom). Instances of - * this class seed themselves by mixing in the current time, PID, UID, - * build fingerprint, and hardware serial number (where available) into - * Linux PRNG. - * - * Concurrency: Read requests to the underlying Linux PRNG are - * serialized (on sLock) to ensure that multiple threads do not get - * duplicated PRNG output. - */ - - private static final File URANDOM_FILE = new File("/dev/urandom"); - - private static final Object sLock = new Object(); - - /** - * Input stream for reading from Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static DataInputStream sUrandomIn; - - /** - * Output stream for writing to Linux PRNG or {@code null} if not yet - * opened. - * - * @GuardedBy("sLock") - */ - private static OutputStream sUrandomOut; - - /** - * Whether this engine instance has been seeded. This is needed because - * each instance needs to seed itself if the client does not explicitly - * seed it. - */ - private boolean mSeeded; - - @Override - protected void engineSetSeed(byte[] bytes) { - try { - OutputStream out; - synchronized (sLock) { - out = getUrandomOutputStream(); - } - out.write(bytes); - out.flush(); - } catch (IOException e) { - // On a small fraction of devices /dev/urandom is not writable. - // Log and ignore. - Log.w(PRNGFixes.class.getSimpleName(), - "Failed to mix seed into " + URANDOM_FILE); - } finally { - mSeeded = true; - } - } - - @Override - protected void engineNextBytes(byte[] bytes) { - if (!mSeeded) { - // Mix in the device- and invocation-specific seed. - engineSetSeed(generateSeed()); - } - - try { - DataInputStream in; - synchronized (sLock) { - in = getUrandomInputStream(); - } - synchronized (in) { - in.readFully(bytes); - } - } catch (IOException e) { - throw new SecurityException( - "Failed to read from " + URANDOM_FILE, e); - } - } - - @Override - protected byte[] engineGenerateSeed(int size) { - byte[] seed = new byte[size]; - engineNextBytes(seed); - return seed; - } - - private DataInputStream getUrandomInputStream() { - synchronized (sLock) { - if (sUrandomIn == null) { - // NOTE: Consider inserting a BufferedInputStream between - // DataInputStream and FileInputStream if you need higher - // PRNG output performance and can live with future PRNG - // output being pulled into this process prematurely. - try { - sUrandomIn = new DataInputStream( - new FileInputStream(URANDOM_FILE)); - } catch (IOException e) { - throw new SecurityException("Failed to open " - + URANDOM_FILE + " for reading", e); - } - } - return sUrandomIn; - } - } - - private OutputStream getUrandomOutputStream() throws IOException { - synchronized (sLock) { - if (sUrandomOut == null) { - sUrandomOut = new FileOutputStream(URANDOM_FILE); - } - return sUrandomOut; - } - } - } - - /** - * Generates a device- and invocation-specific seed to be mixed into the - * Linux PRNG. - */ - private static byte[] generateSeed() { - try { - ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); - DataOutputStream seedBufferOut = - new DataOutputStream(seedBuffer); - seedBufferOut.writeLong(System.currentTimeMillis()); - seedBufferOut.writeLong(System.nanoTime()); - seedBufferOut.writeInt(Process.myPid()); - seedBufferOut.writeInt(Process.myUid()); - seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); - seedBufferOut.close(); - return seedBuffer.toByteArray(); - } catch (IOException e) { - throw new SecurityException("Failed to generate seed", e); - } - } - - /** - * Gets the hardware serial number of this device. - * - * @return serial number or {@code null} if not available. - */ - private static String getDeviceSerialNumber() { - // We're using the Reflection API because Build.SERIAL is only available - // since API Level 9 (Gingerbread, Android 2.3). - try { - return (String) Build.class.getField("SERIAL").get(null); - } catch (Exception ignored) { - return null; - } - } - - private static byte[] getBuildFingerprintAndDeviceSerial() { - StringBuilder result = new StringBuilder(); - String fingerprint = Build.FINGERPRINT; - if (fingerprint != null) { - result.append(fingerprint); - } - String serial = getDeviceSerialNumber(); - if (serial != null) { - result.append(serial); - } - try { - return result.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 encoding not supported"); - } - } -} diff --git a/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png Binary files differdeleted file mode 100644 index 3a2cbc4bf..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/fxaccount_sync_error.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png Binary files differdeleted file mode 100644 index caa6ed246..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_avatar_default.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png Binary files differdeleted file mode 100644 index abf87f16c..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png Binary files differdeleted file mode 100644 index 869dbf402..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_desktop_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png Binary files differdeleted file mode 100644 index 4b25152b2..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png Binary files differdeleted file mode 100644 index e9401797d..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_mobile_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png b/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png Binary files differdeleted file mode 100644 index ea2150508..000000000 --- a/mobile/android/services/src/main/res/drawable-hdpi/sync_promo.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png Binary files differdeleted file mode 100644 index f9bf849fa..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/fxaccount_sync_error.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png Binary files differdeleted file mode 100644 index 30d5b5c09..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png Binary files differdeleted file mode 100644 index 1b5b00a75..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/sync_desktop_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png Binary files differdeleted file mode 100644 index 2c3f45d4a..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png Binary files differdeleted file mode 100644 index 60fd77c8a..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/sync_mobile_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png b/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png Binary files differdeleted file mode 100644 index 63f1a55ad..000000000 --- a/mobile/android/services/src/main/res/drawable-xhdpi/sync_promo.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png b/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png Binary files differdeleted file mode 100644 index 7555bc9d6..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/fxaccount_sync_error.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png Binary files differdeleted file mode 100644 index 16d127882..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_avatar_default.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png Binary files differdeleted file mode 100644 index 9bb9a55c2..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png Binary files differdeleted file mode 100644 index c3fe0ec1d..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_desktop_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png Binary files differdeleted file mode 100644 index 400ddf65b..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png b/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png Binary files differdeleted file mode 100644 index a688b0d7b..000000000 --- a/mobile/android/services/src/main/res/drawable-xxhdpi/sync_mobile_inactive.png +++ /dev/null diff --git a/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml b/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml deleted file mode 100644 index acaafc7c2..000000000 --- a/mobile/android/services/src/main/res/layout/fxaccount_preference_list_fragment.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* -** Copyright 2010, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_height="fill_parent" - android:layout_width="fill_parent" - android:background="@android:color/transparent"> - - <ListView android:id="@android:id/list" - android:layout_width="fill_parent" - android:layout_height="0px" - android:layout_weight="1" - android:paddingTop="0dip" - android:paddingBottom="@dimen/preference_fragment_padding_bottom" - android:paddingLeft="@dimen/preference_fragment_padding_side" - android:paddingRight="@dimen/preference_fragment_padding_side" - android:scrollbarStyle="@integer/preference_fragment_scrollbarStyle" - android:clipToPadding="false" - android:drawSelectorOnTop="false" - android:cacheColorHint="@android:color/transparent" - android:scrollbarAlwaysDrawVerticalTrack="true" /> - -</LinearLayout> diff --git a/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml b/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml deleted file mode 100644 index 4a507cddd..000000000 --- a/mobile/android/services/src/main/res/layout/fxaccount_status_error_preference.xml +++ /dev/null @@ -1,66 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@color/fxaccount_error_preference_backgroundcolor" - android:gravity="center_vertical" - android:minHeight="?android:attr/listPreferredItemHeight" - android:paddingRight="?android:attr/scrollbarSize" > - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center" - android:minWidth="0dp" - android:orientation="horizontal" > - - <ImageView - android:id="@+android:id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:minWidth="48dip" - android:padding="10dip" /> - </LinearLayout> - - <RelativeLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginBottom="6dip" - android:layout_marginLeft="15dip" - android:layout_marginRight="6dip" - android:layout_marginTop="6dip" - android:layout_weight="1" > - - <TextView - android:id="@+android:id/title" - style="@style/FxAccountTextItem" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:gravity="center_vertical" > - </TextView> - </RelativeLayout> - - <!-- We ignore summary and widget_frame, but they still need to be present. We set them to be gone. --> - - <TextView - android:id="@+android:id/summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:maxLines="4" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:attr/textColorSecondary" - android:visibility="gone" /> - - <!-- Preference should place its actual preference widget here. --> - - <LinearLayout - android:id="@+android:id/widget_frame" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center" - android:orientation="vertical" - android:visibility="gone" /> - -</LinearLayout> diff --git a/mobile/android/services/src/main/res/layout/homescreen_prompt.xml b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml deleted file mode 100644 index 26d04ad17..000000000 --- a/mobile/android/services/src/main/res/layout/homescreen_prompt.xml +++ /dev/null @@ -1,92 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<merge xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipChildren="false" - android:clipToPadding="false"> - - <RelativeLayout - android:id="@+id/container" - android:layout_width="@dimen/overlay_prompt_container_width" - android:layout_height="wrap_content" - android:layout_gravity="bottom|center" - android:background="@android:color/white" - android:clickable="true" - android:orientation="vertical"> - - <ImageView - android:id="@+id/close" - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_alignParentRight="true" - android:layout_marginLeft="10dp" - android:layout_marginRight="30dp" - android:layout_marginTop="30dp" - android:ellipsize="end" - android:maxLines="2" - android:padding="6dp" - android:src="@drawable/tab_close_active" /> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="6dp" - android:layout_marginLeft="30dp" - android:layout_marginTop="30dp" - android:layout_toLeftOf="@id/close" - android:fontFamily="sans-serif-light" - android:textColor="@color/text_and_tabs_tray_grey" - android:textSize="20sp" - tools:text="The Pokedex" /> - - <TextView - android:id="@+id/host" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/title" - android:layout_marginBottom="20dp" - android:layout_marginLeft="30dp" - android:layout_marginRight="30dp" - android:ellipsize="end" - android:maxLines="1" - android:textColor="@color/placeholder_grey" - android:textSize="16sp" - tools:text="pokedex.org" /> - - <ImageView - android:id="@+id/icon" - android:layout_width="50dp" - android:layout_height="50dp" - android:layout_below="@id/host" - android:layout_marginBottom="20dp" - android:layout_marginLeft="30dp" - android:src="@drawable/icon" /> - - <Button - android:id="@+id/add" - style="@style/Widget.BaseButton" - android:layout_width="wrap_content" - android:layout_height="50dp" - android:layout_alignParentRight="true" - android:layout_below="@id/host" - android:layout_marginBottom="20dp" - android:layout_marginLeft="100dp" - android:layout_marginRight="30dp" - android:background="@drawable/button_background_action_orange_round" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:text="@string/promotion_add_to_homescreen" - android:maxLines="2" - android:ellipsize="end" - android:textColor="@android:color/white" - android:textSize="16sp" /> - - </RelativeLayout> -</merge> diff --git a/mobile/android/services/src/main/res/layout/simple_helper_ui.xml b/mobile/android/services/src/main/res/layout/simple_helper_ui.xml deleted file mode 100644 index f549d5c31..000000000 --- a/mobile/android/services/src/main/res/layout/simple_helper_ui.xml +++ /dev/null @@ -1,61 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<merge xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipChildren="false" - android:clipToPadding="false"> - - <LinearLayout - android:id="@+id/container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@android:color/white" - android:layout_gravity="bottom|center" - android:clickable="true" - android:orientation="vertical"> - - <ImageView - android:id="@+id/image" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="40dp" - android:layout_marginBottom="40dp" - android:scaleType="fitCenter" - android:layout_gravity="center" - android:adjustViewBounds="true"/> - - <TextView - android:id="@+id/title" - android:layout_width="@dimen/firstrun_content_width" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:gravity="center" - android:textAppearance="@style/TextAppearance.FirstrunLight.Main"/> - - - <TextView - android:id="@+id/message" - android:layout_width="@dimen/firstrun_content_width" - android:layout_height="wrap_content" - android:paddingTop="20dp" - android:paddingBottom="30dp" - android:layout_gravity="center" - android:gravity="center" - android:textAppearance="@style/TextAppearance.FirstrunRegular.Body" - android:singleLine="false"/> - - <Button - android:id="@+id/button" - style="@style/Widget.Firstrun.Button" - android:background="@drawable/button_background_action_orange_round" - android:layout_gravity="center" - android:layout_marginBottom="30dp"/> - - </LinearLayout> -</merge> diff --git a/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml b/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml deleted file mode 100644 index 16f72a7ca..000000000 --- a/mobile/android/services/src/main/res/menu/fxaccount_status_menu.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" > - <item - android:id="@+id/enable_debug_mode" - android:checkable="true" - android:checked="false" - android:title="@string/fxaccount_enable_debug_mode" /> -</menu> diff --git a/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml b/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml deleted file mode 100644 index 5c0a23db5..000000000 --- a/mobile/android/services/src/main/res/values-v11/fxaccount_styles.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - - <!-- FxAccountStatusActivity ActionBar --> - <style name="ActionBar.FxAccountStatusActivity"> - <item name="android:displayOptions">showHome|homeAsUp|showTitle</item> - </style> - - <style name="FxAccountTheme" parent="Gecko.Preferences" /> - - <style name="FxAccountTheme.FxAccountStatusActivity" parent="Gecko.Preferences"> - <item name="android:actionBarStyle">@style/ActionBar.FxAccountStatusActivity</item> - </style> - -</resources> diff --git a/mobile/android/services/src/main/res/values/fxaccount_colors.xml b/mobile/android/services/src/main/res/values/fxaccount_colors.xml deleted file mode 100644 index f7140faff..000000000 --- a/mobile/android/services/src/main/res/values/fxaccount_colors.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - <color name="fxaccount_textColor">#424f59</color> - <color name="fxaccount_error_preference_backgroundcolor">#fad4d2</color> -</resources> diff --git a/mobile/android/services/src/main/res/values/fxaccount_dimens.xml b/mobile/android/services/src/main/res/values/fxaccount_dimens.xml deleted file mode 100644 index d1d44585d..000000000 --- a/mobile/android/services/src/main/res/values/fxaccount_dimens.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<resources> - <!-- Preference fragment padding, bottom --> - <dimen name="preference_fragment_padding_bottom">0dp</dimen> - <!-- Preference fragment padding, sides --> - <dimen name="preference_fragment_padding_side">16dp</dimen> - - <integer name="preference_fragment_scrollbarStyle">0x02000000</integer> <!-- outsideOverlay --> - - <!-- Profile avatar image height. --> - <dimen name="fxaccount_profile_image_height">48dp</dimen> - <!-- Profile avatar image width. --> - <dimen name="fxaccount_profile_image_width">48dp</dimen> -</resources> diff --git a/mobile/android/services/src/main/res/values/fxaccount_styles.xml b/mobile/android/services/src/main/res/values/fxaccount_styles.xml deleted file mode 100644 index d74efac91..000000000 --- a/mobile/android/services/src/main/res/values/fxaccount_styles.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - - <style name="FxAccountTheme" parent="Gecko.Preferences" /> - - <style name="FxAccountTheme.FxAccountStatusActivity" parent="@style/FxAccountTheme"> - <item name="android:windowNoTitle">false</item> - </style> - - <style name="FxAccountTextItem" parent="@android:style/TextAppearance.Medium"> - <item name="android:textColor">@color/fxaccount_textColor</item> - <item name="android:layout_width">fill_parent</item> - <item name="android:layout_height">wrap_content</item> - <item name="android:gravity">center_horizontal</item> - <item name="android:textSize">14sp</item> - <item name="android:layout_marginBottom">10dp</item> - <item name="android:layout_marginLeft">10dp</item> - <item name="android:layout_marginRight">10dp</item> - </style> - -</resources> diff --git a/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml b/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml deleted file mode 100644 index 7b004e209..000000000 --- a/mobile/android/services/src/main/res/xml/fxaccount_authenticator.xml +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" - android:accountType="@string/moz_android_shared_fxaccount_type" - android:icon="@drawable/icon" - android:smallIcon="@drawable/icon" - android:label="@string/fxaccount_label" - android:accountPreferences="@xml/fxaccount_options" /> diff --git a/mobile/android/services/src/main/res/xml/fxaccount_options.xml b/mobile/android/services/src/main/res/xml/fxaccount_options.xml deleted file mode 100644 index 449fc0545..000000000 --- a/mobile/android/services/src/main/res/xml/fxaccount_options.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> - <PreferenceCategory - android:title="@string/fxaccount_options_title" /> - <PreferenceScreen - android:key="options" - android:title="@string/fxaccount_options_configure_title"> - <intent - android:action="android.intent.action.MAIN" - android:targetPackage="@string/android_package_name_for_ui" - android:targetClass="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"> - </intent> - </PreferenceScreen> -</PreferenceScreen> diff --git a/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml b/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml deleted file mode 100644 index 570e362cc..000000000 --- a/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml +++ /dev/null @@ -1,142 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:gecko="http://schemas.android.com/apk/res-auto" - android:key="status_screen"> - - <PreferenceCategory - android:key="signed_in_as_category" - android:title="@string/fxaccount_status_signed_in_as" > - <Preference - android:editable="false" - android:key="profile" - android:icon="@drawable/sync_avatar_default" - android:persistent="false" - android:title="" /> - <Preference - android:editable="false" - android:key="manage_account" - android:persistent="false" - android:title="@string/fxaccount_status_manage_account" /> - <Preference - android:editable="false" - android:key="auth_server" - android:persistent="false" - android:title="@string/fxaccount_status_auth_server" /> - </PreferenceCategory> - <PreferenceCategory - android:key="sync_category" - android:title="@string/fxaccount_status_sync" > - <Preference - android:editable="false" - android:icon="@drawable/fxaccount_sync_error" - android:key="needs_credentials" - android:layout="@layout/fxaccount_status_error_preference" - android:persistent="false" - android:title="@string/fxaccount_status_needs_credentials" /> - <Preference - android:editable="false" - android:icon="@drawable/fxaccount_sync_error" - android:key="needs_upgrade" - android:layout="@layout/fxaccount_status_error_preference" - android:persistent="false" - android:title="@string/fxaccount_status_needs_upgrade" /> - <Preference - android:editable="false" - android:icon="@drawable/fxaccount_sync_error" - android:key="needs_verification" - android:layout="@layout/fxaccount_status_error_preference" - android:persistent="false" - android:title="@string/fxaccount_status_needs_verification" /> - <Preference - android:editable="false" - android:icon="@drawable/fxaccount_sync_error" - android:key="needs_master_sync_automatically_enabled" - android:layout="@layout/fxaccount_status_error_preference" - android:persistent="false" - android:title="@string/fxaccount_status_needs_master_sync_automatically_enabled" /> - <Preference - android:editable="false" - android:icon="@drawable/fxaccount_sync_error" - android:key="needs_finish_migrating" - android:layout="@layout/fxaccount_status_error_preference" - android:persistent="false" - android:title="@string/fxaccount_status_needs_finish_migrating" /> - - <Preference - android:editable="false" - android:key="sync_now" - android:defaultValue="" - android:persistent="false" - android:title="@string/fxaccount_status_sync_now" - android:summary="" /> - - <CheckBoxPreference - android:key="bookmarks" - android:persistent="false" - android:title="@string/fxaccount_status_bookmarks" /> - <CheckBoxPreference - android:key="history" - android:persistent="false" - android:title="@string/fxaccount_status_history" /> - <CheckBoxPreference - android:key="tabs" - android:persistent="false" - android:title="@string/fxaccount_status_tabs" /> - <CheckBoxPreference - android:key="passwords" - android:persistent="false" - android:title="@string/fxaccount_status_passwords" /> - - <EditTextPreference - android:singleLine="true" - android:key="device_name" - android:persistent="false" - android:title="@string/fxaccount_status_device_name" /> - - <Preference - android:editable="false" - android:key="sync_server" - android:persistent="false" - android:title="@string/fxaccount_status_sync_server" /> - <org.mozilla.gecko.fxa.activities.CustomColorPreference - android:editable="false" - android:key="remove_account" - android:persistent="false" - gecko:titleColor="@color/rejection_red" - android:title="@string/fxaccount_remove_account" /> - <Preference - android:editable="false" - android:key="more" - android:persistent="false" - android:title="@string/fxaccount_status_more" /> - - </PreferenceCategory> - <PreferenceCategory - android:key="legal_category" - android:title="@string/fxaccount_status_legal" > - <Preference - android:editable="false" - android:key="linktos" - android:persistent="false" - android:title="@string/fxaccount_status_linktos" /> - <Preference - android:editable="false" - android:key="linkprivacy" - android:persistent="false" - android:title="@string/fxaccount_status_linkprivacy" /> - </PreferenceCategory> - <PreferenceCategory - android:key="debug_category" > - <Preference android:key="debug_refresh" /> - <Preference android:key="debug_dump" /> - <Preference android:key="debug_force_sync" /> - <Preference android:key="debug_invalidate_certificate" /> - <Preference android:key="debug_forget_certificate" /> - <Preference android:key="debug_require_password" /> - <Preference android:key="debug_require_upgrade" /> - <Preference android:key="debug_migrated_from_sync11" /> - <Preference android:key="debug_make_account_stage" /> - <Preference android:key="debug_make_account_default" /> - </PreferenceCategory> - -</PreferenceScreen> diff --git a/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml b/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml deleted file mode 100644 index 761920667..000000000 --- a/mobile/android/services/src/main/res/xml/fxaccount_syncadapter.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" - android:accountType="@string/moz_android_shared_fxaccount_type" - android:contentAuthority="@string/content_authority_db_browser" - android:isAlwaysSyncable="true" - android:supportsUploading="true" - android:userVisible="true" -/> diff --git a/mobile/android/services/strings.xml.in b/mobile/android/services/strings.xml.in deleted file mode 100644 index 143a3db42..000000000 --- a/mobile/android/services/strings.xml.in +++ /dev/null @@ -1,86 +0,0 @@ -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<!-- Configure Engines --> -<string name="sync_configure_engines_title_passwords">&sync.configure.engines.title.passwords2;</string> -<string name="sync_configure_engines_title_history">&sync.configure.engines.title.history;</string> -<string name="sync_configure_engines_title_tabs">&sync.configure.engines.title.tabs;</string> - -<!-- Bookmark folder strings --> -<string name="bookmarks_folder_menu">&bookmarks.folder.menu.label;</string> -<string name="bookmarks_folder_places">&bookmarks.folder.places.label;</string> -<string name="bookmarks_folder_tags">&bookmarks.folder.tags.label;</string> -<string name="bookmarks_folder_toolbar">&bookmarks.folder.toolbar.label;</string> -<string name="bookmarks_folder_unfiled">&bookmarks.folder.other.label;</string> -<string name="bookmarks_folder_desktop">&bookmarks.folder.desktop.label;</string> -<string name="bookmarks_folder_mobile">&bookmarks.folder.mobile.label;</string> -<string name="bookmarks_folder_pinned">&bookmarks.folder.pinned.label;</string> - -<!-- Send tab to device. --> -<string name="sync_default_client_name">&sync.default.client.name;</string> - -<!-- Firefox Account links. --> -<string name="fxaccount_link_tos">https://accounts.firefox.com/legal/terms</string> -<string name="fxaccount_link_pn">https://accounts.firefox.com/legal/privacy</string> - -<string name="fxaccount_getting_started_welcome_to_sync">&fxaccount_getting_started_welcome_to_sync;</string> -<string name="fxaccount_getting_started_description">&fxaccount_getting_started_description2;</string> -<string name="fxaccount_getting_started_get_started">&fxaccount_getting_started_get_started;</string> - -<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string> -<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string> -<string name="fxaccount_status_manage_account">&fxaccount_status_manage_account;</string> -<string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string> -<string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string> -<string name="fxaccount_status_syncing">&fxaccount_status_syncing2;</string> -<string name="fxaccount_status_last_synced">&remote_tabs_last_synced;</string> -<string name="fxaccount_status_never_synced">&remote_tabs_never_synced;</string> -<string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string> -<string name="fxaccount_status_sync_server">&fxaccount_status_sync_server;</string> -<string name="fxaccount_status_sync">&fxaccount_status_sync;</string> -<string name="fxaccount_status_sync_enabled">&fxaccount_status_sync_enabled;</string> -<string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string> -<string name="fxaccount_status_needs_credentials">&fxaccount_status_needs_credentials;</string> -<string name="fxaccount_status_needs_upgrade">&fxaccount_status_needs_upgrade;</string> -<string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string> -<string name="fxaccount_status_needs_master_sync_automatically_enabled_v21">&fxaccount_status_needs_master_sync_automatically_enabled_v21;</string> -<string name="fxaccount_status_needs_finish_migrating">&fxaccount_status_needs_finish_migrating;</string> -<string name="fxaccount_status_bookmarks">&fxaccount_status_bookmarks;</string> -<string name="fxaccount_status_history">&fxaccount_status_history;</string> -<string name="fxaccount_status_passwords">&fxaccount_status_passwords2;</string> -<string name="fxaccount_status_tabs">&fxaccount_status_tabs;</string> -<string name="fxaccount_status_legal">&fxaccount_status_legal;</string> -<string name="fxaccount_status_linktos">&fxaccount_status_linktos2;</string> -<string name="fxaccount_status_linkprivacy">&fxaccount_status_linkprivacy2;</string> -<string name="fxaccount_status_more">&fxaccount_status_more;</string> -<string name="fxaccount_remove_account">&fxaccount_remove_account;</string> - -<string name="fxaccount_label">&fxaccount_account_type_label;</string> - -<string name="fxaccount_options_title">&fxaccount_options_title;</string> -<string name="fxaccount_options_configure_title">&fxaccount_options_configure_title;</string> - -<string name="fxaccount_remote_error_UPGRADE_REQUIRED">&fxaccount_remote_error_UPGRADE_REQUIRED;</string> -<string name="fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS">&fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS_2;</string> -<string name="fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST">&fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;</string> -<string name="fxaccount_remote_error_INCORRECT_PASSWORD">&fxaccount_remote_error_INCORRECT_PASSWORD;</string> -<string name="fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT">&fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;</string> -<string name="fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS">&fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;</string> -<string name="fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD">&fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;</string> -<string name="fxaccount_remote_error_UNKNOWN_ERROR">&fxaccount_remote_error_UNKNOWN_ERROR;</string> -<string name="fxaccount_remote_error_ACCOUNT_LOCKED">&fxaccount_remote_error_ACCOUNT_LOCKED;</string> - -<string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string> -<string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string> - -<!-- Remove Account --> -<string name="fxaccount_remove_account_dialog_title">&fxaccount_remove_account_dialog_title;</string> -<string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string> -<string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string> - -<string name="fxaccount_sync_finish_migrating_notification_title">&fxaccount_sync_finish_migrating_notification_title;</string> -<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string> - -<!-- Log Personal information --> -<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string> diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index aed20c854..10f9b994a 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -5151,9 +5151,6 @@ pref("layout.accessiblecaret.hide_carets_for_mouse_input", true); // Wakelock is disabled by default. pref("dom.wakelock.enabled", false); -// The URL of the Firefox Accounts auth server backend -pref("identity.fxaccounts.auth.uri", "https://api.accounts.firefox.com/v1"); - // disable mozsample size for now pref("image.mozsamplesize.enabled", false); diff --git a/services/fxaccounts/Credentials.jsm b/services/fxaccounts/Credentials.jsm deleted file mode 100644 index 56e8b3db7..000000000 --- a/services/fxaccounts/Credentials.jsm +++ /dev/null @@ -1,136 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * This module implements client-side key stretching for use in Firefox - * Accounts account creation and login. - * - * See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol - */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["Credentials"]; - -const {utils: Cu, interfaces: Ci} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-common/utils.js"); - -const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/"; -const PBKDF2_ROUNDS = 1000; -const STRETCHED_PW_LENGTH_BYTES = 32; -const HKDF_SALT = CommonUtils.hexToBytes("00"); -const HKDF_LENGTH = 32; -const HMAC_ALGORITHM = Ci.nsICryptoHMAC.SHA256; -const HMAC_LENGTH = 32; - -// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO", -// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by -// default. -const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; -try { - this.LOG_LEVEL = - Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL); -} catch (e) { - this.LOG_LEVEL = Log.Level.Error; -} - -var log = Log.repository.getLogger("Identity.FxAccounts"); -log.level = LOG_LEVEL; -log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); - -this.Credentials = Object.freeze({ - /** - * Make constants accessible to tests - */ - constants: { - PROTOCOL_VERSION: PROTOCOL_VERSION, - PBKDF2_ROUNDS: PBKDF2_ROUNDS, - STRETCHED_PW_LENGTH_BYTES: STRETCHED_PW_LENGTH_BYTES, - HKDF_SALT: HKDF_SALT, - HKDF_LENGTH: HKDF_LENGTH, - HMAC_ALGORITHM: HMAC_ALGORITHM, - HMAC_LENGTH: HMAC_LENGTH, - }, - - /** - * KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol - * - * keyWord derivation for use as a salt. - * - * - * @param {String} context String for use in generating salt - * - * @return {bitArray} the salt - * - * Note that PROTOCOL_VERSION does not refer in any way to the version of the - * Firefox Accounts API. - */ - keyWord: function(context) { - return CommonUtils.stringToBytes(PROTOCOL_VERSION + context); - }, - - /** - * KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol - * - * keyWord extended with a name and an email. - * - * @param {String} name The name of the salt - * @param {String} email The email of the user. - * - * @return {bitArray} the salt combination with the namespace - * - * Note that PROTOCOL_VERSION does not refer in any way to the version of the - * Firefox Accounts API. - */ - keyWordExtended: function(name, email) { - return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ':' + email); - }, - - setup: function(emailInput, passwordInput, options={}) { - let deferred = Promise.defer(); - log.debug("setup credentials for " + emailInput); - - let hkdfSalt = options.hkdfSalt || HKDF_SALT; - let hkdfLength = options.hkdfLength || HKDF_LENGTH; - let hmacLength = options.hmacLength || HMAC_LENGTH; - let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM; - let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES; - let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS; - - let result = {}; - - let password = CommonUtils.encodeUTF8(passwordInput); - let salt = this.keyWordExtended("quickStretch", emailInput); - - let runnable = () => { - let start = Date.now(); - let quickStretchedPW = CryptoUtils.pbkdf2Generate( - password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength); - - result.quickStretchedPW = quickStretchedPW; - - result.authPW = - CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength); - - result.unwrapBKey = - CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength); - - log.debug("Credentials set up after " + (Date.now() - start) + " ms"); - deferred.resolve(result); - } - - Services.tm.currentThread.dispatch(runnable, - Ci.nsIThread.DISPATCH_NORMAL); - log.debug("Dispatched thread for credentials setup crypto work"); - - return deferred.promise; - } -}); - diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm deleted file mode 100644 index 5bed881ea..000000000 --- a/services/fxaccounts/FxAccounts.jsm +++ /dev/null @@ -1,1735 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Timer.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", - "resource://gre/modules/FxAccountsClient.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsConfig", - "resource://gre/modules/FxAccountsConfig.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", - "resource://gre/modules/identity/jwcrypto.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient", - "resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile", - "resource://gre/modules/FxAccountsProfile.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://services-sync/util.js"); - -// All properties exposed by the public FxAccounts API. -var publicProperties = [ - "accountStatus", - "checkVerificationStatus", - "getAccountsClient", - "getAssertion", - "getDeviceId", - "getKeys", - "getOAuthToken", - "getSignedInUser", - "getSignedInUserProfile", - "handleDeviceDisconnection", - "invalidateCertificate", - "loadAndPoll", - "localtimeOffsetMsec", - "notifyDevices", - "now", - "promiseAccountsChangeProfileURI", - "promiseAccountsForceSigninURI", - "promiseAccountsManageURI", - "promiseAccountsSignUpURI", - "promiseAccountsSignInURI", - "removeCachedOAuthToken", - "requiresHttps", - "resendVerificationEmail", - "resetCredentials", - "sessionStatus", - "setSignedInUser", - "signOut", - "updateDeviceRegistration", - "updateUserAccountData", - "whenVerified", -]; - -// An AccountState object holds all state related to one specific account. -// Only one AccountState is ever "current" in the FxAccountsInternal object - -// whenever a user logs out or logs in, the current AccountState is discarded, -// making it impossible for the wrong state or state data to be accidentally -// used. -// In addition, it has some promise-related helpers to ensure that if an -// attempt is made to resolve a promise on a "stale" state (eg, if an -// operation starts, but a different user logs in before the operation -// completes), the promise will be rejected. -// It is intended to be used thusly: -// somePromiseBasedFunction: function() { -// let currentState = this.currentAccountState; -// return someOtherPromiseFunction().then( -// data => currentState.resolve(data) -// ); -// } -// If the state has changed between the function being called and the promise -// being resolved, the .resolve() call will actually be rejected. -var AccountState = this.AccountState = function(storageManager) { - this.storageManager = storageManager; - this.promiseInitialized = this.storageManager.getAccountData().then(data => { - this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {}; - }).catch(err => { - log.error("Failed to initialize the storage manager", err); - // Things are going to fall apart, but not much we can do about it here. - }); -}; - -AccountState.prototype = { - oauthTokens: null, - whenVerifiedDeferred: null, - whenKeysReadyDeferred: null, - - // If the storage manager has been nuked then we are no longer current. - get isCurrent() { - return this.storageManager != null; - }, - - abort() { - if (this.whenVerifiedDeferred) { - this.whenVerifiedDeferred.reject( - new Error("Verification aborted; Another user signing in")); - this.whenVerifiedDeferred = null; - } - - if (this.whenKeysReadyDeferred) { - this.whenKeysReadyDeferred.reject( - new Error("Verification aborted; Another user signing in")); - this.whenKeysReadyDeferred = null; - } - - this.cert = null; - this.keyPair = null; - this.oauthTokens = null; - // Avoid finalizing the storageManager multiple times (ie, .signOut() - // followed by .abort()) - if (!this.storageManager) { - return Promise.resolve(); - } - let storageManager = this.storageManager; - this.storageManager = null; - return storageManager.finalize(); - }, - - // Clobber all cached data and write that empty data to storage. - signOut() { - this.cert = null; - this.keyPair = null; - this.oauthTokens = null; - let storageManager = this.storageManager; - this.storageManager = null; - return storageManager.deleteAccountData().then(() => { - return storageManager.finalize(); - }); - }, - - // Get user account data. Optionally specify explicit field names to fetch - // (and note that if you require an in-memory field you *must* specify the - // field name(s).) - getUserAccountData(fieldNames = null) { - if (!this.isCurrent) { - return Promise.reject(new Error("Another user has signed in")); - } - return this.storageManager.getAccountData(fieldNames).then(result => { - return this.resolve(result); - }); - }, - - updateUserAccountData(updatedFields) { - if (!this.isCurrent) { - return Promise.reject(new Error("Another user has signed in")); - } - return this.storageManager.updateAccountData(updatedFields); - }, - - resolve: function(result) { - if (!this.isCurrent) { - log.info("An accountState promise was resolved, but was actually rejected" + - " due to a different user being signed in. Originally resolved" + - " with", result); - return Promise.reject(new Error("A different user signed in")); - } - return Promise.resolve(result); - }, - - reject: function(error) { - // It could be argued that we should just let it reject with the original - // error - but this runs the risk of the error being (eg) a 401, which - // might cause the consumer to attempt some remediation and cause other - // problems. - if (!this.isCurrent) { - log.info("An accountState promise was rejected, but we are ignoring that" + - "reason and rejecting it due to a different user being signed in." + - "Originally rejected with", error); - return Promise.reject(new Error("A different user signed in")); - } - return Promise.reject(error); - }, - - // Abstractions for storage of cached tokens - these are all sync, and don't - // handle revocation etc - it's just storage (and the storage itself is async, - // but we don't return the storage promises, so it *looks* sync) - // These functions are sync simply so we can handle "token races" - when there - // are multiple in-flight requests for the same scope, we can detect this - // and revoke the redundant token. - - // A preamble for the cache helpers... - _cachePreamble() { - if (!this.isCurrent) { - throw new Error("Another user has signed in"); - } - }, - - // Set a cached token. |tokenData| must have a 'token' element, but may also - // have additional fields (eg, it probably specifies the server to revoke - // from). The 'get' functions below return the entire |tokenData| value. - setCachedToken(scopeArray, tokenData) { - this._cachePreamble(); - if (!tokenData.token) { - throw new Error("No token"); - } - let key = getScopeKey(scopeArray); - this.oauthTokens[key] = tokenData; - // And a background save... - this._persistCachedTokens(); - }, - - // Return data for a cached token or null (or throws on bad state etc) - getCachedToken(scopeArray) { - this._cachePreamble(); - let key = getScopeKey(scopeArray); - let result = this.oauthTokens[key]; - if (result) { - // later we might want to check an expiry date - but we currently - // have no such concept, so just return it. - log.trace("getCachedToken returning cached token"); - return result; - } - return null; - }, - - // Remove a cached token from the cache. Does *not* revoke it from anywhere. - // Returns the entire token entry if found, null otherwise. - removeCachedToken(token) { - this._cachePreamble(); - let data = this.oauthTokens; - for (let [key, tokenValue] of Object.entries(data)) { - if (tokenValue.token == token) { - delete data[key]; - // And a background save... - this._persistCachedTokens(); - return tokenValue; - } - } - return null; - }, - - // A hook-point for tests. Returns a promise that's ignored in most cases - // (notable exceptions are tests and when we explicitly are saving the entire - // set of user data.) - _persistCachedTokens() { - this._cachePreamble(); - return this.updateUserAccountData({ oauthTokens: this.oauthTokens }).catch(err => { - log.error("Failed to update cached tokens", err); - }); - }, -} - -/* Given an array of scopes, make a string key by normalizing. */ -function getScopeKey(scopeArray) { - let normalizedScopes = scopeArray.map(item => item.toLowerCase()); - return normalizedScopes.sort().join("|"); -} - -/** - * Copies properties from a given object to another object. - * - * @param from (object) - * The object we read property descriptors from. - * @param to (object) - * The object that we set property descriptors on. - * @param options (object) (optional) - * {keys: [...]} - * Lets the caller pass the names of all properties they want to be - * copied. Will copy all properties of the given source object by - * default. - * {bind: object} - * Lets the caller specify the object that will be used to .bind() - * all function properties we find to. Will bind to the given target - * object by default. - */ -function copyObjectProperties(from, to, opts = {}) { - let keys = (opts && opts.keys) || Object.keys(from); - let thisArg = (opts && opts.bind) || to; - - for (let prop of keys) { - let desc = Object.getOwnPropertyDescriptor(from, prop); - - if (typeof(desc.value) == "function") { - desc.value = desc.value.bind(thisArg); - } - - if (desc.get) { - desc.get = desc.get.bind(thisArg); - } - - if (desc.set) { - desc.set = desc.set.bind(thisArg); - } - - Object.defineProperty(to, prop, desc); - } -} - -function urlsafeBase64Encode(key) { - return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false }); -} - -/** - * The public API's constructor. - */ -this.FxAccounts = function (mockInternal) { - let internal = new FxAccountsInternal(); - let external = {}; - - // Copy all public properties to the 'external' object. - let prototype = FxAccountsInternal.prototype; - let options = {keys: publicProperties, bind: internal}; - copyObjectProperties(prototype, external, options); - - // Copy all of the mock's properties to the internal object. - if (mockInternal && !mockInternal.onlySetInternal) { - copyObjectProperties(mockInternal, internal); - } - - if (mockInternal) { - // Exposes the internal object for testing only. - external.internal = internal; - } - - if (!internal.fxaPushService) { - // internal.fxaPushService option is used in testing. - // Otherwise we load the service lazily. - XPCOMUtils.defineLazyGetter(internal, "fxaPushService", function () { - return Components.classes["@mozilla.org/fxaccounts/push;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - }); - } - - // wait until after the mocks are setup before initializing. - internal.initialize(); - - return Object.freeze(external); -} - -/** - * The internal API's constructor. - */ -function FxAccountsInternal() { - // Make a local copy of this constant so we can mock it in testing - this.POLL_SESSION = POLL_SESSION; - - // All significant initialization should be done in the initialize() method - // below as it helps with testing. -} - -/** - * The internal API's prototype. - */ -FxAccountsInternal.prototype = { - // The timeout (in ms) we use to poll for a verified mail for the first 2 mins. - VERIFICATION_POLL_TIMEOUT_INITIAL: 15000, // 15 seconds - // And how often we poll after the first 2 mins. - VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 30000, // 30 seconds. - // The current version of the device registration, we use this to re-register - // devices after we update what we send on device registration. - DEVICE_REGISTRATION_VERSION: 2, - - _fxAccountsClient: null, - - // All significant initialization should be done in this initialize() method, - // as it's called after this object has been mocked for tests. - initialize() { - this.currentTimer = null; - this.currentAccountState = this.newAccountState(); - }, - - get fxAccountsClient() { - if (!this._fxAccountsClient) { - this._fxAccountsClient = new FxAccountsClient(); - } - return this._fxAccountsClient; - }, - - // The profile object used to fetch the actual user profile. - _profile: null, - get profile() { - if (!this._profile) { - let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri"); - this._profile = new FxAccountsProfile({ - fxa: this, - profileServerUrl: profileServerUrl, - }); - } - return this._profile; - }, - - // A hook-point for tests who may want a mocked AccountState or mocked storage. - newAccountState(credentials) { - let storage = new FxAccountsStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - - /** - * Send a message to a set of devices in the same account - * - * @return Promise - */ - notifyDevices: function(deviceIds, payload, TTL) { - if (!Array.isArray(deviceIds)) { - deviceIds = [deviceIds]; - } - return this.currentAccountState.getUserAccountData() - .then(data => { - if (!data) { - throw this._error(ERROR_NO_ACCOUNT); - } - if (!data.sessionToken) { - throw this._error(ERROR_AUTH_ERROR, - "notifyDevices called without a session token"); - } - return this.fxAccountsClient.notifyDevices(data.sessionToken, deviceIds, - payload, TTL); - }); - }, - - /** - * Return the current time in milliseconds as an integer. Allows tests to - * manipulate the date to simulate certificate expiration. - */ - now: function() { - return this.fxAccountsClient.now(); - }, - - getAccountsClient: function() { - return this.fxAccountsClient; - }, - - /** - * Return clock offset in milliseconds, as reported by the fxAccountsClient. - * This can be overridden for testing. - * - * The offset is the number of milliseconds that must be added to the client - * clock to make it equal to the server clock. For example, if the client is - * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. - */ - get localtimeOffsetMsec() { - return this.fxAccountsClient.localtimeOffsetMsec; - }, - - /** - * Ask the server whether the user's email has been verified - */ - checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) { - if (!sessionToken) { - return Promise.reject(new Error( - "checkEmailStatus called without a session token")); - } - return this.fxAccountsClient.recoveryEmailStatus(sessionToken, - options).catch(error => this._handleTokenError(error)); - }, - - /** - * Once the user's email is verified, we can request the keys - */ - fetchKeys: function fetchKeys(keyFetchToken) { - log.debug("fetchKeys: " + !!keyFetchToken); - if (logPII) { - log.debug("fetchKeys - the token is " + keyFetchToken); - } - return this.fxAccountsClient.accountKeys(keyFetchToken); - }, - - // set() makes sure that polling is happening, if necessary. - // get() does not wait for verification, and returns an object even if - // unverified. The caller of get() must check .verified . - // The "fxaccounts:onverified" event will fire only when the verified - // state goes from false to true, so callers must register their observer - // and then call get(). In particular, it will not fire when the account - // was found to be verified in a previous boot: if our stored state says - // the account is verified, the event will never fire. So callers must do: - // register notification observer (go) - // userdata = get() - // if (userdata.verified()) {go()} - - /** - * Get the user currently signed in to Firefox Accounts. - * - * @return Promise - * The promise resolves to the credentials object of the signed-in user: - * { - * email: The user's email address - * uid: The user's unique id - * sessionToken: Session for the FxA server - * kA: An encryption key from the FxA server - * kB: An encryption key derived from the user's FxA password - * verified: email verification status - * authAt: The time (seconds since epoch) that this record was - * authenticated - * } - * or null if no user is signed in. - */ - getSignedInUser: function getSignedInUser() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - return null; - } - if (!this.isUserEmailVerified(data)) { - // If the email is not verified, start polling for verification, - // but return null right away. We don't want to return a promise - // that might not be fulfilled for a long time. - this.startVerifiedCheck(data); - } - return data; - }).then(result => currentState.resolve(result)); - }, - - /** - * Set the current user signed in to Firefox Accounts. - * - * @param credentials - * The credentials object obtained by logging in or creating - * an account on the FxA server: - * { - * authAt: The time (seconds since epoch) that this record was - * authenticated - * email: The users email address - * keyFetchToken: a keyFetchToken which has not yet been used - * sessionToken: Session for the FxA server - * uid: The user's unique id - * unwrapBKey: used to unwrap kB, derived locally from the - * password (not revealed to the FxA server) - * verified: true/false - * } - * @return Promise - * The promise resolves to null when the data is saved - * successfully and is rejected on error. - */ - setSignedInUser: function setSignedInUser(credentials) { - log.debug("setSignedInUser - aborting any existing flows"); - return this.abortExistingFlow().then(() => { - let currentAccountState = this.currentAccountState = this.newAccountState( - Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object. - ); - // This promise waits for storage, but not for verification. - // We're telling the caller that this is durable now (although is that - // really something we should commit to? Why not let the write happen in - // the background? Already does for updateAccountData ;) - return currentAccountState.promiseInitialized.then(() => { - // Starting point for polling if new user - if (!this.isUserEmailVerified(credentials)) { - this.startVerifiedCheck(credentials); - } - - return this.updateDeviceRegistration(); - }).then(() => { - Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1); - this.notifyObservers(ONLOGIN_NOTIFICATION); - }).then(() => { - return currentAccountState.resolve(); - }); - }) - }, - - /** - * Update account data for the currently signed in user. - * - * @param credentials - * The credentials object containing the fields to be updated. - * This object must contain |email| and |uid| fields and they must - * match the currently signed in user. - */ - updateUserAccountData(credentials) { - log.debug("updateUserAccountData called with fields", Object.keys(credentials)); - if (logPII) { - log.debug("updateUserAccountData called with data", credentials); - } - let currentAccountState = this.currentAccountState; - return currentAccountState.promiseInitialized.then(() => { - return currentAccountState.getUserAccountData(["email", "uid"]); - }).then(existing => { - if (existing.email != credentials.email || existing.uid != credentials.uid) { - throw new Error("The specified credentials aren't for the current user"); - } - // We need to nuke email and uid as storage will complain if we try and - // update them (even when the value is the same) - credentials = Cu.cloneInto(credentials, {}); // clone it first - delete credentials.email; - delete credentials.uid; - return currentAccountState.updateUserAccountData(credentials); - }); - }, - - /** - * returns a promise that fires with the assertion. If there is no verified - * signed-in user, fires with null. - */ - getAssertion: function getAssertion(audience) { - return this._getAssertion(audience); - }, - - // getAssertion() is "public" so screws with our mock story. This - // implementation method *can* be (and is) mocked by tests. - _getAssertion: function _getAssertion(audience) { - log.debug("enter getAssertion()"); - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - // No signed-in user - return null; - } - if (!this.isUserEmailVerified(data)) { - // Signed-in user has not verified email - return null; - } - if (!data.sessionToken) { - // can't get a signed certificate without a session token. This - // can happen if we request an assertion after clearing an invalid - // session token from storage. - throw this._error(ERROR_AUTH_ERROR, "getAssertion called without a session token"); - } - return this.getKeypairAndCertificate(currentState).then( - ({keyPair, certificate}) => { - return this.getAssertionFromCert(data, keyPair, certificate, audience); - } - ); - }).catch(err => - this._handleTokenError(err) - ).then(result => currentState.resolve(result)); - }, - - /** - * Invalidate the FxA certificate, so that it will be refreshed from the server - * the next time it is needed. - */ - invalidateCertificate() { - return this.currentAccountState.updateUserAccountData({ cert: null }); - }, - - getDeviceId() { - return this.currentAccountState.getUserAccountData() - .then(data => { - if (data) { - if (!data.deviceId || !data.deviceRegistrationVersion || - data.deviceRegistrationVersion < this.DEVICE_REGISTRATION_VERSION) { - // There is no device id or the device registration is outdated. - // Either way, we should register the device with FxA - // before returning the id to the caller. - return this._registerOrUpdateDevice(data); - } - - // Return the device id that we already registered with the server. - return data.deviceId; - } - - // Without a signed-in user, there can be no device id. - return null; - }); - }, - - /** - * Resend the verification email fot the currently signed-in user. - * - */ - resendVerificationEmail: function resendVerificationEmail() { - let currentState = this.currentAccountState; - return this.getSignedInUser().then(data => { - // If the caller is asking for verification to be re-sent, and there is - // no signed-in user to begin with, this is probably best regarded as an - // error. - if (data) { - if (!data.sessionToken) { - return Promise.reject(new Error( - "resendVerificationEmail called without a session token")); - } - this.pollEmailStatus(currentState, data.sessionToken, "start"); - return this.fxAccountsClient.resendVerificationEmail( - data.sessionToken).catch(err => this._handleTokenError(err)); - } - throw new Error("Cannot resend verification email; no signed-in user"); - }); - }, - - /* - * Reset state such that any previous flow is canceled. - */ - abortExistingFlow: function abortExistingFlow() { - if (this.currentTimer) { - log.debug("Polling aborted; Another user signing in"); - clearTimeout(this.currentTimer); - this.currentTimer = 0; - } - if (this._profile) { - this._profile.tearDown(); - this._profile = null; - } - // We "abort" the accountState and assume our caller is about to throw it - // away and replace it with a new one. - return this.currentAccountState.abort(); - }, - - accountStatus: function accountStatus() { - return this.currentAccountState.getUserAccountData().then(data => { - if (!data) { - return false; - } - return this.fxAccountsClient.accountStatus(data.uid); - }); - }, - - checkVerificationStatus: function() { - log.trace('checkVerificationStatus'); - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then(data => { - if (!data) { - log.trace("checkVerificationStatus - no user data"); - return null; - } - - // Always check the verification status, even if the local state indicates - // we're already verified. If the user changed their password, the check - // will fail, and we'll enter the reauth state. - log.trace("checkVerificationStatus - forcing verification status check"); - return this.pollEmailStatus(currentState, data.sessionToken, "push"); - }); - }, - - _destroyOAuthToken: function(tokenData) { - let client = new FxAccountsOAuthGrantClient({ - serverURL: tokenData.server, - client_id: FX_OAUTH_CLIENT_ID - }); - return client.destroyToken(tokenData.token) - }, - - _destroyAllOAuthTokens: function(tokenInfos) { - // let's just destroy them all in parallel... - let promises = []; - for (let [key, tokenInfo] of Object.entries(tokenInfos || {})) { - promises.push(this._destroyOAuthToken(tokenInfo)); - } - return Promise.all(promises); - }, - - signOut: function signOut(localOnly) { - let currentState = this.currentAccountState; - let sessionToken; - let tokensToRevoke; - let deviceId; - return currentState.getUserAccountData().then(data => { - // Save the session token, tokens to revoke and the - // device id for use in the call to signOut below. - if (data) { - sessionToken = data.sessionToken; - tokensToRevoke = data.oauthTokens; - deviceId = data.deviceId; - } - return this._signOutLocal(); - }).then(() => { - // FxAccountsManager calls here, then does its own call - // to FxAccountsClient.signOut(). - if (!localOnly) { - // Wrap this in a promise so *any* errors in signOut won't - // block the local sign out. This is *not* returned. - Promise.resolve().then(() => { - // This can happen in the background and shouldn't block - // the user from signing out. The server must tolerate - // clients just disappearing, so this call should be best effort. - if (sessionToken) { - return this._signOutServer(sessionToken, deviceId); - } - log.warn("Missing session token; skipping remote sign out"); - }).catch(err => { - log.error("Error during remote sign out of Firefox Accounts", err); - }).then(() => { - return this._destroyAllOAuthTokens(tokensToRevoke); - }).catch(err => { - log.error("Error during destruction of oauth tokens during signout", err); - }).then(() => { - FxAccountsConfig.resetConfigURLs(); - // just for testing - notifications are cheap when no observers. - this.notifyObservers("testhelper-fxa-signout-complete"); - }) - } else { - // We want to do this either way -- but if we're signing out remotely we - // need to wait until we destroy the oauth tokens if we want that to succeed. - FxAccountsConfig.resetConfigURLs(); - } - }).then(() => { - this.notifyObservers(ONLOGOUT_NOTIFICATION); - }); - }, - - /** - * This function should be called in conjunction with a server-side - * signOut via FxAccountsClient. - */ - _signOutLocal: function signOutLocal() { - let currentAccountState = this.currentAccountState; - return currentAccountState.signOut().then(() => { - // this "aborts" this.currentAccountState but doesn't make a new one. - return this.abortExistingFlow(); - }).then(() => { - this.currentAccountState = this.newAccountState(); - return this.currentAccountState.promiseInitialized; - }); - }, - - _signOutServer(sessionToken, deviceId) { - // For now we assume the service being logged out from is Sync, so - // we must tell the server to either destroy the device or sign out - // (if no device exists). We might need to revisit this when this - // FxA code is used in a context that isn't Sync. - - const options = { service: "sync" }; - - if (deviceId) { - log.debug("destroying device and session"); - return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options); - } - - log.debug("destroying session"); - return this.fxAccountsClient.signOut(sessionToken, options); - }, - - /** - * Check the status of the current session using cached credentials. - * - * @return Promise - * Resolves with a boolean indicating if the session is still valid - */ - sessionStatus() { - return this.getSignedInUser().then(data => { - if (!data.sessionToken) { - return Promise.reject(new Error( - "sessionStatus called without a session token")); - } - return this.fxAccountsClient.sessionStatus(data.sessionToken); - }); - }, - - /** - * Fetch encryption keys for the signed-in-user from the FxA API server. - * - * Not for user consumption. Exists to cause the keys to be fetch. - * - * Returns user data so that it can be chained with other methods. - * - * @return Promise - * The promise resolves to the credentials object of the signed-in user: - * { - * email: The user's email address - * uid: The user's unique id - * sessionToken: Session for the FxA server - * kA: An encryption key from the FxA server - * kB: An encryption key derived from the user's FxA password - * verified: email verification status - * } - * or null if no user is signed in - */ - getKeys: function() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData().then((userData) => { - if (!userData) { - throw new Error("Can't get keys; User is not signed in"); - } - if (userData.kA && userData.kB) { - return userData; - } - if (!currentState.whenKeysReadyDeferred) { - currentState.whenKeysReadyDeferred = Promise.defer(); - if (userData.keyFetchToken) { - this.fetchAndUnwrapKeys(userData.keyFetchToken).then( - (dataWithKeys) => { - if (!dataWithKeys.kA || !dataWithKeys.kB) { - currentState.whenKeysReadyDeferred.reject( - new Error("user data missing kA or kB") - ); - return; - } - currentState.whenKeysReadyDeferred.resolve(dataWithKeys); - }, - (err) => { - currentState.whenKeysReadyDeferred.reject(err); - } - ); - } else { - currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); - } - } - return currentState.whenKeysReadyDeferred.promise; - }).catch(err => - this._handleTokenError(err) - ).then(result => currentState.resolve(result)); - }, - - fetchAndUnwrapKeys: function(keyFetchToken) { - if (logPII) { - log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); - } - let currentState = this.currentAccountState; - return Task.spawn(function* task() { - // Sign out if we don't have a key fetch token. - if (!keyFetchToken) { - log.warn("improper fetchAndUnwrapKeys() call: token missing"); - yield this.signOut(); - return null; - } - - let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); - - let data = yield currentState.getUserAccountData(); - - // Sanity check that the user hasn't changed out from under us - if (data.keyFetchToken !== keyFetchToken) { - throw new Error("Signed in user changed while fetching keys!"); - } - - // Next statements must be synchronous until we setUserAccountData - // so that we don't risk getting into a weird state. - let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), - wrapKB); - - if (logPII) { - log.debug("kB_hex: " + kB_hex); - } - let updateData = { - kA: CommonUtils.bytesAsHex(kA), - kB: CommonUtils.bytesAsHex(kB_hex), - keyFetchToken: null, // null values cause the item to be removed. - unwrapBKey: null, - } - - log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB); - if (logPII) { - log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB); - } - - yield currentState.updateUserAccountData(updateData); - // We are now ready for business. This should only be invoked once - // per setSignedInUser(), regardless of whether we've rebooted since - // setSignedInUser() was called. - this.notifyObservers(ONVERIFIED_NOTIFICATION); - return currentState.getUserAccountData(); - }.bind(this)).then(result => currentState.resolve(result)); - }, - - getAssertionFromCert: function(data, keyPair, cert, audience) { - log.debug("getAssertionFromCert"); - let payload = {}; - let d = Promise.defer(); - let options = { - duration: ASSERTION_LIFETIME, - localtimeOffsetMsec: this.localtimeOffsetMsec, - now: this.now() - }; - let currentState = this.currentAccountState; - // "audience" should look like "http://123done.org". - // The generated assertion will expire in two minutes. - jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { - if (err) { - log.error("getAssertionFromCert: " + err); - d.reject(err); - } else { - log.debug("getAssertionFromCert returning signed: " + !!signed); - if (logPII) { - log.debug("getAssertionFromCert returning signed: " + signed); - } - d.resolve(signed); - } - }); - return d.promise.then(result => currentState.resolve(result)); - }, - - getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { - log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); - if (logPII) { - log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); - } - return this.fxAccountsClient.signCertificate( - sessionToken, - JSON.parse(serializedPublicKey), - lifetime - ); - }, - - /** - * returns a promise that fires with {keyPair, certificate}. - */ - getKeypairAndCertificate: Task.async(function* (currentState) { - // If the debugging pref to ignore cached authentication credentials is set for Sync, - // then don't use any cached key pair/certificate, i.e., generate a new - // one and get it signed. - // The purpose of this pref is to expedite any auth errors as the result of a - // expired or revoked FxA session token, e.g., from resetting or changing the FxA - // password. - let ignoreCachedAuthCredentials = false; - try { - ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials"); - } catch(e) { - // Pref doesn't exist - } - let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; - let accountData = yield currentState.getUserAccountData(["cert", "keyPair", "sessionToken"]); - - let keyPairValid = !ignoreCachedAuthCredentials && - accountData.keyPair && - (accountData.keyPair.validUntil > mustBeValidUntil); - let certValid = !ignoreCachedAuthCredentials && - accountData.cert && - (accountData.cert.validUntil > mustBeValidUntil); - // TODO: get the lifetime from the cert's .exp field - if (keyPairValid && certValid) { - log.debug("getKeypairAndCertificate: already have keyPair and certificate"); - return { - keyPair: accountData.keyPair.rawKeyPair, - certificate: accountData.cert.rawCert - } - } - // We are definately going to generate a new cert, either because it has - // already expired, or the keyPair has - and a new keyPair means we must - // generate a new cert. - - // A keyPair has a longer lifetime than a cert, so it's possible we will - // have a valid keypair but an expired cert, which means we can skip - // keypair generation. - // Either way, the cert will require hitting the network, so bail now if - // we know that's going to fail. - if (Services.io.offline) { - throw new Error(ERROR_OFFLINE); - } - - let keyPair; - if (keyPairValid) { - keyPair = accountData.keyPair; - } else { - let keyWillBeValidUntil = this.now() + KEY_LIFETIME; - keyPair = yield new Promise((resolve, reject) => { - jwcrypto.generateKeyPair("DS160", (err, kp) => { - if (err) { - return reject(err); - } - log.debug("got keyPair"); - resolve({ - rawKeyPair: kp, - validUntil: keyWillBeValidUntil, - }); - }); - }); - } - - // and generate the cert. - let certWillBeValidUntil = this.now() + CERT_LIFETIME; - let certificate = yield this.getCertificateSigned(accountData.sessionToken, - keyPair.rawKeyPair.serializedPublicKey, - CERT_LIFETIME); - log.debug("getCertificate got a new one: " + !!certificate); - if (certificate) { - // Cache both keypair and cert. - let toUpdate = { - keyPair, - cert: { - rawCert: certificate, - validUntil: certWillBeValidUntil, - }, - }; - yield currentState.updateUserAccountData(toUpdate); - } - return { - keyPair: keyPair.rawKeyPair, - certificate: certificate, - } - }), - - getUserAccountData: function() { - return this.currentAccountState.getUserAccountData(); - }, - - isUserEmailVerified: function isUserEmailVerified(data) { - return !!(data && data.verified); - }, - - /** - * Setup for and if necessary do email verification polling. - */ - loadAndPoll: function() { - let currentState = this.currentAccountState; - return currentState.getUserAccountData() - .then(data => { - if (data) { - Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1); - if (!this.isUserEmailVerified(data)) { - this.pollEmailStatus(currentState, data.sessionToken, "start"); - } - } - return data; - }); - }, - - startVerifiedCheck: function(data) { - log.debug("startVerifiedCheck", data && data.verified); - if (logPII) { - log.debug("startVerifiedCheck with user data", data); - } - - // Get us to the verified state, then get the keys. This returns a promise - // that will fire when we are completely ready. - // - // Login is truly complete once keys have been fetched, so once getKeys() - // obtains and stores kA and kB, it will fire the onverified observer - // notification. - - // The callers of startVerifiedCheck never consume a returned promise (ie, - // this is simply kicking off a background fetch) so we must add a rejection - // handler to avoid runtime warnings about the rejection not being handled. - this.whenVerified(data).then( - () => this.getKeys(), - err => log.info("startVerifiedCheck promise was rejected: " + err) - ); - }, - - whenVerified: function(data) { - let currentState = this.currentAccountState; - if (data.verified) { - log.debug("already verified"); - return currentState.resolve(data); - } - if (!currentState.whenVerifiedDeferred) { - log.debug("whenVerified promise starts polling for verified email"); - this.pollEmailStatus(currentState, data.sessionToken, "start"); - } - return currentState.whenVerifiedDeferred.promise.then( - result => currentState.resolve(result) - ); - }, - - notifyObservers: function(topic, data) { - log.debug("Notifying observers of " + topic); - Services.obs.notifyObservers(null, topic, data); - }, - - // XXX - pollEmailStatus should maybe be on the AccountState object? - pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { - log.debug("entering pollEmailStatus: " + why); - if (why == "start" || why == "push") { - if (this.currentTimer) { - log.debug("pollEmailStatus starting while existing timer is running"); - clearTimeout(this.currentTimer); - this.currentTimer = null; - } - - // If we were already polling, stop and start again. This could happen - // if the user requested the verification email to be resent while we - // were already polling for receipt of an earlier email. - this.pollStartDate = Date.now(); - if (!currentState.whenVerifiedDeferred) { - currentState.whenVerifiedDeferred = Promise.defer(); - // This deferred might not end up with any handlers (eg, if sync - // is yet to start up.) This might cause "A promise chain failed to - // handle a rejection" messages, so add an error handler directly - // on the promise to log the error. - currentState.whenVerifiedDeferred.promise.then(null, err => { - log.info("the wait for user verification was stopped: " + err); - }); - } - } - - // We return a promise for testing only. Other callers can ignore this, - // since verification polling continues in the background. - return this.checkEmailStatus(sessionToken, { reason: why }) - .then((response) => { - log.debug("checkEmailStatus -> " + JSON.stringify(response)); - if (response && response.verified) { - currentState.updateUserAccountData({ verified: true }) - .then(() => { - return currentState.getUserAccountData(); - }) - .then(data => { - // Now that the user is verified, we can proceed to fetch keys - if (currentState.whenVerifiedDeferred) { - currentState.whenVerifiedDeferred.resolve(data); - delete currentState.whenVerifiedDeferred; - } - // Tell FxAccountsManager to clear its cache - this.notifyObservers(ON_FXA_UPDATE_NOTIFICATION, ONVERIFIED_NOTIFICATION); - }); - } else { - // Poll email status again after a short delay. - this.pollEmailStatusAgain(currentState, sessionToken); - } - }, error => { - let timeoutMs = undefined; - if (error && error.retryAfter) { - // If the server told us to back off, back off the requested amount. - timeoutMs = (error.retryAfter + 3) * 1000; - } - // The server will return 401 if a request parameter is erroneous or - // if the session token expired. Let's continue polling otherwise. - if (!error || !error.code || error.code != 401) { - this.pollEmailStatusAgain(currentState, sessionToken, timeoutMs); - } else { - let error = new Error("Verification status check failed"); - this._rejectWhenVerified(currentState, error); - } - }); - }, - - _rejectWhenVerified(currentState, error) { - currentState.whenVerifiedDeferred.reject(error); - delete currentState.whenVerifiedDeferred; - }, - - // Poll email status using truncated exponential back-off. - pollEmailStatusAgain: function (currentState, sessionToken, timeoutMs) { - let ageMs = Date.now() - this.pollStartDate; - if (ageMs >= this.POLL_SESSION) { - if (currentState.whenVerifiedDeferred) { - let error = new Error("User email verification timed out."); - this._rejectWhenVerified(currentState, error); - } - log.debug("polling session exceeded, giving up"); - return; - } - if (timeoutMs === undefined) { - let currentMinute = Math.ceil(ageMs / 60000); - timeoutMs = currentMinute <= 2 ? this.VERIFICATION_POLL_TIMEOUT_INITIAL - : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT; - } - log.debug("polling with timeout = " + timeoutMs); - this.currentTimer = setTimeout(() => { - this.pollEmailStatus(currentState, sessionToken, "timer"); - }, timeoutMs); - }, - - requiresHttps: function() { - let allowHttp = false; - try { - allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp"); - } catch(e) { - // Pref doesn't exist - } - return allowHttp !== true; - }, - - promiseAccountsSignUpURI() { - return FxAccountsConfig.promiseAccountsSignUpURI(); - }, - - promiseAccountsSignInURI() { - return FxAccountsConfig.promiseAccountsSignInURI(); - }, - - // Returns a promise that resolves with the URL to use to force a re-signin - // of the current account. - promiseAccountsForceSigninURI: Task.async(function *() { - yield FxAccountsConfig.ensureConfigured(); - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the email address onto a query string. - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "email=" + encodeURIComponent(accountData.email); - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }), - - // Returns a promise that resolves with the URL to use to change - // the current account's profile image. - // if settingToEdit is set, the profile page should hightlight that setting - // for the user to edit. - promiseAccountsChangeProfileURI: function(entrypoint, settingToEdit = null) { - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri"); - - if (settingToEdit) { - url += (url.indexOf("?") == -1 ? "?" : "&") + - "setting=" + encodeURIComponent(settingToEdit); - } - - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the email address onto a query string. - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "email=" + encodeURIComponent(accountData.email); - newQueryPortion += "&uid=" + encodeURIComponent(accountData.uid); - if (entrypoint) { - newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint); - } - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }, - - // Returns a promise that resolves with the URL to use to manage the current - // user's FxA acct. - promiseAccountsManageURI: function(entrypoint) { - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri"); - if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - let currentState = this.currentAccountState; - // but we need to append the uid and email address onto a query string - // (if the server has no matching uid it will offer to sign in with the - // email address) - return this.getSignedInUser().then(accountData => { - if (!accountData) { - return null; - } - let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; - newQueryPortion += "uid=" + encodeURIComponent(accountData.uid) + - "&email=" + encodeURIComponent(accountData.email); - if (entrypoint) { - newQueryPortion += "&entrypoint=" + encodeURIComponent(entrypoint); - } - return url + newQueryPortion; - }).then(result => currentState.resolve(result)); - }, - - /** - * Get an OAuth token for the user - * - * @param options - * { - * scope: (string/array) the oauth scope(s) being requested. As a - * convenience, you may pass a string if only one scope is - * required, or an array of strings if multiple are needed. - * } - * - * @return Promise.<string | Error> - * The promise resolves the oauth token as a string or rejects with - * an error object ({error: ERROR, details: {}}) of the following: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - */ - getOAuthToken: Task.async(function* (options = {}) { - log.debug("getOAuthToken enter"); - let scope = options.scope; - if (typeof scope === "string") { - scope = [scope]; - } - - if (!scope || !scope.length) { - throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'scope' option"); - } - - yield this._getVerifiedAccountOrReject(); - - // Early exit for a cached token. - let currentState = this.currentAccountState; - let cached = currentState.getCachedToken(scope); - if (cached) { - log.debug("getOAuthToken returning a cached token"); - return cached.token; - } - - // We are going to hit the server - this is the string we pass to it. - let scopeString = scope.join(" "); - let client = options.client; - - if (!client) { - try { - let defaultURL = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.oauth.uri"); - client = new FxAccountsOAuthGrantClient({ - serverURL: defaultURL, - client_id: FX_OAUTH_CLIENT_ID - }); - } catch (e) { - throw this._error(ERROR_INVALID_PARAMETER, e); - } - } - let oAuthURL = client.serverURL.href; - - try { - log.debug("getOAuthToken fetching new token from", oAuthURL); - let assertion = yield this.getAssertion(oAuthURL); - let result = yield client.getTokenFromAssertion(assertion, scopeString); - let token = result.access_token; - // If we got one, cache it. - if (token) { - let entry = {token: token, server: oAuthURL}; - // But before we do, check the cache again - if we find one now, it - // means someone else concurrently requested the same scope and beat - // us to the cache write. To be nice to the server, we revoke the one - // we just got and return the newly cached value. - let cached = currentState.getCachedToken(scope); - if (cached) { - log.debug("Detected a race for this token - revoking the new one."); - this._destroyOAuthToken(entry); - return cached.token; - } - currentState.setCachedToken(scope, entry); - } - return token; - } catch (err) { - throw this._errorToErrorClass(err); - } - }), - - /** - * Remove an OAuth token from the token cache. Callers should call this - * after they determine a token is invalid, so a new token will be fetched - * on the next call to getOAuthToken(). - * - * @param options - * { - * token: (string) A previously fetched token. - * } - * @return Promise.<undefined> This function will always resolve, even if - * an unknown token is passed. - */ - removeCachedOAuthToken: Task.async(function* (options) { - if (!options.token || typeof options.token !== "string") { - throw this._error(ERROR_INVALID_PARAMETER, "Missing or invalid 'token' option"); - } - let currentState = this.currentAccountState; - let existing = currentState.removeCachedToken(options.token); - if (existing) { - // background destroy. - this._destroyOAuthToken(existing).catch(err => { - log.warn("FxA failed to revoke a cached token", err); - }); - } - }), - - _getVerifiedAccountOrReject: Task.async(function* () { - let data = yield this.currentAccountState.getUserAccountData(); - if (!data) { - // No signed-in user - throw this._error(ERROR_NO_ACCOUNT); - } - if (!this.isUserEmailVerified(data)) { - // Signed-in user has not verified email - throw this._error(ERROR_UNVERIFIED_ACCOUNT); - } - }), - - /* - * Coerce an error into one of the general error cases: - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - * - * These errors will pass through: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - */ - _errorToErrorClass: function (aError) { - if (aError.errno) { - let error = SERVER_ERRNO_TO_ERROR[aError.errno]; - return this._error(ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, aError); - } else if (aError.message && - (aError.message === "INVALID_PARAMETER" || - aError.message === "NO_ACCOUNT" || - aError.message === "UNVERIFIED_ACCOUNT" || - aError.message === "AUTH_ERROR")) { - return aError; - } - return this._error(ERROR_UNKNOWN, aError); - }, - - _error: function(aError, aDetails) { - log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {aError, aDetails}); - let reason = new Error(aError); - if (aDetails) { - reason.details = aDetails; - } - return reason; - }, - - /** - * Get the user's account and profile data - * - * @param options - * { - * contentUrl: (string) Used by the FxAccountsWebChannel. - * Defaults to pref identity.fxaccounts.settings.uri - * profileServerUrl: (string) Used by the FxAccountsWebChannel. - * Defaults to pref identity.fxaccounts.remote.profile.uri - * } - * - * @return Promise.<object | Error> - * The promise resolves to an accountData object with extra profile - * information such as profileImageUrl, or rejects with - * an error object ({error: ERROR, details: {}}) of the following: - * INVALID_PARAMETER - * NO_ACCOUNT - * UNVERIFIED_ACCOUNT - * NETWORK_ERROR - * AUTH_ERROR - * UNKNOWN_ERROR - */ - getSignedInUserProfile: function () { - let currentState = this.currentAccountState; - return this.profile.getProfile().then( - profileData => { - let profile = Cu.cloneInto(profileData, {}); - return currentState.resolve(profile); - }, - error => { - log.error("Could not retrieve profile data", error); - return currentState.reject(error); - } - ).catch(err => Promise.reject(this._errorToErrorClass(err))); - }, - - // Attempt to update the auth server with whatever device details are stored - // in the account data. Returns a promise that always resolves, never rejects. - // If the promise resolves to a value, that value is the device id. - updateDeviceRegistration() { - return this.getSignedInUser().then(signedInUser => { - if (signedInUser) { - return this._registerOrUpdateDevice(signedInUser); - } - }).catch(error => this._logErrorAndResetDeviceRegistrationVersion(error)); - }, - - handleDeviceDisconnection(deviceId) { - return this.currentAccountState.getUserAccountData() - .then(data => data ? data.deviceId : null) - .then(localDeviceId => { - if (deviceId == localDeviceId) { - this.notifyObservers(ON_DEVICE_DISCONNECTED_NOTIFICATION, deviceId); - return this.signOut(true); - } - log.error( - "The device ID to disconnect doesn't match with the local device ID.\n" - + "Local: " + localDeviceId + ", ID to disconnect: " + deviceId); - }); - }, - - /** - * Delete all the cached persisted credentials we store for FxA. - * - * @return Promise resolves when the user data has been persisted - */ - resetCredentials() { - // Delete all fields except those required for the user to - // reauthenticate. - let updateData = {}; - let clearField = field => { - if (!FXA_PWDMGR_REAUTH_WHITELIST.has(field)) { - updateData[field] = null; - } - } - FXA_PWDMGR_PLAINTEXT_FIELDS.forEach(clearField); - FXA_PWDMGR_SECURE_FIELDS.forEach(clearField); - FXA_PWDMGR_MEMORY_FIELDS.forEach(clearField); - - let currentState = this.currentAccountState; - return currentState.updateUserAccountData(updateData); - }, - - // If you change what we send to the FxA servers during device registration, - // you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older - // devices to re-register when Firefox updates - _registerOrUpdateDevice(signedInUser) { - try { - // Allow tests to skip device registration because: - // 1. It makes remote requests to the auth server. - // 2. _getDeviceName does not work from xpcshell. - // 3. The B2G tests fail when attempting to import services-sync/util.js. - if (Services.prefs.getBoolPref("identity.fxaccounts.skipDeviceRegistration")) { - return Promise.resolve(); - } - } catch(ignore) {} - - if (!signedInUser.sessionToken) { - return Promise.reject(new Error( - "_registerOrUpdateDevice called without a session token")); - } - - return this.fxaPushService.registerPushEndpoint().then(subscription => { - const deviceName = this._getDeviceName(); - let deviceOptions = {}; - - // if we were able to obtain a subscription - if (subscription && subscription.endpoint) { - deviceOptions.pushCallback = subscription.endpoint; - let publicKey = subscription.getKey('p256dh'); - let authKey = subscription.getKey('auth'); - if (publicKey && authKey) { - deviceOptions.pushPublicKey = urlsafeBase64Encode(publicKey); - deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey); - } - } - - if (signedInUser.deviceId) { - log.debug("updating existing device details"); - return this.fxAccountsClient.updateDevice( - signedInUser.sessionToken, signedInUser.deviceId, deviceName, deviceOptions); - } - - log.debug("registering new device details"); - return this.fxAccountsClient.registerDevice( - signedInUser.sessionToken, deviceName, this._getDeviceType(), deviceOptions); - }).then(device => - this.currentAccountState.updateUserAccountData({ - deviceId: device.id, - deviceRegistrationVersion: this.DEVICE_REGISTRATION_VERSION - }).then(() => device.id) - ).catch(error => this._handleDeviceError(error, signedInUser.sessionToken)); - }, - - _getDeviceName() { - return Utils.getDeviceName(); - }, - - _getDeviceType() { - return Utils.getDeviceType(); - }, - - _handleDeviceError(error, sessionToken) { - return Promise.resolve().then(() => { - if (error.code === 400) { - if (error.errno === ERRNO_UNKNOWN_DEVICE) { - return this._recoverFromUnknownDevice(); - } - - if (error.errno === ERRNO_DEVICE_SESSION_CONFLICT) { - return this._recoverFromDeviceSessionConflict(error, sessionToken); - } - } - - // `_handleTokenError` re-throws the error. - return this._handleTokenError(error); - }).catch(error => - this._logErrorAndResetDeviceRegistrationVersion(error) - ).catch(() => {}); - }, - - _recoverFromUnknownDevice() { - // FxA did not recognise the device id. Handle it by clearing the device - // id on the account data. At next sync or next sign-in, registration is - // retried and should succeed. - log.warn("unknown device id, clearing the local device data"); - return this.currentAccountState.updateUserAccountData({ deviceId: null }) - .catch(error => this._logErrorAndResetDeviceRegistrationVersion(error)); - }, - - _recoverFromDeviceSessionConflict(error, sessionToken) { - // FxA has already associated this session with a different device id. - // Perhaps we were beaten in a race to register. Handle the conflict: - // 1. Fetch the list of devices for the current user from FxA. - // 2. Look for ourselves in the list. - // 3. If we find a match, set the correct device id and device registration - // version on the account data and return the correct device id. At next - // sync or next sign-in, registration is retried and should succeed. - // 4. If we don't find a match, log the original error. - log.warn("device session conflict, attempting to ascertain the correct device id"); - return this.fxAccountsClient.getDeviceList(sessionToken) - .then(devices => { - const matchingDevices = devices.filter(device => device.isCurrentDevice); - const length = matchingDevices.length; - if (length === 1) { - const deviceId = matchingDevices[0].id; - return this.currentAccountState.updateUserAccountData({ - deviceId, - deviceRegistrationVersion: null - }).then(() => deviceId); - } - if (length > 1) { - log.error("insane server state, " + length + " devices for this session"); - } - return this._logErrorAndResetDeviceRegistrationVersion(error); - }).catch(secondError => { - log.error("failed to recover from device-session conflict", secondError); - this._logErrorAndResetDeviceRegistrationVersion(error) - }); - }, - - _logErrorAndResetDeviceRegistrationVersion(error) { - // Device registration should never cause other operations to fail. - // If we've reached this point, just log the error and reset the device - // registration version on the account data. At next sync or next sign-in, - // registration will be retried. - log.error("device registration failed", error); - return this.currentAccountState.updateUserAccountData({ - deviceRegistrationVersion: null - }).catch(secondError => { - log.error( - "failed to reset the device registration version, device registration won't be retried", - secondError); - }).then(() => {}); - }, - - _handleTokenError(err) { - if (!err || err.code != 401 || err.errno != ERRNO_INVALID_AUTH_TOKEN) { - throw err; - } - log.warn("recovering from invalid token error", err); - return this.accountStatus().then(exists => { - if (!exists) { - // Delete all local account data. Since the account no longer - // exists, we can skip the remote calls. - log.info("token invalidated because the account no longer exists"); - return this.signOut(true); - } - log.info("clearing credentials to handle invalid token error"); - return this.resetCredentials(); - }).then(() => Promise.reject(err)); - }, -}; - - -// A getter for the instance to export -XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { - let a = new FxAccounts(); - - // XXX Bug 947061 - We need a strategy for resuming email verification after - // browser restart - a.loadAndPoll(); - - return a; -}); diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm deleted file mode 100644 index fbe8da2fe..000000000 --- a/services/fxaccounts/FxAccountsClient.jsm +++ /dev/null @@ -1,623 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -this.EXPORTED_SYMBOLS = ["FxAccountsClient"]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-common/hawkclient.js"); -Cu.import("resource://services-common/hawkrequest.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/Credentials.jsm"); - -const HOST_PREF = "identity.fxaccounts.auth.uri"; - -const SIGNIN = "/account/login"; -const SIGNUP = "/account/create"; - -this.FxAccountsClient = function(host = Services.prefs.getCharPref(HOST_PREF)) { - this.host = host; - - // The FxA auth server expects requests to certain endpoints to be authorized - // using Hawk. - this.hawk = new HawkClient(host); - this.hawk.observerPrefix = "FxA:hawk"; - - // Manage server backoff state. C.f. - // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol - this.backoffError = null; -}; - -this.FxAccountsClient.prototype = { - - /** - * Return client clock offset, in milliseconds, as determined by hawk client. - * Provided because callers should not have to know about hawk - * implementation. - * - * The offset is the number of milliseconds that must be added to the client - * clock to make it equal to the server clock. For example, if the client is - * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. - */ - get localtimeOffsetMsec() { - return this.hawk.localtimeOffsetMsec; - }, - - /* - * Return current time in milliseconds - * - * Not used by this module, but made available to the FxAccounts.jsm - * that uses this client. - */ - now: function() { - return this.hawk.now(); - }, - - /** - * Common code from signIn and signUp. - * - * @param path - * Request URL path. Can be /account/create or /account/login - * @param email - * The email address for the account (utf8) - * @param password - * The user's password - * @param [getKeys=false] - * If set to true the keyFetchToken will be retrieved - * @param [retryOK=true] - * If capitalization of the email is wrong and retryOK is set to true, - * we will retry with the suggested capitalization from the server - * @return Promise - * Returns a promise that resolves to an object: - * { - * authAt: authentication time for the session (seconds since epoch) - * email: the primary email for this account - * keyFetchToken: a key fetch token (hex) - * sessionToken: a session token (hex) - * uid: the user's unique ID (hex) - * unwrapBKey: used to unwrap kB, derived locally from the - * password (not revealed to the FxA server) - * verified (optional): flag indicating verification status of the - * email - * } - */ - _createSession: function(path, email, password, getKeys=false, - retryOK=true) { - return Credentials.setup(email, password).then((creds) => { - let data = { - authPW: CommonUtils.bytesAsHex(creds.authPW), - email: email, - }; - let keys = getKeys ? "?keys=true" : ""; - - return this._request(path + keys, "POST", null, data).then( - // Include the canonical capitalization of the email in the response so - // the caller can set its signed-in user state accordingly. - result => { - result.email = data.email; - result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey); - - return result; - }, - error => { - log.debug("Session creation failed", error); - // If the user entered an email with different capitalization from - // what's stored in the database (e.g., Greta.Garbo@gmail.COM as - // opposed to greta.garbo@gmail.com), the server will respond with a - // errno 120 (code 400) and the expected capitalization of the email. - // We retry with this email exactly once. If successful, we use the - // server's version of the email as the signed-in-user's email. This - // is necessary because the email also serves as salt; so we must be - // in agreement with the server on capitalization. - // - // API reference: - // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md - if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) { - if (!error.email) { - log.error("Server returned errno 120 but did not provide email"); - throw error; - } - return this._createSession(path, error.email, password, getKeys, - false); - } - throw error; - } - ); - }); - }, - - /** - * Create a new Firefox Account and authenticate - * - * @param email - * The email address for the account (utf8) - * @param password - * The user's password - * @param [getKeys=false] - * If set to true the keyFetchToken will be retrieved - * @return Promise - * Returns a promise that resolves to an object: - * { - * uid: the user's unique ID (hex) - * sessionToken: a session token (hex) - * keyFetchToken: a key fetch token (hex), - * unwrapBKey: used to unwrap kB, derived locally from the - * password (not revealed to the FxA server) - * } - */ - signUp: function(email, password, getKeys=false) { - return this._createSession(SIGNUP, email, password, getKeys, - false /* no retry */); - }, - - /** - * Authenticate and create a new session with the Firefox Account API server - * - * @param email - * The email address for the account (utf8) - * @param password - * The user's password - * @param [getKeys=false] - * If set to true the keyFetchToken will be retrieved - * @return Promise - * Returns a promise that resolves to an object: - * { - * authAt: authentication time for the session (seconds since epoch) - * email: the primary email for this account - * keyFetchToken: a key fetch token (hex) - * sessionToken: a session token (hex) - * uid: the user's unique ID (hex) - * unwrapBKey: used to unwrap kB, derived locally from the - * password (not revealed to the FxA server) - * verified: flag indicating verification status of the email - * } - */ - signIn: function signIn(email, password, getKeys=false) { - return this._createSession(SIGNIN, email, password, getKeys, - true /* retry */); - }, - - /** - * Check the status of a session given a session token - * - * @param sessionTokenHex - * The session token encoded in hex - * @return Promise - * Resolves with a boolean indicating if the session is still valid - */ - sessionStatus: function (sessionTokenHex) { - return this._request("/session/status", "GET", - deriveHawkCredentials(sessionTokenHex, "sessionToken")).then( - () => Promise.resolve(true), - error => { - if (isInvalidTokenError(error)) { - return Promise.resolve(false); - } - throw error; - } - ); - }, - - /** - * Destroy the current session with the Firefox Account API server - * - * @param sessionTokenHex - * The session token encoded in hex - * @return Promise - */ - signOut: function (sessionTokenHex, options = {}) { - let path = "/session/destroy"; - if (options.service) { - path += "?service=" + encodeURIComponent(options.service); - } - return this._request(path, "POST", - deriveHawkCredentials(sessionTokenHex, "sessionToken")); - }, - - /** - * Check the verification status of the user's FxA email address - * - * @param sessionTokenHex - * The current session token encoded in hex - * @return Promise - */ - recoveryEmailStatus: function (sessionTokenHex, options = {}) { - let path = "/recovery_email/status"; - if (options.reason) { - path += "?reason=" + encodeURIComponent(options.reason); - } - - return this._request(path, "GET", - deriveHawkCredentials(sessionTokenHex, "sessionToken")); - }, - - /** - * Resend the verification email for the user - * - * @param sessionTokenHex - * The current token encoded in hex - * @return Promise - */ - resendVerificationEmail: function(sessionTokenHex) { - return this._request("/recovery_email/resend_code", "POST", - deriveHawkCredentials(sessionTokenHex, "sessionToken")); - }, - - /** - * Retrieve encryption keys - * - * @param keyFetchTokenHex - * A one-time use key fetch token encoded in hex - * @return Promise - * Returns a promise that resolves to an object: - * { - * kA: an encryption key for recevorable data (bytes) - * wrapKB: an encryption key that requires knowledge of the - * user's password (bytes) - * } - */ - accountKeys: function (keyFetchTokenHex) { - let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); - let keyRequestKey = creds.extra.slice(0, 32); - let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined, - Credentials.keyWord("account/keys"), 3 * 32); - let respHMACKey = morecreds.slice(0, 32); - let respXORKey = morecreds.slice(32, 96); - - return this._request("/account/keys", "GET", creds).then(resp => { - if (!resp.bundle) { - throw new Error("failed to retrieve keys"); - } - - let bundle = CommonUtils.hexToBytes(resp.bundle); - let mac = bundle.slice(-32); - - let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, - CryptoUtils.makeHMACKey(respHMACKey)); - - let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher); - if (mac !== bundleMAC) { - throw new Error("error unbundling encryption keys"); - } - - let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64)); - - return { - kA: keyAWrapB.slice(0, 32), - wrapKB: keyAWrapB.slice(32) - }; - }); - }, - - /** - * Sends a public key to the FxA API server and returns a signed certificate - * - * @param sessionTokenHex - * The current session token encoded in hex - * @param serializedPublicKey - * A public key (usually generated by jwcrypto) - * @param lifetime - * The lifetime of the certificate - * @return Promise - * Returns a promise that resolves to the signed certificate. - * The certificate can be used to generate a Persona assertion. - * @throws a new Error - * wrapping any of these HTTP code/errno pairs: - * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12 - */ - signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) { - let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); - - let body = { publicKey: serializedPublicKey, - duration: lifetime }; - return Promise.resolve() - .then(_ => this._request("/certificate/sign", "POST", creds, body)) - .then(resp => resp.cert, - err => { - log.error("HAWK.signCertificate error: " + JSON.stringify(err)); - throw err; - }); - }, - - /** - * Determine if an account exists - * - * @param email - * The email address to check - * @return Promise - * The promise resolves to true if the account exists, or false - * if it doesn't. The promise is rejected on other errors. - */ - accountExists: function (email) { - return this.signIn(email, "").then( - (cantHappen) => { - throw new Error("How did I sign in with an empty password?"); - }, - (expectedError) => { - switch (expectedError.errno) { - case ERRNO_ACCOUNT_DOES_NOT_EXIST: - return false; - break; - case ERRNO_INCORRECT_PASSWORD: - return true; - break; - default: - // not so expected, any more ... - throw expectedError; - break; - } - } - ); - }, - - /** - * Given the uid of an existing account (not an arbitrary email), ask - * the server if it still exists via /account/status. - * - * Used for differentiating between password change and account deletion. - */ - accountStatus: function(uid) { - return this._request("/account/status?uid="+uid, "GET").then( - (result) => { - return result.exists; - }, - (error) => { - log.error("accountStatus failed with: " + error); - return Promise.reject(error); - } - ); - }, - - /** - * Register a new device - * - * @method registerDevice - * @param sessionTokenHex - * Session token obtained from signIn - * @param name - * Device name - * @param type - * Device type (mobile|desktop) - * @param [options] - * Extra device options - * @param [options.pushCallback] - * `pushCallback` push endpoint callback - * @param [options.pushPublicKey] - * `pushPublicKey` push public key (URLSafe Base64 string) - * @param [options.pushAuthKey] - * `pushAuthKey` push auth secret (URLSafe Base64 string) - * @return Promise - * Resolves to an object: - * { - * id: Device identifier - * createdAt: Creation time (milliseconds since epoch) - * name: Name of device - * type: Type of device (mobile|desktop) - * } - */ - registerDevice(sessionTokenHex, name, type, options = {}) { - let path = "/account/device"; - - let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); - let body = { name, type }; - - if (options.pushCallback) { - body.pushCallback = options.pushCallback; - } - if (options.pushPublicKey && options.pushAuthKey) { - body.pushPublicKey = options.pushPublicKey; - body.pushAuthKey = options.pushAuthKey; - } - - return this._request(path, "POST", creds, body); - }, - - /** - * Sends a message to other devices. Must conform with the push payload schema: - * https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json - * - * @method notifyDevice - * @param sessionTokenHex - * Session token obtained from signIn - * @param deviceIds - * Devices to send the message to - * @param payload - * Data to send with the message - * @return Promise - * Resolves to an empty object: - * {} - */ - notifyDevices(sessionTokenHex, deviceIds, payload, TTL = 0) { - const body = { - to: deviceIds, - payload, - TTL - }; - return this._request("/account/devices/notify", "POST", - deriveHawkCredentials(sessionTokenHex, "sessionToken"), body); - }, - - /** - * Update the session or name for an existing device - * - * @method updateDevice - * @param sessionTokenHex - * Session token obtained from signIn - * @param id - * Device identifier - * @param name - * Device name - * @param [options] - * Extra device options - * @param [options.pushCallback] - * `pushCallback` push endpoint callback - * @param [options.pushPublicKey] - * `pushPublicKey` push public key (URLSafe Base64 string) - * @param [options.pushAuthKey] - * `pushAuthKey` push auth secret (URLSafe Base64 string) - * @return Promise - * Resolves to an object: - * { - * id: Device identifier - * name: Device name - * } - */ - updateDevice(sessionTokenHex, id, name, options = {}) { - let path = "/account/device"; - - let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); - let body = { id, name }; - if (options.pushCallback) { - body.pushCallback = options.pushCallback; - } - if (options.pushPublicKey && options.pushAuthKey) { - body.pushPublicKey = options.pushPublicKey; - body.pushAuthKey = options.pushAuthKey; - } - - return this._request(path, "POST", creds, body); - }, - - /** - * Delete a device and its associated session token, signing the user - * out of the server. - * - * @method signOutAndDestroyDevice - * @param sessionTokenHex - * Session token obtained from signIn - * @param id - * Device identifier - * @param [options] - * Options object - * @param [options.service] - * `service` query parameter - * @return Promise - * Resolves to an empty object: - * {} - */ - signOutAndDestroyDevice(sessionTokenHex, id, options = {}) { - let path = "/account/device/destroy"; - - if (options.service) { - path += "?service=" + encodeURIComponent(options.service); - } - - let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); - let body = { id }; - - return this._request(path, "POST", creds, body); - }, - - /** - * Get a list of currently registered devices - * - * @method getDeviceList - * @param sessionTokenHex - * Session token obtained from signIn - * @return Promise - * Resolves to an array of objects: - * [ - * { - * id: Device id - * isCurrentDevice: Boolean indicating whether the item - * represents the current device - * name: Device name - * type: Device type (mobile|desktop) - * }, - * ... - * ] - */ - getDeviceList(sessionTokenHex) { - let path = "/account/devices"; - let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); - - return this._request(path, "GET", creds, {}); - }, - - _clearBackoff: function() { - this.backoffError = null; - }, - - /** - * A general method for sending raw API calls to the FxA auth server. - * All request bodies and responses are JSON. - * - * @param path - * API endpoint path - * @param method - * The HTTP request method - * @param credentials - * Hawk credentials - * @param jsonPayload - * A JSON payload - * @return Promise - * Returns a promise that resolves to the JSON response of the API call, - * or is rejected with an error. Error responses have the following properties: - * { - * "code": 400, // matches the HTTP status code - * "errno": 107, // stable application-level error number - * "error": "Bad Request", // string description of the error type - * "message": "the value of salt is not allowed to be undefined", - * "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error - * } - */ - _request: function hawkRequest(path, method, credentials, jsonPayload) { - let deferred = Promise.defer(); - - // We were asked to back off. - if (this.backoffError) { - log.debug("Received new request during backoff, re-rejecting."); - deferred.reject(this.backoffError); - return deferred.promise; - } - - this.hawk.request(path, method, credentials, jsonPayload).then( - (response) => { - try { - let responseObj = JSON.parse(response.body); - deferred.resolve(responseObj); - } catch (err) { - log.error("json parse error on response: " + response.body); - deferred.reject({error: err}); - } - }, - - (error) => { - log.error("error " + method + "ing " + path + ": " + JSON.stringify(error)); - if (error.retryAfter) { - log.debug("Received backoff response; caching error as flag."); - this.backoffError = error; - // Schedule clearing of cached-error-as-flag. - CommonUtils.namedTimer( - this._clearBackoff, - error.retryAfter * 1000, - this, - "fxaBackoffTimer" - ); - } - deferred.reject(error); - } - ); - - return deferred.promise; - }, -}; - -function isInvalidTokenError(error) { - if (error.code != 401) { - return false; - } - switch (error.errno) { - case ERRNO_INVALID_AUTH_TOKEN: - case ERRNO_INVALID_AUTH_TIMESTAMP: - case ERRNO_INVALID_AUTH_NONCE: - return true; - } - return false; -} diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js deleted file mode 100644 index 71fe78a50..000000000 --- a/services/fxaccounts/FxAccountsCommon.js +++ /dev/null @@ -1,368 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var { interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); - -// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config", -// "Debug", "Trace" or "All". If none is specified, "Debug" will be used by -// default. Note "Debug" is usually appropriate so that when this log is -// included in the Sync file logs we get verbose output. -const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; -// The level of messages that will be dumped to the console. If not specified, -// "Error" will be used. -const PREF_LOG_LEVEL_DUMP = "identity.fxaccounts.log.appender.dump"; - -// A pref that can be set so "sensitive" information (eg, personally -// identifiable info, credentials, etc) will be logged. -const PREF_LOG_SENSITIVE_DETAILS = "identity.fxaccounts.log.sensitive"; - -var exports = Object.create(null); - -XPCOMUtils.defineLazyGetter(exports, 'log', function() { - let log = Log.repository.getLogger("FirefoxAccounts"); - // We set the log level to debug, but the default dump appender is set to - // the level reflected in the pref. Other code that consumes FxA may then - // choose to add another appender at a different level. - log.level = Log.Level.Debug; - let appender = new Log.DumpAppender(); - appender.level = Log.Level.Error; - - log.addAppender(appender); - try { - // The log itself. - let level = - Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL); - log.level = Log.Level[level] || Log.Level.Debug; - - // The appender. - level = - Services.prefs.getPrefType(PREF_LOG_LEVEL_DUMP) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL_DUMP); - appender.level = Log.Level[level] || Log.Level.Error; - } catch (e) { - log.error(e); - } - - return log; -}); - -// A boolean to indicate if personally identifiable information (or anything -// else sensitive, such as credentials) should be logged. -XPCOMUtils.defineLazyGetter(exports, 'logPII', function() { - try { - return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS); - } catch (_) { - return false; - } -}); - -exports.FXACCOUNTS_PERMISSION = "firefox-accounts"; - -exports.DATA_FORMAT_VERSION = 1; -exports.DEFAULT_STORAGE_FILENAME = "signedInUser.json"; - -// Token life times. -// Having this parameter be short has limited security value and can cause -// spurious authentication values if the client's clock is skewed and -// we fail to adjust. See Bug 983256. -exports.ASSERTION_LIFETIME = 1000 * 3600 * 24 * 365 * 25; // 25 years -// This is a time period we want to guarantee that the assertion will be -// valid after we generate it (e.g., the signed cert won't expire in this -// period). -exports.ASSERTION_USE_PERIOD = 1000 * 60 * 5; // 5 minutes -exports.CERT_LIFETIME = 1000 * 3600 * 6; // 6 hours -exports.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours - -// After we start polling for account verification, we stop polling when this -// many milliseconds have elapsed. -exports.POLL_SESSION = 1000 * 60 * 20; // 20 minutes - -// Observer notifications. -exports.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin"; -exports.ONVERIFIED_NOTIFICATION = "fxaccounts:onverified"; -exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout"; -// Internal to services/fxaccounts only -exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update"; -exports.ON_DEVICE_DISCONNECTED_NOTIFICATION = "fxaccounts:device_disconnected"; -exports.ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed"; -exports.ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset"; -exports.ON_COLLECTION_CHANGED_NOTIFICATION = "sync:collection_changed"; - -exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update"; - -exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; -exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange"; - -// UI Requests. -exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow"; -exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication"; - -// The OAuth client ID for Firefox Desktop -exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776"; - -// Firefox Accounts WebChannel ID -exports.WEBCHANNEL_ID = "account_updates"; - -// Server errno. -// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format -exports.ERRNO_ACCOUNT_ALREADY_EXISTS = 101; -exports.ERRNO_ACCOUNT_DOES_NOT_EXIST = 102; -exports.ERRNO_INCORRECT_PASSWORD = 103; -exports.ERRNO_UNVERIFIED_ACCOUNT = 104; -exports.ERRNO_INVALID_VERIFICATION_CODE = 105; -exports.ERRNO_NOT_VALID_JSON_BODY = 106; -exports.ERRNO_INVALID_BODY_PARAMETERS = 107; -exports.ERRNO_MISSING_BODY_PARAMETERS = 108; -exports.ERRNO_INVALID_REQUEST_SIGNATURE = 109; -exports.ERRNO_INVALID_AUTH_TOKEN = 110; -exports.ERRNO_INVALID_AUTH_TIMESTAMP = 111; -exports.ERRNO_MISSING_CONTENT_LENGTH = 112; -exports.ERRNO_REQUEST_BODY_TOO_LARGE = 113; -exports.ERRNO_TOO_MANY_CLIENT_REQUESTS = 114; -exports.ERRNO_INVALID_AUTH_NONCE = 115; -exports.ERRNO_ENDPOINT_NO_LONGER_SUPPORTED = 116; -exports.ERRNO_INCORRECT_LOGIN_METHOD = 117; -exports.ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD = 118; -exports.ERRNO_INCORRECT_API_VERSION = 119; -exports.ERRNO_INCORRECT_EMAIL_CASE = 120; -exports.ERRNO_ACCOUNT_LOCKED = 121; -exports.ERRNO_ACCOUNT_UNLOCKED = 122; -exports.ERRNO_UNKNOWN_DEVICE = 123; -exports.ERRNO_DEVICE_SESSION_CONFLICT = 124; -exports.ERRNO_SERVICE_TEMP_UNAVAILABLE = 201; -exports.ERRNO_PARSE = 997; -exports.ERRNO_NETWORK = 998; -exports.ERRNO_UNKNOWN_ERROR = 999; - -// Offset oauth server errnos so they don't conflict with auth server errnos -exports.OAUTH_SERVER_ERRNO_OFFSET = 1000; - -// OAuth Server errno. -exports.ERRNO_UNKNOWN_CLIENT_ID = 101 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INCORRECT_CLIENT_SECRET = 102 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INCORRECT_REDIRECT_URI = 103 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INVALID_FXA_ASSERTION = 104 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_UNKNOWN_CODE = 105 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INCORRECT_CODE = 106 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_EXPIRED_CODE = 107 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_OAUTH_INVALID_TOKEN = 108 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INVALID_REQUEST_PARAM = 109 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INVALID_RESPONSE_TYPE = 110 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_UNAUTHORIZED = 111 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_FORBIDDEN = 112 + exports.OAUTH_SERVER_ERRNO_OFFSET; -exports.ERRNO_INVALID_CONTENT_TYPE = 113 + exports.OAUTH_SERVER_ERRNO_OFFSET; - -// Errors. -exports.ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS"; -exports.ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST "; -exports.ERROR_ACCOUNT_LOCKED = "ACCOUNT_LOCKED"; -exports.ERROR_ACCOUNT_UNLOCKED = "ACCOUNT_UNLOCKED"; -exports.ERROR_ALREADY_SIGNED_IN_USER = "ALREADY_SIGNED_IN_USER"; -exports.ERROR_DEVICE_SESSION_CONFLICT = "DEVICE_SESSION_CONFLICT"; -exports.ERROR_ENDPOINT_NO_LONGER_SUPPORTED = "ENDPOINT_NO_LONGER_SUPPORTED"; -exports.ERROR_INCORRECT_API_VERSION = "INCORRECT_API_VERSION"; -exports.ERROR_INCORRECT_EMAIL_CASE = "INCORRECT_EMAIL_CASE"; -exports.ERROR_INCORRECT_KEY_RETRIEVAL_METHOD = "INCORRECT_KEY_RETRIEVAL_METHOD"; -exports.ERROR_INCORRECT_LOGIN_METHOD = "INCORRECT_LOGIN_METHOD"; -exports.ERROR_INVALID_EMAIL = "INVALID_EMAIL"; -exports.ERROR_INVALID_AUDIENCE = "INVALID_AUDIENCE"; -exports.ERROR_INVALID_AUTH_TOKEN = "INVALID_AUTH_TOKEN"; -exports.ERROR_INVALID_AUTH_TIMESTAMP = "INVALID_AUTH_TIMESTAMP"; -exports.ERROR_INVALID_AUTH_NONCE = "INVALID_AUTH_NONCE"; -exports.ERROR_INVALID_BODY_PARAMETERS = "INVALID_BODY_PARAMETERS"; -exports.ERROR_INVALID_PASSWORD = "INVALID_PASSWORD"; -exports.ERROR_INVALID_VERIFICATION_CODE = "INVALID_VERIFICATION_CODE"; -exports.ERROR_INVALID_REFRESH_AUTH_VALUE = "INVALID_REFRESH_AUTH_VALUE"; -exports.ERROR_INVALID_REQUEST_SIGNATURE = "INVALID_REQUEST_SIGNATURE"; -exports.ERROR_INTERNAL_INVALID_USER = "INTERNAL_ERROR_INVALID_USER"; -exports.ERROR_MISSING_BODY_PARAMETERS = "MISSING_BODY_PARAMETERS"; -exports.ERROR_MISSING_CONTENT_LENGTH = "MISSING_CONTENT_LENGTH"; -exports.ERROR_NO_TOKEN_SESSION = "NO_TOKEN_SESSION"; -exports.ERROR_NO_SILENT_REFRESH_AUTH = "NO_SILENT_REFRESH_AUTH"; -exports.ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY"; -exports.ERROR_OFFLINE = "OFFLINE"; -exports.ERROR_PERMISSION_DENIED = "PERMISSION_DENIED"; -exports.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE"; -exports.ERROR_SERVER_ERROR = "SERVER_ERROR"; -exports.ERROR_SYNC_DISABLED = "SYNC_DISABLED"; -exports.ERROR_TOO_MANY_CLIENT_REQUESTS = "TOO_MANY_CLIENT_REQUESTS"; -exports.ERROR_SERVICE_TEMP_UNAVAILABLE = "SERVICE_TEMPORARY_UNAVAILABLE"; -exports.ERROR_UI_ERROR = "UI_ERROR"; -exports.ERROR_UI_REQUEST = "UI_REQUEST"; -exports.ERROR_PARSE = "PARSE_ERROR"; -exports.ERROR_NETWORK = "NETWORK_ERROR"; -exports.ERROR_UNKNOWN = "UNKNOWN_ERROR"; -exports.ERROR_UNKNOWN_DEVICE = "UNKNOWN_DEVICE"; -exports.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT"; - -// OAuth errors. -exports.ERROR_UNKNOWN_CLIENT_ID = "UNKNOWN_CLIENT_ID"; -exports.ERROR_INCORRECT_CLIENT_SECRET = "INCORRECT_CLIENT_SECRET"; -exports.ERROR_INCORRECT_REDIRECT_URI = "INCORRECT_REDIRECT_URI"; -exports.ERROR_INVALID_FXA_ASSERTION = "INVALID_FXA_ASSERTION"; -exports.ERROR_UNKNOWN_CODE = "UNKNOWN_CODE"; -exports.ERROR_INCORRECT_CODE = "INCORRECT_CODE"; -exports.ERROR_EXPIRED_CODE = "EXPIRED_CODE"; -exports.ERROR_OAUTH_INVALID_TOKEN = "OAUTH_INVALID_TOKEN"; -exports.ERROR_INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM"; -exports.ERROR_INVALID_RESPONSE_TYPE = "INVALID_RESPONSE_TYPE"; -exports.ERROR_UNAUTHORIZED = "UNAUTHORIZED"; -exports.ERROR_FORBIDDEN = "FORBIDDEN"; -exports.ERROR_INVALID_CONTENT_TYPE = "INVALID_CONTENT_TYPE"; - -// Additional generic error classes for external consumers -exports.ERROR_NO_ACCOUNT = "NO_ACCOUNT"; -exports.ERROR_AUTH_ERROR = "AUTH_ERROR"; -exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER"; - -// Status code errors -exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405; -exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED"; - -// FxAccounts has the ability to "split" the credentials between a plain-text -// JSON file in the profile dir and in the login manager. -// In order to prevent new fields accidentally ending up in the "wrong" place, -// all fields stored are listed here. - -// The fields we save in the plaintext JSON. -// See bug 1013064 comments 23-25 for why the sessionToken is "safe" -exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set( - ["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile", - "deviceId", "deviceRegistrationVersion"]); - -// Fields we store in secure storage if it exists. -exports.FXA_PWDMGR_SECURE_FIELDS = new Set( - ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]); - -// Fields we keep in memory and don't persist anywhere. -exports.FXA_PWDMGR_MEMORY_FIELDS = new Set( - ["cert", "keyPair"]); - -// A whitelist of fields that remain in storage when the user needs to -// reauthenticate. All other fields will be removed. -exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set( - ["email", "uid", "profile", "deviceId", "deviceRegistrationVersion", "verified"]); - -// The pseudo-host we use in the login manager -exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts"; -// The realm we use in the login manager. -exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials"; - -// Error matching. -exports.SERVER_ERRNO_TO_ERROR = {}; - -// Error mapping -exports.ERROR_TO_GENERAL_ERROR_CLASS = {}; - -for (let id in exports) { - this[id] = exports[id]; -} - -// Allow this file to be imported via Components.utils.import(). -this.EXPORTED_SYMBOLS = Object.keys(exports); - -// Set these up now that everything has been loaded into |this|. -SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_ALREADY_EXISTS] = ERROR_ACCOUNT_ALREADY_EXISTS; -SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXIST] = ERROR_ACCOUNT_DOES_NOT_EXIST; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_PASSWORD] = ERROR_INVALID_PASSWORD; -SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE; -SERVER_ERRNO_TO_ERROR[ERRNO_NOT_VALID_JSON_BODY] = ERROR_NOT_VALID_JSON_BODY; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_PARAMETERS] = ERROR_INVALID_BODY_PARAMETERS; -SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_BODY_PARAMETERS] = ERROR_MISSING_BODY_PARAMETERS; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE] = ERROR_INVALID_REQUEST_SIGNATURE; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN] = ERROR_INVALID_AUTH_TOKEN; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TIMESTAMP] = ERROR_INVALID_AUTH_TIMESTAMP; -SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH] = ERROR_MISSING_CONTENT_LENGTH; -SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE] = ERROR_REQUEST_BODY_TOO_LARGE; -SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_CLIENT_REQUESTS] = ERROR_TOO_MANY_CLIENT_REQUESTS; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_NONCE] = ERROR_INVALID_AUTH_NONCE; -SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_ENDPOINT_NO_LONGER_SUPPORTED; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_LOGIN_METHOD] = ERROR_INCORRECT_LOGIN_METHOD; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_INCORRECT_KEY_RETRIEVAL_METHOD; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_API_VERSION] = ERROR_INCORRECT_API_VERSION; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_EMAIL_CASE] = ERROR_INCORRECT_EMAIL_CASE; -SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_LOCKED] = ERROR_ACCOUNT_LOCKED; -SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_UNLOCKED] = ERROR_ACCOUNT_UNLOCKED; -SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_DEVICE] = ERROR_UNKNOWN_DEVICE; -SERVER_ERRNO_TO_ERROR[ERRNO_DEVICE_SESSION_CONFLICT] = ERROR_DEVICE_SESSION_CONFLICT; -SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE] = ERROR_SERVICE_TEMP_UNAVAILABLE; -SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN; -SERVER_ERRNO_TO_ERROR[ERRNO_NETWORK] = ERROR_NETWORK; - -// oauth -SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CLIENT_ID] = ERROR_UNKNOWN_CLIENT_ID; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CLIENT_SECRET] = ERROR_INCORRECT_CLIENT_SECRET; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_REDIRECT_URI] = ERROR_INCORRECT_REDIRECT_URI; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_FXA_ASSERTION] = ERROR_INVALID_FXA_ASSERTION; -SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CODE] = ERROR_UNKNOWN_CODE; -SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CODE] = ERROR_INCORRECT_CODE; -SERVER_ERRNO_TO_ERROR[ERRNO_EXPIRED_CODE] = ERROR_EXPIRED_CODE; -SERVER_ERRNO_TO_ERROR[ERRNO_OAUTH_INVALID_TOKEN] = ERROR_OAUTH_INVALID_TOKEN; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_PARAM] = ERROR_INVALID_REQUEST_PARAM; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_RESPONSE_TYPE] = ERROR_INVALID_RESPONSE_TYPE; -SERVER_ERRNO_TO_ERROR[ERRNO_UNAUTHORIZED] = ERROR_UNAUTHORIZED; -SERVER_ERRNO_TO_ERROR[ERRNO_FORBIDDEN] = ERROR_FORBIDDEN; -SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_CONTENT_TYPE] = ERROR_INVALID_CONTENT_TYPE; - - -// Map internal errors to more generic error classes for consumers -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_ALREADY_EXISTS] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_DOES_NOT_EXIST] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_LOCKED] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_UNLOCKED] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ALREADY_SIGNED_IN_USER] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_DEVICE_SESSION_CONFLICT] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_API_VERSION] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_EMAIL_CASE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_LOGIN_METHOD] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_EMAIL] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUDIENCE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TOKEN] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TIMESTAMP] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_NONCE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_BODY_PARAMETERS] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_PASSWORD] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_VERIFICATION_CODE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REFRESH_AUTH_VALUE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_SIGNATURE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INTERNAL_INVALID_USER] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_BODY_PARAMETERS] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_CONTENT_LENGTH] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_TOKEN_SESSION] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_SILENT_REFRESH_AUTH] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NOT_VALID_JSON_BODY] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PERMISSION_DENIED] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_REQUEST_BODY_TOO_LARGE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNKNOWN_DEVICE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNVERIFIED_ACCOUNT] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_ERROR] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_REQUEST] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OFFLINE] = ERROR_NETWORK; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVER_ERROR] = ERROR_NETWORK; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_TOO_MANY_CLIENT_REQUESTS] = ERROR_NETWORK; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVICE_TEMP_UNAVAILABLE] = ERROR_NETWORK; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PARSE] = ERROR_NETWORK; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NETWORK] = ERROR_NETWORK; - -// oauth -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CLIENT_SECRET] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_REDIRECT_URI] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_FXA_ASSERTION] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNKNOWN_CODE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CODE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_EXPIRED_CODE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OAUTH_INVALID_TOKEN] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_PARAM] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_RESPONSE_TYPE] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNAUTHORIZED] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_FORBIDDEN] = ERROR_AUTH_ERROR; -ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_CONTENT_TYPE] = ERROR_AUTH_ERROR; diff --git a/services/fxaccounts/FxAccountsComponents.manifest b/services/fxaccounts/FxAccountsComponents.manifest deleted file mode 100644 index 5069755bc..000000000 --- a/services/fxaccounts/FxAccountsComponents.manifest +++ /dev/null @@ -1,4 +0,0 @@ -# FxAccountsPush.js -component {1b7db999-2ecd-4abf-bb95-a726896798ca} FxAccountsPush.js process=main -contract @mozilla.org/fxaccounts/push;1 {1b7db999-2ecd-4abf-bb95-a726896798ca} -category push chrome://fxa-device-update @mozilla.org/fxaccounts/push;1 diff --git a/services/fxaccounts/FxAccountsConfig.jsm b/services/fxaccounts/FxAccountsConfig.jsm deleted file mode 100644 index 9dcf532ab..000000000 --- a/services/fxaccounts/FxAccountsConfig.jsm +++ /dev/null @@ -1,179 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; -this.EXPORTED_SYMBOLS = ["FxAccountsConfig"]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel", - "resource://gre/modules/FxAccountsWebChannel.jsm"); - -const CONFIG_PREFS = [ - "identity.fxaccounts.auth.uri", - "identity.fxaccounts.remote.oauth.uri", - "identity.fxaccounts.remote.profile.uri", - "identity.sync.tokenserver.uri", - "identity.fxaccounts.remote.webchannel.uri", - "identity.fxaccounts.settings.uri", - "identity.fxaccounts.remote.signup.uri", - "identity.fxaccounts.remote.signin.uri", - "identity.fxaccounts.remote.force_auth.uri", -]; - -this.FxAccountsConfig = { - - // Returns a promise that resolves with the URI of the remote UI flows. - promiseAccountsSignUpURI: Task.async(function*() { - yield this.ensureConfigured(); - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri"); - if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - return url; - }), - - // Returns a promise that resolves with the URI of the remote UI flows. - promiseAccountsSignInURI: Task.async(function*() { - yield this.ensureConfigured(); - let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri"); - if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting - throw new Error("Firefox Accounts server must use HTTPS"); - } - return url; - }), - - resetConfigURLs() { - let autoconfigURL = this.getAutoConfigURL(); - if (!autoconfigURL) { - return; - } - // They have the autoconfig uri pref set, so we clear all the prefs that we - // will have initialized, which will leave them pointing at production. - for (let pref of CONFIG_PREFS) { - Services.prefs.clearUserPref(pref); - } - // Reset the webchannel. - EnsureFxAccountsWebChannel(); - if (!Services.prefs.prefHasUserValue("webchannel.allowObject.urlWhitelist")) { - return; - } - let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist"); - if (whitelistValue.startsWith(autoconfigURL + " ")) { - whitelistValue = whitelistValue.slice(autoconfigURL.length + 1); - // Check and see if the value will be the default, and just clear the pref if it would - // to avoid it showing up as changed in about:config. - let defaultWhitelist; - try { - defaultWhitelist = Services.prefs.getDefaultBranch("webchannel.allowObject.").getCharPref("urlWhitelist"); - } catch (e) { - // No default value ... - } - - if (defaultWhitelist === whitelistValue) { - Services.prefs.clearUserPref("webchannel.allowObject.urlWhitelist"); - } else { - Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue); - } - } - }, - - getAutoConfigURL() { - let pref; - try { - pref = Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri"); - } catch (e) { /* no pref */ } - if (!pref) { - // no pref / empty pref means we don't bother here. - return ""; - } - let rootURL = Services.urlFormatter.formatURL(pref); - if (rootURL.endsWith("/")) { - rootURL.slice(0, -1); - } - return rootURL; - }, - - ensureConfigured: Task.async(function*() { - let isSignedIn = !!(yield fxAccounts.getSignedInUser()); - if (!isSignedIn) { - yield this.fetchConfigURLs(); - } - }), - - // Read expected client configuration from the fxa auth server - // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration) - // and replace all the relevant our prefs with the information found there. - // This is only done before sign-in and sign-up, and even then only if the - // `identity.fxaccounts.autoconfig.uri` preference is set. - fetchConfigURLs: Task.async(function*() { - let rootURL = this.getAutoConfigURL(); - if (!rootURL) { - return; - } - let configURL = rootURL + "/.well-known/fxa-client-configuration"; - let jsonStr = yield new Promise((resolve, reject) => { - let request = new RESTRequest(configURL); - request.setHeader("Accept", "application/json"); - request.get(error => { - if (error) { - log.error(`Failed to get configuration object from "${configURL}"`, error); - return reject(error); - } - if (!request.response.success) { - log.error(`Received HTTP response code ${request.response.status} from configuration object request`); - if (request.response && request.response.body) { - log.debug("Got error response", request.response.body); - } - return reject(request.response.status); - } - resolve(request.response.body); - }); - }); - - log.debug("Got successful configuration response", jsonStr); - try { - // Update the prefs directly specified by the config. - let config = JSON.parse(jsonStr) - let authServerBase = config.auth_server_base_url; - if (!authServerBase.endsWith("/v1")) { - authServerBase += "/v1"; - } - Services.prefs.setCharPref("identity.fxaccounts.auth.uri", authServerBase); - Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", config.oauth_server_base_url + "/v1"); - Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", config.profile_server_base_url + "/v1"); - Services.prefs.setCharPref("identity.sync.tokenserver.uri", config.sync_tokenserver_base_url + "/1.0/sync/1.5"); - // Update the prefs that are based off of the autoconfig url - - let contextParam = encodeURIComponent( - Services.prefs.getCharPref("identity.fxaccounts.contextParam")); - - Services.prefs.setCharPref("identity.fxaccounts.remote.webchannel.uri", rootURL); - Services.prefs.setCharPref("identity.fxaccounts.settings.uri", rootURL + "/settings?service=sync&context=" + contextParam); - Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", rootURL + "/signup?service=sync&context=" + contextParam); - Services.prefs.setCharPref("identity.fxaccounts.remote.signin.uri", rootURL + "/signin?service=sync&context=" + contextParam); - Services.prefs.setCharPref("identity.fxaccounts.remote.force_auth.uri", rootURL + "/force_auth?service=sync&context=" + contextParam); - - let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist"); - if (!whitelistValue.includes(rootURL)) { - whitelistValue = `${rootURL} ${whitelistValue}`; - Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue); - } - // Ensure the webchannel is pointed at the correct uri - EnsureFxAccountsWebChannel(); - } catch (e) { - log.error("Failed to initialize configuration preferences from autoconfig object", e); - throw e; - } - }), - -}; diff --git a/services/fxaccounts/FxAccountsOAuthClient.jsm b/services/fxaccounts/FxAccountsOAuthClient.jsm deleted file mode 100644 index c59f1a869..000000000 --- a/services/fxaccounts/FxAccountsOAuthClient.jsm +++ /dev/null @@ -1,269 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Firefox Accounts OAuth browser login helper. - * Uses the WebChannel component to receive OAuth messages and complete login flows. - */ - -this.EXPORTED_SYMBOLS = ["FxAccountsOAuthClient"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", - "resource://gre/modules/WebChannel.jsm"); -Cu.importGlobalProperties(["URL"]); - -/** - * Create a new FxAccountsOAuthClient for browser some service. - * - * @param {Object} options Options - * @param {Object} options.parameters - * Opaque alphanumeric token to be included in verification links - * @param {String} options.parameters.client_id - * OAuth id returned from client registration - * @param {String} options.parameters.state - * A value that will be returned to the client as-is upon redirection - * @param {String} options.parameters.oauth_uri - * The FxA OAuth server uri - * @param {String} options.parameters.content_uri - * The FxA Content server uri - * @param {String} [options.parameters.scope] - * Optional. A colon-separated list of scopes that the user has authorized - * @param {String} [options.parameters.action] - * Optional. If provided, should be either signup, signin or force_auth. - * @param {String} [options.parameters.email] - * Optional. Required if options.paramters.action is 'force_auth'. - * @param {Boolean} [options.parameters.keys] - * Optional. If true then relier-specific encryption keys will be - * available in the second argument to onComplete. - * @param [authorizationEndpoint] {String} - * Optional authorization endpoint for the OAuth server - * @constructor - */ -this.FxAccountsOAuthClient = function(options) { - this._validateOptions(options); - this.parameters = options.parameters; - this._configureChannel(); - - let authorizationEndpoint = options.authorizationEndpoint || "/authorization"; - - try { - this._fxaOAuthStartUrl = new URL(this.parameters.oauth_uri + authorizationEndpoint + "?"); - } catch (e) { - throw new Error("Invalid OAuth Url"); - } - - let params = this._fxaOAuthStartUrl.searchParams; - params.append("client_id", this.parameters.client_id); - params.append("state", this.parameters.state); - params.append("scope", this.parameters.scope || ""); - params.append("action", this.parameters.action || "signin"); - params.append("webChannelId", this._webChannelId); - if (this.parameters.keys) { - params.append("keys", "true"); - } - // Only append if we actually have a value. - if (this.parameters.email) { - params.append("email", this.parameters.email); - } -}; - -this.FxAccountsOAuthClient.prototype = { - /** - * Function that gets called once the OAuth flow is complete. - * The callback will receive an object with code and state properties. - * If the keys parameter was specified and true, the callback will receive - * a second argument with kAr and kBr properties. - */ - onComplete: null, - /** - * Function that gets called if there is an error during the OAuth flow, - * for example due to a state mismatch. - * The callback will receive an Error object as its argument. - */ - onError: null, - /** - * Configuration object that stores all OAuth parameters. - */ - parameters: null, - /** - * WebChannel that is used to communicate with content page. - */ - _channel: null, - /** - * Boolean to indicate if this client has completed an OAuth flow. - */ - _complete: false, - /** - * The url that opens the Firefox Accounts OAuth flow. - */ - _fxaOAuthStartUrl: null, - /** - * WebChannel id. - */ - _webChannelId: null, - /** - * WebChannel origin, used to validate origin of messages. - */ - _webChannelOrigin: null, - /** - * Opens a tab at "this._fxaOAuthStartUrl". - * Registers a WebChannel listener and sets up a callback if needed. - */ - launchWebFlow: function () { - if (!this._channelCallback) { - this._registerChannel(); - } - - if (this._complete) { - throw new Error("This client already completed the OAuth flow"); - } else { - let opener = Services.wm.getMostRecentWindow("navigator:browser").gBrowser; - opener.selectedTab = opener.addTab(this._fxaOAuthStartUrl.href); - } - }, - - /** - * Release all resources that are in use. - */ - tearDown: function() { - this.onComplete = null; - this.onError = null; - this._complete = true; - this._channel.stopListening(); - this._channel = null; - }, - - /** - * Configures WebChannel id and origin - * - * @private - */ - _configureChannel: function() { - this._webChannelId = "oauth_" + this.parameters.client_id; - - // if this.parameters.content_uri is present but not a valid URI, then this will throw an error. - try { - this._webChannelOrigin = Services.io.newURI(this.parameters.content_uri, null, null); - } catch (e) { - throw e; - } - }, - - /** - * Create a new channel with the WebChannelBroker, setup a callback listener - * @private - */ - _registerChannel: function() { - /** - * Processes messages that are called back from the FxAccountsChannel - * - * @param webChannelId {String} - * Command webChannelId - * @param message {Object} - * Command message - * @param sendingContext {Object} - * Channel message event sendingContext - * @private - */ - let listener = function (webChannelId, message, sendingContext) { - if (message) { - let command = message.command; - let data = message.data; - let target = sendingContext && sendingContext.browser; - - switch (command) { - case "oauth_complete": - // validate the returned state and call onComplete or onError - let result = null; - let err = null; - - if (this.parameters.state !== data.state) { - err = new Error("OAuth flow failed. State doesn't match"); - } else if (this.parameters.keys && !data.keys) { - err = new Error("OAuth flow failed. Keys were not returned"); - } else { - result = { - code: data.code, - state: data.state - }; - } - - // if the message asked to close the tab - if (data.closeWindow && target) { - // for e10s reasons the best way is to use the TabBrowser to close the tab. - let tabbrowser = target.getTabBrowser(); - - if (tabbrowser) { - let tab = tabbrowser.getTabForBrowser(target); - - if (tab) { - tabbrowser.removeTab(tab); - log.debug("OAuth flow closed the tab."); - } else { - log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser."); - } - } else { - log.debug("OAuth flow failed to close the tab. TabBrowser not found."); - } - } - - if (err) { - log.debug(err.message); - if (this.onError) { - this.onError(err); - } - } else { - log.debug("OAuth flow completed."); - if (this.onComplete) { - if (this.parameters.keys) { - this.onComplete(result, data.keys); - } else { - this.onComplete(result); - } - } - } - - // onComplete will be called for this client only once - // calling onComplete again will result in a failure of the OAuth flow - this.tearDown(); - break; - } - } - }; - - this._channelCallback = listener.bind(this); - this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); - this._channel.listen(this._channelCallback); - log.debug("Channel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); - }, - - /** - * Validates the required FxA OAuth parameters - * - * @param options {Object} - * OAuth client options - * @private - */ - _validateOptions: function (options) { - if (!options || !options.parameters) { - throw new Error("Missing 'parameters' configuration option"); - } - - ["oauth_uri", "client_id", "content_uri", "state"].forEach(option => { - if (!options.parameters[option]) { - throw new Error("Missing 'parameters." + option + "' parameter"); - } - }); - - if (options.parameters.action == "force_auth" && !options.parameters.email) { - throw new Error("parameters.email is required for action 'force_auth'"); - } - }, -}; diff --git a/services/fxaccounts/FxAccountsOAuthGrantClient.jsm b/services/fxaccounts/FxAccountsOAuthGrantClient.jsm deleted file mode 100644 index 4319a07ab..000000000 --- a/services/fxaccounts/FxAccountsOAuthGrantClient.jsm +++ /dev/null @@ -1,241 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Firefox Accounts OAuth Grant Client allows clients to obtain - * an OAuth token from a BrowserID assertion. Only certain client - * IDs support this privilage. - */ - -this.EXPORTED_SYMBOLS = ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://services-common/rest.js"); - -Cu.importGlobalProperties(["URL"]); - -const AUTH_ENDPOINT = "/authorization"; -const DESTROY_ENDPOINT = "/destroy"; - -/** - * Create a new FxAccountsOAuthClient for browser some service. - * - * @param {Object} options Options - * @param {Object} options.parameters - * @param {String} options.parameters.client_id - * OAuth id returned from client registration - * @param {String} options.parameters.serverURL - * The FxA OAuth server URL - * @param [authorizationEndpoint] {String} - * Optional authorization endpoint for the OAuth server - * @constructor - */ -this.FxAccountsOAuthGrantClient = function(options) { - - this._validateOptions(options); - this.parameters = options; - - try { - this.serverURL = new URL(this.parameters.serverURL); - } catch (e) { - throw new Error("Invalid 'serverURL'"); - } - - log.debug("FxAccountsOAuthGrantClient Initialized"); -}; - -this.FxAccountsOAuthGrantClient.prototype = { - - /** - * Retrieves an OAuth access token for the signed in user - * - * @param {Object} assertion BrowserID assertion - * @param {String} scope OAuth scope - * @return Promise - * Resolves: {Object} Object with access_token property - */ - getTokenFromAssertion: function (assertion, scope) { - if (!assertion) { - throw new Error("Missing 'assertion' parameter"); - } - if (!scope) { - throw new Error("Missing 'scope' parameter"); - } - let params = { - scope: scope, - client_id: this.parameters.client_id, - assertion: assertion, - response_type: "token" - }; - - return this._createRequest(AUTH_ENDPOINT, "POST", params); - }, - - /** - * Destroys a previously fetched OAuth access token. - * - * @param {String} token The previously fetched token - * @return Promise - * Resolves: {Object} with the server response, which is typically - * ignored. - */ - destroyToken: function (token) { - if (!token) { - throw new Error("Missing 'token' parameter"); - } - let params = { - token: token, - }; - - return this._createRequest(DESTROY_ENDPOINT, "POST", params); - }, - - /** - * Validates the required FxA OAuth parameters - * - * @param options {Object} - * OAuth client options - * @private - */ - _validateOptions: function (options) { - if (!options) { - throw new Error("Missing configuration options"); - } - - ["serverURL", "client_id"].forEach(option => { - if (!options[option]) { - throw new Error("Missing '" + option + "' parameter"); - } - }); - }, - - /** - * Interface for making remote requests. - */ - _Request: RESTRequest, - - /** - * Remote request helper - * - * @param {String} path - * Profile server path, i.e "/profile". - * @param {String} [method] - * Type of request, i.e "GET". - * @return Promise - * Resolves: {Object} Successful response from the Profile server. - * Rejects: {FxAccountsOAuthGrantClientError} Profile client error. - * @private - */ - _createRequest: function(path, method = "POST", params) { - return new Promise((resolve, reject) => { - let profileDataUrl = this.serverURL + path; - let request = new this._Request(profileDataUrl); - method = method.toUpperCase(); - - request.setHeader("Accept", "application/json"); - request.setHeader("Content-Type", "application/json"); - - request.onComplete = function (error) { - if (error) { - return reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK, - message: error.toString(), - })); - } - - let body = null; - try { - body = JSON.parse(request.response.body); - } catch (e) { - return reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_PARSE, - errno: ERRNO_PARSE, - code: request.response.status, - message: request.response.body, - })); - } - - // "response.success" means status code is 200 - if (request.response.success) { - return resolve(body); - } - - if (typeof body.errno === 'number') { - // Offset oauth server errnos to avoid conflict with other FxA server errnos - body.errno += OAUTH_SERVER_ERRNO_OFFSET; - } else if (body.errno) { - body.errno = ERRNO_UNKNOWN_ERROR; - } - return reject(new FxAccountsOAuthGrantClientError(body)); - }; - - if (method === "POST") { - request.post(params); - } else { - // method not supported - return reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK, - code: ERROR_CODE_METHOD_NOT_ALLOWED, - message: ERROR_MSG_METHOD_NOT_ALLOWED, - })); - } - }); - }, - -}; - -/** - * Normalized profile client errors - * @param {Object} [details] - * Error details object - * @param {number} [details.code] - * Error code - * @param {number} [details.errno] - * Error number - * @param {String} [details.error] - * Error description - * @param {String|null} [details.message] - * Error message - * @constructor - */ -this.FxAccountsOAuthGrantClientError = function(details) { - details = details || {}; - - this.name = "FxAccountsOAuthGrantClientError"; - this.code = details.code || null; - this.errno = details.errno || ERRNO_UNKNOWN_ERROR; - this.error = details.error || ERROR_UNKNOWN; - this.message = details.message || null; -}; - -/** - * Returns error object properties - * - * @returns {{name: *, code: *, errno: *, error: *, message: *}} - * @private - */ -FxAccountsOAuthGrantClientError.prototype._toStringFields = function() { - return { - name: this.name, - code: this.code, - errno: this.errno, - error: this.error, - message: this.message, - }; -}; - -/** - * String representation of a oauth grant client error - * - * @returns {String} - */ -FxAccountsOAuthGrantClientError.prototype.toString = function() { - return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; -}; diff --git a/services/fxaccounts/FxAccountsProfile.jsm b/services/fxaccounts/FxAccountsProfile.jsm deleted file mode 100644 index b63cd64c1..000000000 --- a/services/fxaccounts/FxAccountsProfile.jsm +++ /dev/null @@ -1,191 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/** - * Firefox Accounts Profile helper. - * - * This class abstracts interaction with the profile server for an account. - * It will handle things like fetching profile data, listening for updates to - * the user's profile in open browser tabs, and cacheing/invalidating profile data. - */ - -this.EXPORTED_SYMBOLS = ["FxAccountsProfile"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient", - "resource://gre/modules/FxAccountsProfileClient.jsm"); - -// Based off of deepEqual from Assert.jsm -function deepEqual(actual, expected) { - if (actual === expected) { - return true; - } else if (typeof actual != "object" && typeof expected != "object") { - return actual == expected; - } else { - return objEquiv(actual, expected); - } -} - -function isUndefinedOrNull(value) { - return value === null || value === undefined; -} - -function objEquiv(a, b) { - if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { - return false; - } - if (a.prototype !== b.prototype) { - return false; - } - let ka, kb, key, i; - try { - ka = Object.keys(a); - kb = Object.keys(b); - } catch (e) { - return false; - } - if (ka.length != kb.length) { - return false; - } - ka.sort(); - kb.sort(); - for (i = ka.length - 1; i >= 0; i--) { - key = ka[i]; - if (!deepEqual(a[key], b[key])) { - return false; - } - } - return true; -} - -function hasChanged(oldData, newData) { - return !deepEqual(oldData, newData); -} - -this.FxAccountsProfile = function (options = {}) { - this._cachedProfile = null; - this._cachedAt = 0; // when we saved the cached version. - this._currentFetchPromise = null; - this._isNotifying = false; // are we sending a notification? - this.fxa = options.fxa || fxAccounts; - this.client = options.profileClient || new FxAccountsProfileClient({ - fxa: this.fxa, - serverURL: options.profileServerUrl, - }); - - // An observer to invalidate our _cachedAt optimization. We use a weak-ref - // just incase this.tearDown isn't called in some cases. - Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true); - // for testing - if (options.channel) { - this.channel = options.channel; - } -} - -this.FxAccountsProfile.prototype = { - // If we get subsequent requests for a profile within this period, don't bother - // making another request to determine if it is fresh or not. - PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes - - observe(subject, topic, data) { - // If we get a profile change notification from our webchannel it means - // the user has just changed their profile via the web, so we want to - // ignore our "freshness threshold" - if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) { - log.debug("FxAccountsProfile observed profile change"); - this._cachedAt = 0; - } - }, - - tearDown: function () { - this.fxa = null; - this.client = null; - this._cachedProfile = null; - Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION); - }, - - _getCachedProfile: function () { - // The cached profile will end up back in the generic accountData - // once bug 1157529 is fixed. - return Promise.resolve(this._cachedProfile); - }, - - _notifyProfileChange: function (uid) { - this._isNotifying = true; - Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); - this._isNotifying = false; - }, - - // Cache fetched data if it is different from what's in the cache. - // Send out a notification if it has changed so that UI can update. - _cacheProfile: function (profileData) { - if (!hasChanged(this._cachedProfile, profileData)) { - log.debug("fetched profile matches cached copy"); - return Promise.resolve(null); // indicates no change (but only tests care) - } - this._cachedProfile = profileData; - this._cachedAt = Date.now(); - return this.fxa.getSignedInUser() - .then(userData => { - log.debug("notifying profile changed for user ${uid}", userData); - this._notifyProfileChange(userData.uid); - return profileData; - }); - }, - - _fetchAndCacheProfile: function () { - if (!this._currentFetchPromise) { - this._currentFetchPromise = this.client.fetchProfile().then(profile => { - return this._cacheProfile(profile).then(() => { - return profile; - }); - }).then(profile => { - this._currentFetchPromise = null; - return profile; - }, err => { - this._currentFetchPromise = null; - throw err; - }); - } - return this._currentFetchPromise - }, - - // Returns cached data right away if available, then fetches the latest profile - // data in the background. After data is fetched a notification will be sent - // out if the profile has changed. - getProfile: function () { - return this._getCachedProfile() - .then(cachedProfile => { - if (cachedProfile) { - if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) { - // Note that _fetchAndCacheProfile isn't returned, so continues - // in the background. - this._fetchAndCacheProfile().catch(err => { - log.error("Background refresh of profile failed", err); - }); - } else { - log.trace("not checking freshness of profile as it remains recent"); - } - return cachedProfile; - } - return this._fetchAndCacheProfile(); - }) - .then(profile => { - return profile; - }); - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIObserver, - Ci.nsISupportsWeakReference, - ]), -}; diff --git a/services/fxaccounts/FxAccountsProfileClient.jsm b/services/fxaccounts/FxAccountsProfileClient.jsm deleted file mode 100644 index 1e5edc634..000000000 --- a/services/fxaccounts/FxAccountsProfileClient.jsm +++ /dev/null @@ -1,260 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * A client to fetch profile information for a Firefox Account. - */ - "use strict;" - -this.EXPORTED_SYMBOLS = ["FxAccountsProfileClient", "FxAccountsProfileClientError"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://services-common/rest.js"); - -Cu.importGlobalProperties(["URL"]); - -/** - * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information. - * - * @param {Object} options Options - * @param {String} options.serverURL - * The URL of the profile server to query. - * Example: https://profile.accounts.firefox.com/v1 - * @param {String} options.token - * The bearer token to access the profile server - * @constructor - */ -this.FxAccountsProfileClient = function(options) { - if (!options || !options.serverURL) { - throw new Error("Missing 'serverURL' configuration option"); - } - - this.fxa = options.fxa || fxAccounts; - // This is a work-around for loop that manages its own oauth tokens. - // * If |token| is in options we use it and don't attempt any token refresh - // on 401. This is for loop. - // * If |token| doesn't exist we will fetch our own token. This is for the - // normal FxAccounts methods for obtaining the profile. - // We should nuke all |this.token| support once loop moves closer to FxAccounts. - this.token = options.token; - - try { - this.serverURL = new URL(options.serverURL); - } catch (e) { - throw new Error("Invalid 'serverURL'"); - } - this.oauthOptions = { - scope: "profile", - }; - log.debug("FxAccountsProfileClient: Initialized"); -}; - -this.FxAccountsProfileClient.prototype = { - /** - * {nsIURI} - * The server to fetch profile information from. - */ - serverURL: null, - - /** - * Interface for making remote requests. - */ - _Request: RESTRequest, - - /** - * Remote request helper which abstracts authentication away. - * - * @param {String} path - * Profile server path, i.e "/profile". - * @param {String} [method] - * Type of request, i.e "GET". - * @return Promise - * Resolves: {Object} Successful response from the Profile server. - * Rejects: {FxAccountsProfileClientError} Profile client error. - * @private - */ - _createRequest: Task.async(function* (path, method = "GET") { - let token = this.token; - if (!token) { - // tokens are cached, so getting them each request is cheap. - token = yield this.fxa.getOAuthToken(this.oauthOptions); - } - try { - return (yield this._rawRequest(path, method, token)); - } catch (ex) { - if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) { - throw ex; - } - // If this object was instantiated with a token then we don't refresh it. - if (this.token) { - throw ex; - } - // it's an auth error - assume our token expired and retry. - log.info("Fetching the profile returned a 401 - revoking our token and retrying"); - yield this.fxa.removeCachedOAuthToken({token}); - token = yield this.fxa.getOAuthToken(this.oauthOptions); - // and try with the new token - if that also fails then we fail after - // revoking the token. - try { - return (yield this._rawRequest(path, method, token)); - } catch (ex) { - if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) { - throw ex; - } - log.info("Retry fetching the profile still returned a 401 - revoking our token and failing"); - yield this.fxa.removeCachedOAuthToken({token}); - throw ex; - } - } - }), - - /** - * Remote "raw" request helper - doesn't handle auth errors and tokens. - * - * @param {String} path - * Profile server path, i.e "/profile". - * @param {String} method - * Type of request, i.e "GET". - * @param {String} token - * @return Promise - * Resolves: {Object} Successful response from the Profile server. - * Rejects: {FxAccountsProfileClientError} Profile client error. - * @private - */ - _rawRequest: function(path, method, token) { - return new Promise((resolve, reject) => { - let profileDataUrl = this.serverURL + path; - let request = new this._Request(profileDataUrl); - method = method.toUpperCase(); - - request.setHeader("Authorization", "Bearer " + token); - request.setHeader("Accept", "application/json"); - - request.onComplete = function (error) { - if (error) { - return reject(new FxAccountsProfileClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK, - message: error.toString(), - })); - } - - let body = null; - try { - body = JSON.parse(request.response.body); - } catch (e) { - return reject(new FxAccountsProfileClientError({ - error: ERROR_PARSE, - errno: ERRNO_PARSE, - code: request.response.status, - message: request.response.body, - })); - } - - // "response.success" means status code is 200 - if (request.response.success) { - return resolve(body); - } else { - return reject(new FxAccountsProfileClientError({ - error: body.error || ERROR_UNKNOWN, - errno: body.errno || ERRNO_UNKNOWN_ERROR, - code: request.response.status, - message: body.message || body, - })); - } - }; - - if (method === "GET") { - request.get(); - } else { - // method not supported - return reject(new FxAccountsProfileClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK, - code: ERROR_CODE_METHOD_NOT_ALLOWED, - message: ERROR_MSG_METHOD_NOT_ALLOWED, - })); - } - }); - }, - - /** - * Retrieve user's profile from the server - * - * @return Promise - * Resolves: {Object} Successful response from the '/profile' endpoint. - * Rejects: {FxAccountsProfileClientError} profile client error. - */ - fetchProfile: function () { - log.debug("FxAccountsProfileClient: Requested profile"); - return this._createRequest("/profile", "GET"); - }, - - /** - * Retrieve user's profile from the server - * - * @return Promise - * Resolves: {Object} Successful response from the '/avatar' endpoint. - * Rejects: {FxAccountsProfileClientError} profile client error. - */ - fetchProfileImage: function () { - log.debug("FxAccountsProfileClient: Requested avatar"); - return this._createRequest("/avatar", "GET"); - } -}; - -/** - * Normalized profile client errors - * @param {Object} [details] - * Error details object - * @param {number} [details.code] - * Error code - * @param {number} [details.errno] - * Error number - * @param {String} [details.error] - * Error description - * @param {String|null} [details.message] - * Error message - * @constructor - */ -this.FxAccountsProfileClientError = function(details) { - details = details || {}; - - this.name = "FxAccountsProfileClientError"; - this.code = details.code || null; - this.errno = details.errno || ERRNO_UNKNOWN_ERROR; - this.error = details.error || ERROR_UNKNOWN; - this.message = details.message || null; -}; - -/** - * Returns error object properties - * - * @returns {{name: *, code: *, errno: *, error: *, message: *}} - * @private - */ -FxAccountsProfileClientError.prototype._toStringFields = function() { - return { - name: this.name, - code: this.code, - errno: this.errno, - error: this.error, - message: this.message, - }; -}; - -/** - * String representation of a profile client error - * - * @returns {String} - */ -FxAccountsProfileClientError.prototype.toString = function() { - return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; -}; diff --git a/services/fxaccounts/FxAccountsPush.js b/services/fxaccounts/FxAccountsPush.js deleted file mode 100644 index 358be06ee..000000000 --- a/services/fxaccounts/FxAccountsPush.js +++ /dev/null @@ -1,240 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/Task.jsm"); - -/** - * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser - * - * @param [options] - * Object, custom options that used for testing - * @constructor - */ -function FxAccountsPushService(options = {}) { - this.log = log; - - if (options.log) { - // allow custom log for testing purposes - this.log = options.log; - } - - this.log.debug("FxAccountsPush loading service"); - this.wrappedJSObject = this; - this.initialize(options); -} - -FxAccountsPushService.prototype = { - /** - * Helps only initialize observers once. - */ - _initialized: false, - /** - * Instance of the nsIPushService or a mocked object. - */ - pushService: null, - /** - * Instance of FxAccounts or a mocked object. - */ - fxAccounts: null, - /** - * Component ID of this service, helps register this component. - */ - classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"), - /** - * Register used interfaces in this service - */ - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - /** - * Initialize the service and register all the required observers. - * - * @param [options] - */ - initialize(options) { - if (this._initialized) { - return false; - } - - this._initialized = true; - - if (options.pushService) { - this.pushService = options.pushService; - } else { - this.pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService); - } - - if (options.fxAccounts) { - this.fxAccounts = options.fxAccounts; - } else { - XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - } - - // listen to new push messages, push changes and logout events - Services.obs.addObserver(this, this.pushService.pushTopic, false); - Services.obs.addObserver(this, this.pushService.subscriptionChangeTopic, false); - Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); - - this.log.debug("FxAccountsPush initialized"); - }, - /** - * Registers a new endpoint with the Push Server - * - * @returns {Promise} - * Promise always resolves with a subscription or a null if failed to subscribe. - */ - registerPushEndpoint() { - this.log.trace("FxAccountsPush registerPushEndpoint"); - - return new Promise((resolve) => { - this.pushService.subscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE, - Services.scriptSecurityManager.getSystemPrincipal(), - (result, subscription) => { - if (Components.isSuccessCode(result)) { - this.log.debug("FxAccountsPush got subscription"); - resolve(subscription); - } else { - this.log.warn("FxAccountsPush failed to subscribe", result); - resolve(null); - } - }); - }); - }, - /** - * Standard observer interface to listen to push messages, changes and logout. - * - * @param subject - * @param topic - * @param data - * @returns {Promise} - */ - _observe(subject, topic, data) { - this.log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`); - switch (topic) { - case this.pushService.pushTopic: - if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { - let message = subject.QueryInterface(Ci.nsIPushMessage); - return this._onPushMessage(message); - } - break; - case this.pushService.subscriptionChangeTopic: - if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) { - return this._onPushSubscriptionChange(); - } - break; - case ONLOGOUT_NOTIFICATION: - // user signed out, we need to stop polling the Push Server - return this.unsubscribe().catch(err => { - this.log.error("Error during unsubscribe", err); - }); - break; - default: - break; - } - }, - /** - * Wrapper around _observe that catches errors - */ - observe(subject, topic, data) { - Promise.resolve() - .then(() => this._observe(subject, topic, data)) - .catch(err => this.log.error(err)); - }, - /** - * Fired when the Push server sends a notification. - * - * @private - * @returns {Promise} - */ - _onPushMessage(message) { - this.log.trace("FxAccountsPushService _onPushMessage"); - if (!message.data) { - // Use the empty signal to check the verification state of the account right away - this.log.debug("empty push message - checking account status"); - return this.fxAccounts.checkVerificationStatus(); - } - let payload = message.data.json(); - this.log.debug(`push command: ${payload.command}`); - switch (payload.command) { - case ON_DEVICE_DISCONNECTED_NOTIFICATION: - return this.fxAccounts.handleDeviceDisconnection(payload.data.id); - break; - case ON_PASSWORD_CHANGED_NOTIFICATION: - case ON_PASSWORD_RESET_NOTIFICATION: - return this._onPasswordChanged(); - break; - case ON_COLLECTION_CHANGED_NOTIFICATION: - Services.obs.notifyObservers(null, ON_COLLECTION_CHANGED_NOTIFICATION, payload.data.collections); - default: - this.log.warn("FxA Push command unrecognized: " + payload.command); - } - }, - /** - * Check the FxA session status after a password change/reset event. - * If the session is invalid, reset credentials and notify listeners of - * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed - * - * @returns {Promise} - * @private - */ - _onPasswordChanged: Task.async(function* () { - if (!(yield this.fxAccounts.sessionStatus())) { - yield this.fxAccounts.resetCredentials(); - Services.obs.notifyObservers(null, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION, null); - } - }), - /** - * Fired when the Push server drops a subscription, or the subscription identifier changes. - * - * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages - * - * @returns {Promise} - * @private - */ - _onPushSubscriptionChange() { - this.log.trace("FxAccountsPushService _onPushSubscriptionChange"); - return this.fxAccounts.updateDeviceRegistration(); - }, - /** - * Unsubscribe from the Push server - * - * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe() - * - * @returns {Promise} - * @private - */ - unsubscribe() { - this.log.trace("FxAccountsPushService unsubscribe"); - return new Promise((resolve) => { - this.pushService.unsubscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE, - Services.scriptSecurityManager.getSystemPrincipal(), - (result, ok) => { - if (Components.isSuccessCode(result)) { - if (ok === true) { - this.log.debug("FxAccountsPushService unsubscribed"); - } else { - this.log.debug("FxAccountsPushService had no subscription to unsubscribe"); - } - } else { - this.log.warn("FxAccountsPushService failed to unsubscribe", result); - } - return resolve(ok); - }); - }); - }, -}; - -// Service registration below registers with FxAccountsComponents.manifest -const components = [FxAccountsPushService]; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); - -// The following registration below helps with testing this service. -this.EXPORTED_SYMBOLS=["FxAccountsPushService"]; diff --git a/services/fxaccounts/FxAccountsStorage.jsm b/services/fxaccounts/FxAccountsStorage.jsm deleted file mode 100644 index 4362cdf5b..000000000 --- a/services/fxaccounts/FxAccountsStorage.jsm +++ /dev/null @@ -1,606 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "FxAccountsStorageManagerCanStoreField", - "FxAccountsStorageManager", -]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/AppConstants.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://services-common/utils.js"); - -var haveLoginManager = true; - -// A helper function so code can check what fields are able to be stored by -// the storage manager without having a reference to a manager instance. -function FxAccountsStorageManagerCanStoreField(fieldName) { - return FXA_PWDMGR_MEMORY_FIELDS.has(fieldName) || - FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName) || - FXA_PWDMGR_SECURE_FIELDS.has(fieldName); -} - -// The storage manager object. -this.FxAccountsStorageManager = function(options = {}) { - this.options = { - filename: options.filename || DEFAULT_STORAGE_FILENAME, - baseDir: options.baseDir || OS.Constants.Path.profileDir, - } - this.plainStorage = new JSONStorage(this.options); - // On b2g we have no loginManager for secure storage, and tests may want - // to pretend secure storage isn't available. - let useSecure = 'useSecure' in options ? options.useSecure : haveLoginManager; - if (useSecure) { - this.secureStorage = new LoginManagerStorage(); - } else { - this.secureStorage = null; - } - this._clearCachedData(); - // See .initialize() below - this protects against it not being called. - this._promiseInitialized = Promise.reject("initialize not called"); - // A promise to avoid storage races - see _queueStorageOperation - this._promiseStorageComplete = Promise.resolve(); -} - -this.FxAccountsStorageManager.prototype = { - _initialized: false, - _needToReadSecure: true, - - // An initialization routine that *looks* synchronous to the callers, but - // is actually async as everything else waits for it to complete. - initialize(accountData) { - if (this._initialized) { - throw new Error("already initialized"); - } - this._initialized = true; - // If we just throw away our pre-rejected promise it is reported as an - // unhandled exception when it is GCd - so add an empty .catch handler here - // to prevent this. - this._promiseInitialized.catch(() => {}); - this._promiseInitialized = this._initialize(accountData); - }, - - _initialize: Task.async(function* (accountData) { - log.trace("initializing new storage manager"); - try { - if (accountData) { - // If accountData is passed we don't need to read any storage. - this._needToReadSecure = false; - // split it into the 2 parts, write it and we are done. - for (let [name, val] of Object.entries(accountData)) { - if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { - this.cachedPlain[name] = val; - } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { - this.cachedSecure[name] = val; - } else { - // Hopefully it's an "in memory" field. If it's not we log a warning - // but still treat it as such (so it will still be available in this - // session but isn't persisted anywhere.) - if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) { - log.warn("Unknown FxA field name in user data, treating as in-memory", name); - } - this.cachedMemory[name] = val; - } - } - // write it out and we are done. - yield this._write(); - return; - } - // So we were initialized without account data - that means we need to - // read the state from storage. We try and read plain storage first and - // only attempt to read secure storage if the plain storage had a user. - this._needToReadSecure = yield this._readPlainStorage(); - if (this._needToReadSecure && this.secureStorage) { - yield this._doReadAndUpdateSecure(); - } - } finally { - log.trace("initializing of new storage manager done"); - } - }), - - finalize() { - // We can't throw this instance away while it is still writing or we may - // end up racing with the newly created one. - log.trace("StorageManager finalizing"); - return this._promiseInitialized.then(() => { - return this._promiseStorageComplete; - }).then(() => { - this._promiseStorageComplete = null; - this._promiseInitialized = null; - this._clearCachedData(); - log.trace("StorageManager finalized"); - }) - }, - - // We want to make sure we don't end up doing multiple storage requests - // concurrently - which has a small window for reads if the master-password - // is locked at initialization time and becomes unlocked later, and always - // has an opportunity for updates. - // We also want to make sure we finished writing when finalizing, so we - // can't accidentally end up with the previous user's write finishing after - // a signOut attempts to clear it. - // So all such operations "queue" themselves via this. - _queueStorageOperation(func) { - // |result| is the promise we return - it has no .catch handler, so callers - // of the storage operation still see failure as a normal rejection. - let result = this._promiseStorageComplete.then(func); - // But the promise we assign to _promiseStorageComplete *does* have a catch - // handler so that rejections in one storage operation does not prevent - // future operations from starting (ie, _promiseStorageComplete must never - // be in a rejected state) - this._promiseStorageComplete = result.catch(err => { - log.error("${func} failed: ${err}", {func, err}); - }); - return result; - }, - - // Get the account data by combining the plain and secure storage. - // If fieldNames is specified, it may be a string or an array of strings, - // and only those fields are returned. If not specified the entire account - // data is returned except for "in memory" fields. Note that not specifying - // field names will soon be deprecated/removed - we want all callers to - // specify the fields they care about. - getAccountData: Task.async(function* (fieldNames = null) { - yield this._promiseInitialized; - // We know we are initialized - this means our .cachedPlain is accurate - // and doesn't need to be read (it was read if necessary by initialize). - // So if there's no uid, there's no user signed in. - if (!('uid' in this.cachedPlain)) { - return null; - } - let result = {}; - if (fieldNames === null) { - // The "old" deprecated way of fetching a logged in user. - for (let [name, value] of Object.entries(this.cachedPlain)) { - result[name] = value; - } - // But the secure data may not have been read, so try that now. - yield this._maybeReadAndUpdateSecure(); - // .cachedSecure now has as much as it possibly can (which is possibly - // nothing if (a) secure storage remains locked and (b) we've never updated - // a field to be stored in secure storage.) - for (let [name, value] of Object.entries(this.cachedSecure)) { - result[name] = value; - } - // Note we don't return cachedMemory fields here - they must be explicitly - // requested. - return result; - } - // The new explicit way of getting attributes. - if (!Array.isArray(fieldNames)) { - fieldNames = [fieldNames]; - } - let checkedSecure = false; - for (let fieldName of fieldNames) { - if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) { - if (this.cachedMemory[fieldName] !== undefined) { - result[fieldName] = this.cachedMemory[fieldName]; - } - } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) { - if (this.cachedPlain[fieldName] !== undefined) { - result[fieldName] = this.cachedPlain[fieldName]; - } - } else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) { - // We may not have read secure storage yet. - if (!checkedSecure) { - yield this._maybeReadAndUpdateSecure(); - checkedSecure = true; - } - if (this.cachedSecure[fieldName] !== undefined) { - result[fieldName] = this.cachedSecure[fieldName]; - } - } else { - throw new Error("unexpected field '" + name + "'"); - } - } - return result; - }), - - // Update just the specified fields. This DOES NOT allow you to change to - // a different user, nor to set the user as signed-out. - updateAccountData: Task.async(function* (newFields) { - yield this._promiseInitialized; - if (!('uid' in this.cachedPlain)) { - // If this storage instance shows no logged in user, then you can't - // update fields. - throw new Error("No user is logged in"); - } - if (!newFields || 'uid' in newFields || 'email' in newFields) { - // Once we support - // user changing email address this may need to change, but it's not - // clear how we would be told of such a change anyway... - throw new Error("Can't change uid or email address"); - } - log.debug("_updateAccountData with items", Object.keys(newFields)); - // work out what bucket. - for (let [name, value] of Object.entries(newFields)) { - if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) { - if (value == null) { - delete this.cachedMemory[name]; - } else { - this.cachedMemory[name] = value; - } - } else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) { - if (value == null) { - delete this.cachedPlain[name]; - } else { - this.cachedPlain[name] = value; - } - } else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) { - // don't do the "delete on null" thing here - we need to keep it until - // we have managed to read so we can nuke it on write. - this.cachedSecure[name] = value; - } else { - // Throwing seems reasonable here as some client code has explicitly - // specified the field name, so it's either confused or needs to update - // how this field is to be treated. - throw new Error("unexpected field '" + name + "'"); - } - } - // If we haven't yet read the secure data, do so now, else we may write - // out partial data. - yield this._maybeReadAndUpdateSecure(); - // Now save it - but don't wait on the _write promise - it's queued up as - // a storage operation, so .finalize() will wait for completion, but no need - // for us to. - this._write(); - }), - - _clearCachedData() { - this.cachedMemory = {}; - this.cachedPlain = {}; - // If we don't have secure storage available we have cachedPlain and - // cachedSecure be the same object. - this.cachedSecure = this.secureStorage == null ? this.cachedPlain : {}; - }, - - /* Reads the plain storage and caches the read values in this.cachedPlain. - Only ever called once and unlike the "secure" storage, is expected to never - fail (ie, plain storage is considered always available, whereas secure - storage may be unavailable if it is locked). - - Returns a promise that resolves with true if valid account data was found, - false otherwise. - - Note: _readPlainStorage is only called during initialize, so isn't - protected via _queueStorageOperation() nor _promiseInitialized. - */ - _readPlainStorage: Task.async(function* () { - let got; - try { - got = yield this.plainStorage.get(); - } catch(err) { - // File hasn't been created yet. That will be done - // when write is called. - if (!(err instanceof OS.File.Error) || !err.becauseNoSuchFile) { - log.error("Failed to read plain storage", err); - } - // either way, we return null. - got = null; - } - if (!got || !got.accountData || !got.accountData.uid || - got.version != DATA_FORMAT_VERSION) { - return false; - } - // We need to update our .cachedPlain, but can't just assign to it as - // it may need to be the exact same object as .cachedSecure - // As a sanity check, .cachedPlain must be empty (as we are called by init) - // XXX - this would be a good use-case for a RuntimeAssert or similar, as - // being added in bug 1080457. - if (Object.keys(this.cachedPlain).length != 0) { - throw new Error("should be impossible to have cached data already.") - } - for (let [name, value] of Object.entries(got.accountData)) { - this.cachedPlain[name] = value; - } - return true; - }), - - /* If we haven't managed to read the secure storage, try now, so - we can merge our cached data with the data that's already been set. - */ - _maybeReadAndUpdateSecure: Task.async(function* () { - if (this.secureStorage == null || !this._needToReadSecure) { - return; - } - return this._queueStorageOperation(() => { - if (this._needToReadSecure) { // we might have read it by now! - return this._doReadAndUpdateSecure(); - } - }); - }), - - /* Unconditionally read the secure storage and merge our cached data (ie, data - which has already been set while the secure storage was locked) with - the read data - */ - _doReadAndUpdateSecure: Task.async(function* () { - let { uid, email } = this.cachedPlain; - try { - log.debug("reading secure storage with existing", Object.keys(this.cachedSecure)); - // If we already have anything in .cachedSecure it means something has - // updated cachedSecure before we've read it. That means that after we do - // manage to read we must write back the merged data. - let needWrite = Object.keys(this.cachedSecure).length != 0; - let readSecure = yield this.secureStorage.get(uid, email); - // and update our cached data with it - anything already in .cachedSecure - // wins (including the fact it may be null or undefined, the latter - // which means it will be removed from storage. - if (readSecure && readSecure.version != DATA_FORMAT_VERSION) { - log.warn("got secure data but the data format version doesn't match"); - readSecure = null; - } - if (readSecure && readSecure.accountData) { - log.debug("secure read fetched items", Object.keys(readSecure.accountData)); - for (let [name, value] of Object.entries(readSecure.accountData)) { - if (!(name in this.cachedSecure)) { - this.cachedSecure[name] = value; - } - } - if (needWrite) { - log.debug("successfully read secure data; writing updated data back") - yield this._doWriteSecure(); - } - } - this._needToReadSecure = false; - } catch (ex) { - if (ex instanceof this.secureStorage.STORAGE_LOCKED) { - log.debug("setAccountData: secure storage is locked trying to read"); - } else { - log.error("failed to read secure storage", ex); - throw ex; - } - } - }), - - _write() { - // We don't want multiple writes happening concurrently, and we also need to - // know when an "old" storage manager is done (this.finalize() waits for this) - return this._queueStorageOperation(() => this.__write()); - }, - - __write: Task.async(function* () { - // Write everything back - later we could track what's actually dirty, - // but for now we write it all. - log.debug("writing plain storage", Object.keys(this.cachedPlain)); - let toWritePlain = { - version: DATA_FORMAT_VERSION, - accountData: this.cachedPlain, - } - yield this.plainStorage.set(toWritePlain); - - // If we have no secure storage manager we are done. - if (this.secureStorage == null) { - return; - } - // and only attempt to write to secure storage if we've managed to read it, - // otherwise we might clobber data that's already there. - if (!this._needToReadSecure) { - yield this._doWriteSecure(); - } - }), - - /* Do the actual write of secure data. Caller is expected to check if we actually - need to write and to ensure we are in a queued storage operation. - */ - _doWriteSecure: Task.async(function* () { - // We need to remove null items here. - for (let [name, value] of Object.entries(this.cachedSecure)) { - if (value == null) { - delete this.cachedSecure[name]; - } - } - log.debug("writing secure storage", Object.keys(this.cachedSecure)); - let toWriteSecure = { - version: DATA_FORMAT_VERSION, - accountData: this.cachedSecure, - } - try { - yield this.secureStorage.set(this.cachedPlain.uid, toWriteSecure); - } catch (ex) { - if (!(ex instanceof this.secureStorage.STORAGE_LOCKED)) { - throw ex; - } - // This shouldn't be possible as once it is unlocked it can't be - // re-locked, and we can only be here if we've previously managed to - // read. - log.error("setAccountData: secure storage is locked trying to write"); - } - }), - - // Delete the data for an account - ie, called on "sign out". - deleteAccountData() { - return this._queueStorageOperation(() => this._deleteAccountData()); - }, - - _deleteAccountData: Task.async(function* () { - log.debug("removing account data"); - yield this._promiseInitialized; - yield this.plainStorage.set(null); - if (this.secureStorage) { - yield this.secureStorage.set(null); - } - this._clearCachedData(); - log.debug("account data reset"); - }), -} - -/** - * JSONStorage constructor that creates instances that may set/get - * to a specified file, in a directory that will be created if it - * doesn't exist. - * - * @param options { - * filename: of the file to write to - * baseDir: directory where the file resides - * } - * @return instance - */ -function JSONStorage(options) { - this.baseDir = options.baseDir; - this.path = OS.Path.join(options.baseDir, options.filename); -}; - -JSONStorage.prototype = { - set: function(contents) { - log.trace("starting write of json user data", contents ? Object.keys(contents.accountData) : "null"); - let start = Date.now(); - return OS.File.makeDir(this.baseDir, {ignoreExisting: true}) - .then(CommonUtils.writeJSON.bind(null, contents, this.path)) - .then(result => { - log.trace("finished write of json user data - took", Date.now()-start); - return result; - }); - }, - - get: function() { - log.trace("starting fetch of json user data"); - let start = Date.now(); - return CommonUtils.readJSON(this.path).then(result => { - log.trace("finished fetch of json user data - took", Date.now()-start); - return result; - }); - }, -}; - -function StorageLockedError() { -} -/** - * LoginManagerStorage constructor that creates instances that set/get - * data stored securely in the nsILoginManager. - * - * @return instance - */ - -function LoginManagerStorage() { -} - -LoginManagerStorage.prototype = { - STORAGE_LOCKED: StorageLockedError, - // The fields in the credentials JSON object that are stored in plain-text - // in the profile directory. All other fields are stored in the login manager, - // and thus are only available when the master-password is unlocked. - - // a hook point for testing. - get _isLoggedIn() { - return Services.logins.isLoggedIn; - }, - - // Clear any data from the login manager. Returns true if the login manager - // was unlocked (even if no existing logins existed) or false if it was - // locked (meaning we don't even know if it existed or not.) - _clearLoginMgrData: Task.async(function* () { - try { // Services.logins might be third-party and broken... - yield Services.logins.initializationPromise; - if (!this._isLoggedIn) { - return false; - } - let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM); - for (let login of logins) { - Services.logins.removeLogin(login); - } - return true; - } catch (ex) { - log.error("Failed to clear login data: ${}", ex); - return false; - } - }), - - set: Task.async(function* (uid, contents) { - if (!contents) { - // Nuke it from the login manager. - let cleared = yield this._clearLoginMgrData(); - if (!cleared) { - // just log a message - we verify that the uid matches when - // we reload it, so having a stale entry doesn't really hurt. - log.info("not removing credentials from login manager - not logged in"); - } - log.trace("storage set finished clearing account data"); - return; - } - - // We are saving actual data. - log.trace("starting write of user data to the login manager"); - try { // Services.logins might be third-party and broken... - // and the stuff into the login manager. - yield Services.logins.initializationPromise; - // If MP is locked we silently fail - the user may need to re-auth - // next startup. - if (!this._isLoggedIn) { - log.info("not saving credentials to login manager - not logged in"); - throw new this.STORAGE_LOCKED(); - } - // write the data to the login manager. - let loginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new loginInfo(FXA_PWDMGR_HOST, - null, // aFormSubmitURL, - FXA_PWDMGR_REALM, // aHttpRealm, - uid, // aUsername - JSON.stringify(contents), // aPassword - "", // aUsernameField - "");// aPasswordField - - let existingLogins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, - FXA_PWDMGR_REALM); - if (existingLogins.length) { - Services.logins.modifyLogin(existingLogins[0], login); - } else { - Services.logins.addLogin(login); - } - log.trace("finished write of user data to the login manager"); - } catch (ex) { - if (ex instanceof this.STORAGE_LOCKED) { - throw ex; - } - // just log and consume the error here - it may be a 3rd party login - // manager replacement that's simply broken. - log.error("Failed to save data to the login manager", ex); - } - }), - - get: Task.async(function* (uid, email) { - log.trace("starting fetch of user data from the login manager"); - - try { // Services.logins might be third-party and broken... - // read the data from the login manager and merge it for return. - yield Services.logins.initializationPromise; - - if (!this._isLoggedIn) { - log.info("returning partial account data as the login manager is locked."); - throw new this.STORAGE_LOCKED(); - } - - let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM); - if (logins.length == 0) { - // This could happen if the MP was locked when we wrote the data. - log.info("Can't find any credentials in the login manager"); - return null; - } - let login = logins[0]; - // Support either the uid or the email as the username - as of bug 1183951 - // we store the uid, but we support having either for b/w compat. - if (login.username == uid || login.username == email) { - return JSON.parse(login.password); - } - log.info("username in the login manager doesn't match - ignoring it"); - yield this._clearLoginMgrData(); - } catch (ex) { - if (ex instanceof this.STORAGE_LOCKED) { - throw ex; - } - // just log and consume the error here - it may be a 3rd party login - // manager replacement that's simply broken. - log.error("Failed to get data from the login manager", ex); - } - return null; - }), -} - diff --git a/services/fxaccounts/FxAccountsWebChannel.jsm b/services/fxaccounts/FxAccountsWebChannel.jsm deleted file mode 100644 index 810d93c65..000000000 --- a/services/fxaccounts/FxAccountsWebChannel.jsm +++ /dev/null @@ -1,474 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Firefox Accounts Web Channel. - * - * Uses the WebChannel component to receive messages - * about account state changes. - */ - -this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WebChannel", - "resource://gre/modules/WebChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsStorageManagerCanStoreField", - "resource://gre/modules/FxAccountsStorage.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Weave", - "resource://services-sync/main.js"); - -const COMMAND_PROFILE_CHANGE = "profile:change"; -const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"; -const COMMAND_LOGIN = "fxaccounts:login"; -const COMMAND_LOGOUT = "fxaccounts:logout"; -const COMMAND_DELETE = "fxaccounts:delete"; -const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences"; -const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password"; - -const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; -const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog"; - -/** - * A helper function that extracts the message and stack from an error object. - * Returns a `{ message, stack }` tuple. `stack` will be null if the error - * doesn't have a stack trace. - */ -function getErrorDetails(error) { - let details = { message: String(error), stack: null }; - - // Adapted from Console.jsm. - if (error.stack) { - let frames = []; - for (let frame = error.stack; frame; frame = frame.caller) { - frames.push(String(frame).padStart(4)); - } - details.stack = frames.join("\n"); - } - - return details; -} - -/** - * Create a new FxAccountsWebChannel to listen for account updates - * - * @param {Object} options Options - * @param {Object} options - * @param {String} options.content_uri - * The FxA Content server uri - * @param {String} options.channel_id - * The ID of the WebChannel - * @param {String} options.helpers - * Helpers functions. Should only be passed in for testing. - * @constructor - */ -this.FxAccountsWebChannel = function(options) { - if (!options) { - throw new Error("Missing configuration options"); - } - if (!options["content_uri"]) { - throw new Error("Missing 'content_uri' option"); - } - this._contentUri = options.content_uri; - - if (!options["channel_id"]) { - throw new Error("Missing 'channel_id' option"); - } - this._webChannelId = options.channel_id; - - // options.helpers is only specified by tests. - this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options); - - this._setupChannel(); -}; - -this.FxAccountsWebChannel.prototype = { - /** - * WebChannel that is used to communicate with content page - */ - _channel: null, - - /** - * Helpers interface that does the heavy lifting. - */ - _helpers: null, - - /** - * WebChannel ID. - */ - _webChannelId: null, - /** - * WebChannel origin, used to validate origin of messages - */ - _webChannelOrigin: null, - - /** - * Release all resources that are in use. - */ - tearDown() { - this._channel.stopListening(); - this._channel = null; - this._channelCallback = null; - }, - - /** - * Configures and registers a new WebChannel - * - * @private - */ - _setupChannel() { - // if this.contentUri is present but not a valid URI, then this will throw an error. - try { - this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null); - this._registerChannel(); - } catch (e) { - log.error(e); - throw e; - } - }, - - _receiveMessage(message, sendingContext) { - let command = message.command; - let data = message.data; - - switch (command) { - case COMMAND_PROFILE_CHANGE: - Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid); - break; - case COMMAND_LOGIN: - this._helpers.login(data).catch(error => - this._sendError(error, message, sendingContext)); - break; - case COMMAND_LOGOUT: - case COMMAND_DELETE: - this._helpers.logout(data.uid).catch(error => - this._sendError(error, message, sendingContext)); - break; - case COMMAND_CAN_LINK_ACCOUNT: - let canLinkAccount = this._helpers.shouldAllowRelink(data.email); - - let response = { - command: command, - messageId: message.messageId, - data: { ok: canLinkAccount } - }; - - log.debug("FxAccountsWebChannel response", response); - this._channel.send(response, sendingContext); - break; - case COMMAND_SYNC_PREFERENCES: - this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint); - break; - case COMMAND_CHANGE_PASSWORD: - this._helpers.changePassword(data).catch(error => - this._sendError(error, message, sendingContext)); - break; - default: - log.warn("Unrecognized FxAccountsWebChannel command", command); - break; - } - }, - - _sendError(error, incomingMessage, sendingContext) { - log.error("Failed to handle FxAccountsWebChannel message", error); - this._channel.send({ - command: incomingMessage.command, - messageId: incomingMessage.messageId, - data: { - error: getErrorDetails(error), - }, - }, sendingContext); - }, - - /** - * Create a new channel with the WebChannelBroker, setup a callback listener - * @private - */ - _registerChannel() { - /** - * Processes messages that are called back from the FxAccountsChannel - * - * @param webChannelId {String} - * Command webChannelId - * @param message {Object} - * Command message - * @param sendingContext {Object} - * Message sending context. - * @param sendingContext.browser {browser} - * The <browser> object that captured the - * WebChannelMessageToChrome. - * @param sendingContext.eventTarget {EventTarget} - * The <EventTarget> where the message was sent. - * @param sendingContext.principal {Principal} - * The <Principal> of the EventTarget where the message was sent. - * @private - * - */ - let listener = (webChannelId, message, sendingContext) => { - if (message) { - log.debug("FxAccountsWebChannel message received", message.command); - if (logPII) { - log.debug("FxAccountsWebChannel message details", message); - } - try { - this._receiveMessage(message, sendingContext); - } catch (error) { - this._sendError(error, message, sendingContext); - } - } - }; - - this._channelCallback = listener; - this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); - this._channel.listen(listener); - log.debug("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); - } -}; - -this.FxAccountsWebChannelHelpers = function(options) { - options = options || {}; - - this._fxAccounts = options.fxAccounts || fxAccounts; -}; - -this.FxAccountsWebChannelHelpers.prototype = { - // If the last fxa account used for sync isn't this account, we display - // a modal dialog checking they really really want to do this... - // (This is sync-specific, so ideally would be in sync's identity module, - // but it's a little more seamless to do here, and sync is currently the - // only fxa consumer, so... - shouldAllowRelink(acctName) { - return !this._needRelinkWarning(acctName) || - this._promptForRelink(acctName); - }, - - /** - * New users are asked in the content server whether they want to - * customize which data should be synced. The user is only shown - * the dialog listing the possible data types upon verification. - * - * Save a bit into prefs that is read on verification to see whether - * to show the list of data types that can be saved. - */ - setShowCustomizeSyncPref(showCustomizeSyncPref) { - Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, showCustomizeSyncPref); - }, - - getShowCustomizeSyncPref() { - return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION); - }, - - /** - * stores sync login info it in the fxaccounts service - * - * @param accountData the user's account data and credentials - */ - login(accountData) { - if (accountData.customizeSync) { - this.setShowCustomizeSyncPref(true); - delete accountData.customizeSync; - } - - if (accountData.declinedSyncEngines) { - let declinedSyncEngines = accountData.declinedSyncEngines; - log.debug("Received declined engines", declinedSyncEngines); - Weave.Service.engineManager.setDeclined(declinedSyncEngines); - declinedSyncEngines.forEach(engine => { - Services.prefs.setBoolPref("services.sync.engine." + engine, false); - }); - - // if we got declinedSyncEngines that means we do not need to show the customize screen. - this.setShowCustomizeSyncPref(false); - delete accountData.declinedSyncEngines; - } - - // the user has already been shown the "can link account" - // screen. No need to keep this data around. - delete accountData.verifiedCanLinkAccount; - - // Remember who it was so we can log out next time. - this.setPreviousAccountNameHashPref(accountData.email); - - // A sync-specific hack - we want to ensure sync has been initialized - // before we set the signed-in user. - let xps = Cc["@mozilla.org/weave/service;1"] - .getService(Ci.nsISupports) - .wrappedJSObject; - return xps.whenLoaded().then(() => { - return this._fxAccounts.setSignedInUser(accountData); - }); - }, - - /** - * logout the fxaccounts service - * - * @param the uid of the account which have been logged out - */ - logout(uid) { - return fxAccounts.getSignedInUser().then(userData => { - if (userData.uid === uid) { - // true argument is `localOnly`, because server-side stuff - // has already been taken care of by the content server - return fxAccounts.signOut(true); - } - }); - }, - - changePassword(credentials) { - // If |credentials| has fields that aren't handled by accounts storage, - // updateUserAccountData will throw - mainly to prevent errors in code - // that hard-codes field names. - // However, in this case the field names aren't really in our control. - // We *could* still insist the server know what fields names are valid, - // but that makes life difficult for the server when Firefox adds new - // features (ie, new fields) - forcing the server to track a map of - // versions to supported field names doesn't buy us much. - // So we just remove field names we know aren't handled. - let newCredentials = { - deviceId: null - }; - for (let name of Object.keys(credentials)) { - if (name == "email" || name == "uid" || FxAccountsStorageManagerCanStoreField(name)) { - newCredentials[name] = credentials[name]; - } else { - log.info("changePassword ignoring unsupported field", name); - } - } - return this._fxAccounts.updateUserAccountData(newCredentials) - .then(() => this._fxAccounts.updateDeviceRegistration()); - }, - - /** - * Get the hash of account name of the previously signed in account - */ - getPreviousAccountNameHashPref() { - try { - return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; - } catch (_) { - return ""; - } - }, - - /** - * Given an account name, set the hash of the previously signed in account - * - * @param acctName the account name of the user's account. - */ - setPreviousAccountNameHashPref(acctName) { - let string = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - string.data = this.sha256(acctName); - Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); - }, - - /** - * Given a string, returns the SHA265 hash in base64 - */ - sha256(str) { - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - // Data is an array of bytes. - let data = converter.convertToByteArray(str, {}); - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher.SHA256); - hasher.update(data, data.length); - - return hasher.finish(true); - }, - - /** - * Open Sync Preferences in the current tab of the browser - * - * @param {Object} browser the browser in which to open preferences - * @param {String} [entryPoint] entryPoint to use for logging - */ - openSyncPreferences(browser, entryPoint) { - let uri = "about:preferences"; - if (entryPoint) { - uri += "?entrypoint=" + encodeURIComponent(entryPoint); - } - uri += "#sync"; - - browser.loadURI(uri); - }, - - /** - * If a user signs in using a different account, the data from the - * previous account and the new account will be merged. Ask the user - * if they want to continue. - * - * @private - */ - _needRelinkWarning(acctName) { - let prevAcctHash = this.getPreviousAccountNameHashPref(); - return prevAcctHash && prevAcctHash != this.sha256(acctName); - }, - - /** - * Show the user a warning dialog that the data from the previous account - * and the new account will be merged. - * - * @private - */ - _promptForRelink(acctName) { - let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); - let continueLabel = sb.GetStringFromName("continue.label"); - let title = sb.GetStringFromName("relinkVerify.title"); - let description = sb.formatStringFromName("relinkVerify.description", - [acctName], 1); - let body = sb.GetStringFromName("relinkVerify.heading") + - "\n\n" + description; - let ps = Services.prompt; - let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) + - (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) + - ps.BUTTON_POS_1_DEFAULT; - - // If running in context of the browser chrome, window does not exist. - var targetWindow = typeof window === 'undefined' ? null : window; - let pressed = Services.prompt.confirmEx(targetWindow, title, body, buttonFlags, - continueLabel, null, null, null, - {}); - return pressed === 0; // 0 is the "continue" button - } -}; - -var singleton; -// The entry-point for this module, which ensures only one of our channels is -// ever created - we require this because the WebChannel is global in scope -// (eg, it uses the observer service to tell interested parties of interesting -// things) and allowing multiple channels would cause such notifications to be -// sent multiple times. -this.EnsureFxAccountsWebChannel = function() { - let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri"); - if (singleton && singleton._contentUri !== contentUri) { - singleton.tearDown(); - singleton = null; - } - if (!singleton) { - try { - if (contentUri) { - // The FxAccountsWebChannel listens for events and updates - // the state machine accordingly. - singleton = new this.FxAccountsWebChannel({ - content_uri: contentUri, - channel_id: WEBCHANNEL_ID, - }); - } else { - log.warn("FxA WebChannel functionaly is disabled due to no URI pref."); - } - } catch (ex) { - log.error("Failed to create FxA WebChannel", ex); - } - } -} diff --git a/services/fxaccounts/interfaces/moz.build b/services/fxaccounts/interfaces/moz.build deleted file mode 100644 index ac80b3e93..000000000 --- a/services/fxaccounts/interfaces/moz.build +++ /dev/null @@ -1,11 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -XPIDL_SOURCES += [ - 'nsIFxAccountsUIGlue.idl' -] - -XPIDL_MODULE = 'services_fxaccounts' diff --git a/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl b/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl deleted file mode 100644 index 950fdbc25..000000000 --- a/services/fxaccounts/interfaces/nsIFxAccountsUIGlue.idl +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -[scriptable, uuid(ab8d0700-9577-11e3-a5e2-0800200c9a66)] -interface nsIFxAccountsUIGlue : nsISupports -{ - // Returns a Promise. - jsval signInFlow(); - - // Returns a Promise. - jsval refreshAuthentication(in DOMString email); -}; diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build deleted file mode 100644 index b1cd3b59c..000000000 --- a/services/fxaccounts/moz.build +++ /dev/null @@ -1,32 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DIRS += ['interfaces'] - -MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini'] - -XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini'] - -EXTRA_COMPONENTS += [ - 'FxAccountsComponents.manifest', - 'FxAccountsPush.js', -] - -EXTRA_JS_MODULES += [ - 'Credentials.jsm', - 'FxAccounts.jsm', - 'FxAccountsClient.jsm', - 'FxAccountsCommon.js', - 'FxAccountsConfig.jsm', - 'FxAccountsOAuthClient.jsm', - 'FxAccountsOAuthGrantClient.jsm', - 'FxAccountsProfile.jsm', - 'FxAccountsProfileClient.jsm', - 'FxAccountsPush.js', - 'FxAccountsStorage.jsm', - 'FxAccountsWebChannel.jsm', -] - diff --git a/services/fxaccounts/tests/mochitest/chrome.ini b/services/fxaccounts/tests/mochitest/chrome.ini deleted file mode 100644 index ab2e77053..000000000 --- a/services/fxaccounts/tests/mochitest/chrome.ini +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -skip-if = os == 'android' -support-files= - file_invalidEmailCase.sjs - -[test_invalidEmailCase.html] - diff --git a/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs b/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs deleted file mode 100644 index 9d97ac70c..000000000 --- a/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs +++ /dev/null @@ -1,80 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * This server simulates the behavior of /account/login on the Firefox Accounts - * auth server in the case where the user is trying to sign in with an email - * with the wrong capitalization. - * - * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin - * - * The expected behavior is that on the first attempt, with the wrong email, - * the server will respond with a 400 and the canonical email capitalization - * that the client should use. The client then has one chance to sign in with - * this different capitalization. - * - * In this test, the user with the account id "Greta.Garbo@gmail.COM" initially - * tries to sign in as "greta.garbo@gmail.com". - * - * On success, the client is responsible for updating its sign-in user state - * and recording the proper email capitalization. - */ - -const CC = Components.Constructor; -const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", - "nsIBinaryInputStream", - "setInputStream"); - -const goodEmail = "Greta.Garbo@gmail.COM"; -const badEmail = "greta.garbo@gmail.com"; - -function handleRequest(request, response) { - let body = new BinaryInputStream(request.bodyInputStream); - let bytes = []; - let available; - while ((available = body.available()) > 0) { - Array.prototype.push.apply(bytes, body.readByteArray(available)); - } - - let data = JSON.parse(String.fromCharCode.apply(null, bytes)); - let message; - - switch (data.email) { - case badEmail: - // Almost - try again with fixed email case - message = { - code: 400, - errno: 120, - error: "Incorrect email case", - email: goodEmail, - }; - response.setStatusLine(request.httpVersion, 400, "Almost"); - break; - - case goodEmail: - // Successful login. - message = { - uid: "your-uid", - sessionToken: "your-sessionToken", - keyFetchToken: "your-keyFetchToken", - verified: true, - authAt: 1392144866, - }; - response.setStatusLine(request.httpVersion, 200, "Yay"); - break; - - default: - // Anything else happening in this test is a failure. - message = { - code: 400, - errno: 999, - error: "What happened!?", - }; - response.setStatusLine(request.httpVersion, 400, "Ouch"); - break; - } - - messageStr = JSON.stringify(message); - response.bodyOutputStream.write(messageStr, messageStr.length); -} - diff --git a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html b/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html deleted file mode 100644 index 52866cc4b..000000000 --- a/services/fxaccounts/tests/mochitest/test_invalidEmailCase.html +++ /dev/null @@ -1,131 +0,0 @@ -<!-- - Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ ---> -<!DOCTYPE HTML> -<html> -<!-- -Tests for Firefox Accounts signin with invalid email case -https://bugzilla.mozilla.org/show_bug.cgi?id=963835 ---> -<head> - <title>Test for Firefox Accounts (Bug 963835)</title> - <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> -</head> -<body> - -<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=963835">Mozilla Bug 963835</a> -<p id="display"></p> -<div id="content" style="display: none"> - Test for correction of invalid email case in Fx Accounts signIn -</div> -<pre id="test"> -<script class="testbody" type="text/javascript;version=1.8"> - -SimpleTest.waitForExplicitFinish(); - -Components.utils.import("resource://gre/modules/Promise.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/FxAccounts.jsm"); -Components.utils.import("resource://gre/modules/FxAccountsClient.jsm"); -Components.utils.import("resource://services-common/hawkclient.js"); - -const TEST_SERVER = - "http://mochi.test:8888/chrome/services/fxaccounts/tests/mochitest/file_invalidEmailCase.sjs?path="; - -let MockStorage = function() { - this.data = null; -}; -MockStorage.prototype = Object.freeze({ - set: function (contents) { - this.data = contents; - return Promise.resolve(null); - }, - get: function () { - return Promise.resolve(this.data); - }, - getOAuthTokens() { - return Promise.resolve(null); - }, - setOAuthTokens(contents) { - return Promise.resolve(); - }, -}); - -function MockFxAccounts() { - return new FxAccounts({ - _now_is: new Date(), - - now: function() { - return this._now_is; - }, - - signedInUserStorage: new MockStorage(), - - fxAccountsClient: new FxAccountsClient(TEST_SERVER), - }); -} - -let wrongEmail = "greta.garbo@gmail.com"; -let rightEmail = "Greta.Garbo@gmail.COM"; -let password = "123456"; - -function runTest() { - is(Services.prefs.getCharPref("identity.fxaccounts.auth.uri"), TEST_SERVER, - "Pref for auth.uri should be set to test server"); - - let fxa = new MockFxAccounts(); - let client = fxa.internal.fxAccountsClient; - - ok(true, !!fxa, "Couldn't mock fxa"); - ok(true, !!client, "Couldn't mock fxa client"); - is(client.host, TEST_SERVER, "Should be using the test auth server uri"); - - // First try to sign in using the email with the wrong capitalization. The - // FxAccountsClient will receive a 400 from the server with the corrected email. - // It will automatically try to sign in again. We expect this to succeed. - client.signIn(wrongEmail, password).then( - user => { - - // Now store the signed-in user state. This will include the correct - // email capitalization. - fxa.setSignedInUser(user).then( - () => { - - // Confirm that the correct email got stored. - fxa.getSignedInUser().then( - data => { - is(data.email, rightEmail); - SimpleTest.finish(); - }, - getUserError => { - ok(false, JSON.stringify(getUserError)); - } - ); - }, - setSignedInUserError => { - ok(false, JSON.stringify(setSignedInUserError)); - } - ); - }, - signInError => { - ok(false, JSON.stringify(signInError)); - } - ); -}; - -SpecialPowers.pushPrefEnv({"set": [ - ["identity.fxaccounts.enabled", true], // fx accounts - ["identity.fxaccounts.auth.uri", TEST_SERVER], // our sjs server - ["toolkit.identity.debug", true], // verbose identity logging - ["browser.dom.window.dump.enabled", true], - ]}, - function () { runTest(); } -); - -</script> -</pre> -</body> -</html> - diff --git a/services/fxaccounts/tests/xpcshell/head.js b/services/fxaccounts/tests/xpcshell/head.js deleted file mode 100644 index ed70fdac5..000000000 --- a/services/fxaccounts/tests/xpcshell/head.js +++ /dev/null @@ -1,18 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -"use strict"; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -(function initFxAccountsTestingInfrastructure() { - do_get_profile(); - - let ns = {}; - Cu.import("resource://testing-common/services/common/logging.js", ns); - - ns.initTestLogging("Trace"); -}).call(this); - diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js deleted file mode 100644 index d6139a076..000000000 --- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ /dev/null @@ -1,1531 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); - -// We grab some additional stuff via backstage passes. -var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {}); - -const ONE_HOUR_MS = 1000 * 60 * 60; -const ONE_DAY_MS = ONE_HOUR_MS * 24; -const TWO_MINUTES_MS = 1000 * 60 * 2; - -initTestLogging("Trace"); - -// XXX until bug 937114 is fixed -Cu.importGlobalProperties(['atob']); - -var log = Log.repository.getLogger("Services.FxAccounts.test"); -log.level = Log.Level.Debug; - -// See verbose logging from FxAccounts.jsm -Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); -Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; - -// The oauth server is mocked, but set these prefs to pass param checks -Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); -Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123"); - - -const PROFILE_SERVER_URL = "http://example.com/v1"; -const CONTENT_URL = "http://accounts.example.com/"; - -Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL); -Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL); - -/* - * The FxAccountsClient communicates with the remote Firefox - * Accounts auth server. Mock the server calls, with a little - * lag time to simulate some latency. - * - * We add the _verified attribute to mock the change in verification - * state on the FXA server. - */ - -function MockStorageManager() { -} - -MockStorageManager.prototype = { - promiseInitialized: Promise.resolve(), - - initialize(accountData) { - this.accountData = accountData; - }, - - finalize() { - return Promise.resolve(); - }, - - getAccountData() { - return Promise.resolve(this.accountData); - }, - - updateAccountData(updatedFields) { - for (let [name, value] of Object.entries(updatedFields)) { - if (value == null) { - delete this.accountData[name]; - } else { - this.accountData[name] = value; - } - } - return Promise.resolve(); - }, - - deleteAccountData() { - this.accountData = null; - return Promise.resolve(); - } -} - -function MockFxAccountsClient() { - this._email = "nobody@example.com"; - this._verified = false; - this._deletedOnServer = false; // for testing accountStatus - - // mock calls up to the auth server to determine whether the - // user account has been verified - this.recoveryEmailStatus = function (sessionToken) { - // simulate a call to /recovery_email/status - return Promise.resolve({ - email: this._email, - verified: this._verified - }); - }; - - this.accountStatus = function(uid) { - let deferred = Promise.defer(); - deferred.resolve(!!uid && (!this._deletedOnServer)); - return deferred.promise; - }; - - this.accountKeys = function (keyFetchToken) { - let deferred = Promise.defer(); - - do_timeout(50, () => { - let response = { - kA: expandBytes("11"), - wrapKB: expandBytes("22") - }; - deferred.resolve(response); - }); - return deferred.promise; - }; - - this.resendVerificationEmail = function(sessionToken) { - // Return the session token to show that we received it in the first place - return Promise.resolve(sessionToken); - }; - - this.signCertificate = function() { throw "no" }; - - this.signOut = () => Promise.resolve(); - this.signOutAndDestroyDevice = () => Promise.resolve({}); - - FxAccountsClient.apply(this); -} -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -} - -/* - * We need to mock the FxAccounts module's interfaces to external - * services, such as storage and the FxAccounts client. We also - * mock the now() method, so that we can simulate the passing of - * time and verify that signatures expire correctly. - */ -function MockFxAccounts() { - return new FxAccounts({ - VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms - - _getCertificateSigned_calls: [], - _d_signCertificate: Promise.defer(), - _now_is: new Date(), - now: function () { - return this._now_is; - }, - newAccountState(credentials) { - // we use a real accountState but mocked storage. - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - getCertificateSigned: function (sessionToken, serializedPublicKey) { - _("mock getCertificateSigned\n"); - this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]); - return this._d_signCertificate.promise; - }, - _registerOrUpdateDevice() { - return Promise.resolve(); - }, - fxAccountsClient: new MockFxAccountsClient() - }); -} - -/* - * Some tests want a "real" fxa instance - however, we still mock the storage - * to keep the tests fast on b2g. - */ -function MakeFxAccounts(internal = {}) { - if (!internal.newAccountState) { - // we use a real accountState but mocked storage. - internal.newAccountState = function(credentials) { - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }; - } - if (!internal._signOutServer) { - internal._signOutServer = () => Promise.resolve(); - } - if (!internal._registerOrUpdateDevice) { - internal._registerOrUpdateDevice = () => Promise.resolve(); - } - return new FxAccounts(internal); -} - -add_task(function* test_non_https_remote_server_uri_with_requireHttps_false() { - Services.prefs.setBoolPref( - "identity.fxaccounts.allowHttp", - true); - Services.prefs.setCharPref( - "identity.fxaccounts.remote.signup.uri", - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - do_check_eq(yield fxAccounts.promiseAccountsSignUpURI(), - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - - Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); - Services.prefs.clearUserPref("identity.fxaccounts.allowHttp"); -}); - -add_task(function* test_non_https_remote_server_uri() { - Services.prefs.setCharPref( - "identity.fxaccounts.remote.signup.uri", - "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); - rejects(fxAccounts.promiseAccountsSignUpURI(), null, "Firefox Accounts server must use HTTPS"); - Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); -}); - -add_task(function* test_get_signed_in_user_initially_unset() { - _("Check getSignedInUser initially and after signout reports no user"); - let account = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - let result = yield account.getSignedInUser(); - do_check_eq(result, null); - - yield account.setSignedInUser(credentials); - let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED"); - do_check_eq(histogram.snapshot().sum, 1); - histogram.clear(); - - result = yield account.getSignedInUser(); - do_check_eq(result.email, credentials.email); - do_check_eq(result.assertion, credentials.assertion); - do_check_eq(result.kB, credentials.kB); - - // Delete the memory cache and force the user - // to be read and parsed from storage (e.g. disk via JSONStorage). - delete account.internal.signedInUser; - result = yield account.getSignedInUser(); - do_check_eq(result.email, credentials.email); - do_check_eq(result.assertion, credentials.assertion); - do_check_eq(result.kB, credentials.kB); - - // sign out - let localOnly = true; - yield account.signOut(localOnly); - - // user should be undefined after sign out - result = yield account.getSignedInUser(); - do_check_eq(result, null); -}); - -add_task(function* test_update_account_data() { - _("Check updateUserAccountData does the right thing."); - let account = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - yield account.setSignedInUser(credentials); - - let newCreds = { - email: credentials.email, - uid: credentials.uid, - assertion: "new_assertion", - } - yield account.updateUserAccountData(newCreds); - do_check_eq((yield account.getSignedInUser()).assertion, "new_assertion", - "new field value was saved"); - - // but we should fail attempting to change email or uid. - newCreds = { - email: "someoneelse@example.com", - uid: credentials.uid, - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - newCreds = { - email: credentials.email, - uid: "another_uid", - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - - // should fail without email or uid. - newCreds = { - assertion: "new_assertion", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); - - // and should fail with a field name that's not known by storage. - newCreds = { - email: credentials.email, - uid: "another_uid", - foo: "bar", - } - yield Assert.rejects(account.updateUserAccountData(newCreds)); -}); - -add_task(function* test_getCertificateOffline() { - _("getCertificateOffline()"); - let fxa = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - }; - - yield fxa.setSignedInUser(credentials); - - // Test that an expired cert throws if we're offline. - let offline = Services.io.offline; - Services.io.offline = true; - yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState).then( - result => { - Services.io.offline = offline; - do_throw("Unexpected success"); - }, - err => { - Services.io.offline = offline; - // ... so we have to check the error string. - do_check_eq(err, "Error: OFFLINE"); - } - ); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateCached() { - _("getCertificateCached()"); - let fxa = MakeFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair and cert that remain valid. - keyPair: { - validUntil: Date.now() + KEY_LIFETIME + 10000, - rawKeyPair: "good-keypair", - }, - cert: { - validUntil: Date.now() + CERT_LIFETIME + 10000, - rawCert: "good-cert", - }, - }; - - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // should have the same keypair and cert. - do_check_eq(keyPair, credentials.keyPair.rawKeyPair); - do_check_eq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateExpiredCert() { - _("getCertificateExpiredCert()"); - let fxa = MakeFxAccounts({ - getCertificateSigned() { - return "new cert"; - } - }); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair that remains valid. - keyPair: { - validUntil: Date.now() + KEY_LIFETIME + 10000, - rawKeyPair: "good-keypair", - }, - // A cached certificate which has expired. - cert: { - validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"), - rawCert: "expired-cert", - }, - }; - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // should have the same keypair but a new cert. - do_check_eq(keyPair, credentials.keyPair.rawKeyPair); - do_check_neq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -add_task(function* test_getCertificateExpiredKeypair() { - _("getCertificateExpiredKeypair()"); - let fxa = MakeFxAccounts({ - getCertificateSigned() { - return "new cert"; - }, - }); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - sessionToken: "dead", - verified: true, - // A cached keypair that has expired. - keyPair: { - validUntil: Date.now() - 1000, - rawKeyPair: "expired-keypair", - }, - // A cached certificate which remains valid. - cert: { - validUntil: Date.now() + CERT_LIFETIME + 10000, - rawCert: "expired-cert", - }, - }; - - yield fxa.setSignedInUser(credentials); - let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); - // even though the cert was valid, the fact the keypair was not means we - // should have fetched both. - do_check_neq(keyPair, credentials.keyPair.rawKeyPair); - do_check_neq(certificate, credentials.cert.rawCert); - yield fxa.signOut(/*localOnly = */true); -}); - -// Sanity-check that our mocked client is working correctly -add_test(function test_client_mock() { - let fxa = new MockFxAccounts(); - let client = fxa.internal.fxAccountsClient; - do_check_eq(client._verified, false); - do_check_eq(typeof client.signIn, "function"); - - // The recoveryEmailStatus function eventually fulfills its promise - client.recoveryEmailStatus() - .then(response => { - do_check_eq(response.verified, false); - run_next_test(); - }); -}); - -// Sign in a user, and after a little while, verify the user's email. -// Right after signing in the user, we should get the 'onlogin' notification. -// Polling should detect that the email is verified, and eventually -// 'onverified' should be observed -add_test(function test_verification_poll() { - let fxa = new MockFxAccounts(); - let test_user = getTestUser("francine"); - let login_notification_received = false; - - makeObserver(ONVERIFIED_NOTIFICATION, function() { - log.debug("test_verification_poll observed onverified"); - // Once email verification is complete, we will observe onverified - fxa.internal.getUserAccountData().then(user => { - // And confirm that the user's state has changed - do_check_eq(user.verified, true); - do_check_eq(user.email, test_user.email); - do_check_true(login_notification_received); - run_next_test(); - }); - }); - - makeObserver(ONLOGIN_NOTIFICATION, function() { - log.debug("test_verification_poll observer onlogin"); - login_notification_received = true; - }); - - fxa.setSignedInUser(test_user).then(() => { - fxa.internal.getUserAccountData().then(user => { - // The user is signing in, but email has not been verified yet - do_check_eq(user.verified, false); - do_timeout(200, function() { - log.debug("Mocking verification of francine's email"); - fxa.internal.fxAccountsClient._email = test_user.email; - fxa.internal.fxAccountsClient._verified = true; - }); - }); - }); -}); - -// Sign in the user, but never verify the email. The check-email -// poll should time out. No verifiedlogin event should be observed, and the -// internal whenVerified promise should be rejected -add_test(function test_polling_timeout() { - // This test could be better - the onverified observer might fire on - // somebody else's stack, and we're not making sure that we're not receiving - // such a message. In other words, this tests either failure, or success, but - // not both. - - let fxa = new MockFxAccounts(); - let test_user = getTestUser("carol"); - - let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() { - do_throw("We should not be getting a login event!"); - }); - - fxa.internal.POLL_SESSION = 1; - - let p = fxa.internal.whenVerified({}); - - fxa.setSignedInUser(test_user).then(() => { - p.then( - (success) => { - do_throw("this should not succeed"); - }, - (fail) => { - removeObserver(); - fxa.signOut().then(run_next_test); - } - ); - }); -}); - -add_test(function test_getKeys() { - let fxa = new MockFxAccounts(); - let user = getTestUser("eusebius"); - - // Once email has been verified, we will be able to get keys - user.verified = true; - - fxa.setSignedInUser(user).then(() => { - fxa.getSignedInUser().then((user) => { - // Before getKeys, we have no keys - do_check_eq(!!user.kA, false); - do_check_eq(!!user.kB, false); - // And we still have a key-fetch token and unwrapBKey to use - do_check_eq(!!user.keyFetchToken, true); - do_check_eq(!!user.unwrapBKey, true); - - fxa.internal.getKeys().then(() => { - fxa.getSignedInUser().then((user) => { - // Now we should have keys - do_check_eq(fxa.internal.isUserEmailVerified(user), true); - do_check_eq(!!user.verified, true); - do_check_eq(user.kA, expandHex("11")); - do_check_eq(user.kB, expandHex("66")); - do_check_eq(user.keyFetchToken, undefined); - do_check_eq(user.unwrapBKey, undefined); - run_next_test(); - }); - }); - }); - }); -}); - -add_task(function* test_getKeys_nonexistent_account() { - let fxa = new MockFxAccounts(); - let bismarck = getTestUser("bismarck"); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(false); - client.accountKeys = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - - yield fxa.setSignedInUser(bismarck); - - let promiseLogout = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_getKeys_nonexistent_account observed logout"); - resolve(); - }); - }); - - try { - yield fxa.internal.getKeys(); - do_check_true(false); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - yield promiseLogout; - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user, null); -}); - -// getKeys with invalid keyFetchToken should delete keyFetchToken from storage -add_task(function* test_getKeys_invalid_token() { - let fxa = new MockFxAccounts(); - let yusuf = getTestUser("yusuf"); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(true); - client.accountKeys = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - - yield fxa.setSignedInUser(yusuf); - - try { - yield fxa.internal.getKeys(); - do_check_true(false); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, yusuf.email); - do_check_eq(user.keyFetchToken, null); -}); - -// fetchAndUnwrapKeys with no keyFetchToken should trigger signOut -add_test(function test_fetchAndUnwrapKeys_no_token() { - let fxa = new MockFxAccounts(); - let user = getTestUser("lettuce.protheroe"); - delete user.keyFetchToken - - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_fetchAndUnwrapKeys_no_token observed logout"); - fxa.internal.getUserAccountData().then(user => { - run_next_test(); - }); - }); - - fxa.setSignedInUser(user).then( - user => { - return fxa.internal.fetchAndUnwrapKeys(); - } - ).then( - null, - error => { - log.info("setSignedInUser correctly rejected"); - } - ) -}); - -// Alice (User A) signs up but never verifies her email. Then Bob (User B) -// signs in with a verified email. Ensure that no sign-in events are triggered -// on Alice's behalf. In the end, Bob should be the signed-in user. -add_test(function test_overlapping_signins() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - let bob = getTestUser("bob"); - - makeObserver(ONVERIFIED_NOTIFICATION, function() { - log.debug("test_overlapping_signins observed onverified"); - // Once email verification is complete, we will observe onverified - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, bob.email); - do_check_eq(user.verified, true); - run_next_test(); - }); - }); - - // Alice is the user signing in; her email is unverified. - fxa.setSignedInUser(alice).then(() => { - log.debug("Alice signing in ..."); - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, false); - log.debug("Alice has not verified her email ..."); - - // Now Bob signs in instead and actually verifies his email - log.debug("Bob signing in ..."); - fxa.setSignedInUser(bob).then(() => { - do_timeout(200, function() { - // Mock email verification ... - log.debug("Bob verifying his email ..."); - fxa.internal.fxAccountsClient._verified = true; - }); - }); - }); - }); -}); - -add_task(function* test_getAssertion_invalid_token() { - let fxa = new MockFxAccounts(); - - let client = fxa.internal.fxAccountsClient; - client.accountStatus = () => Promise.resolve(true); - - let creds = { - sessionToken: "sessionToken", - kA: expandHex("11"), - kB: expandHex("66"), - verified: true, - email: "sonia@example.com", - }; - yield fxa.setSignedInUser(creds); - - try { - let promiseAssertion = fxa.getAssertion("audience.example.com"); - fxa.internal._d_signCertificate.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - yield promiseAssertion; - do_check_true(false, "getAssertion should reject invalid session token"); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, creds.email); - do_check_eq(user.sessionToken, null); -}); - -add_task(function* test_getAssertion() { - let fxa = new MockFxAccounts(); - - do_check_throws(function* () { - yield fxa.getAssertion("nonaudience"); - }); - - let creds = { - sessionToken: "sessionToken", - kA: expandHex("11"), - kB: expandHex("66"), - verified: true - }; - // By putting kA/kB/verified in "creds", we skip ahead - // to the "we're ready" stage. - yield fxa.setSignedInUser(creds); - - _("== ready to go\n"); - // Start with a nice arbitrary but realistic date. Here we use a nice RFC - // 1123 date string like we would get from an HTTP header. Over the course of - // the test, we will update 'now', but leave 'start' where it is. - let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT"); - let start = now; - fxa.internal._now_is = now; - - let d = fxa.getAssertion("audience.example.com"); - // At this point, a thread has been spawned to generate the keys. - _("-- back from fxa.getAssertion\n"); - fxa.internal._d_signCertificate.resolve("cert1"); - let assertion = yield d; - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); - do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken"); - do_check_neq(assertion, null); - _("ASSERTION: " + assertion + "\n"); - let pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert1"); - let userData = yield fxa.getSignedInUser(); - let keyPair = userData.keyPair; - let cert = userData.cert; - do_check_neq(keyPair, undefined); - _(keyPair.validUntil + "\n"); - let p2 = pieces[1].split("."); - let header = JSON.parse(atob(p2[0])); - _("HEADER: " + JSON.stringify(header) + "\n"); - do_check_eq(header.alg, "DS128"); - let payload = JSON.parse(atob(p2[1])); - _("PAYLOAD: " + JSON.stringify(payload) + "\n"); - do_check_eq(payload.aud, "audience.example.com"); - do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); - do_check_eq(cert.validUntil, start + CERT_LIFETIME); - _("delta: " + Date.parse(payload.exp - start) + "\n"); - let exp = Number(payload.exp); - - do_check_eq(exp, now + ASSERTION_LIFETIME); - - // Reset for next call. - fxa.internal._d_signCertificate = Promise.defer(); - - // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for - // a new audience, should not provoke key generation or a signing request. - assertion = yield fxa.getAssertion("other.example.com"); - - // There were no additional calls - same number of getcert calls as before - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); - - // Wait an hour; assertion use period expires, but not the certificate - now += ONE_HOUR_MS; - fxa.internal._now_is = now; - - // This won't block on anything - will make an assertion, but not get a - // new certificate. - assertion = yield fxa.getAssertion("third.example.com"); - - // Test will time out if that failed (i.e., if that had to go get a new cert) - pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert1"); - p2 = pieces[1].split("."); - header = JSON.parse(atob(p2[0])); - payload = JSON.parse(atob(p2[1])); - do_check_eq(payload.aud, "third.example.com"); - - // The keypair and cert should have the same validity as before, but the - // expiration time of the assertion should be different. We compare this to - // the initial start time, to which they are relative, not the current value - // of "now". - userData = yield fxa.getSignedInUser(); - - keyPair = userData.keyPair; - cert = userData.cert; - do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); - do_check_eq(cert.validUntil, start + CERT_LIFETIME); - exp = Number(payload.exp); - do_check_eq(exp, now + ASSERTION_LIFETIME); - - // Now we wait even longer, and expect both assertion and cert to expire. So - // we will have to get a new keypair and cert. - now += ONE_DAY_MS; - fxa.internal._now_is = now; - d = fxa.getAssertion("fourth.example.com"); - fxa.internal._d_signCertificate.resolve("cert2"); - assertion = yield d; - do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2); - do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken"); - pieces = assertion.split("~"); - do_check_eq(pieces[0], "cert2"); - p2 = pieces[1].split("."); - header = JSON.parse(atob(p2[0])); - payload = JSON.parse(atob(p2[1])); - do_check_eq(payload.aud, "fourth.example.com"); - userData = yield fxa.getSignedInUser(); - keyPair = userData.keyPair; - cert = userData.cert; - do_check_eq(keyPair.validUntil, now + KEY_LIFETIME); - do_check_eq(cert.validUntil, now + CERT_LIFETIME); - exp = Number(payload.exp); - - do_check_eq(exp, now + ASSERTION_LIFETIME); - _("----- DONE ----\n"); -}); - -add_task(function* test_resend_email_not_signed_in() { - let fxa = new MockFxAccounts(); - - try { - yield fxa.resendVerificationEmail(); - } catch(err) { - do_check_eq(err.message, - "Cannot resend verification email; no signed-in user"); - return; - } - do_throw("Should not be able to resend email when nobody is signed in"); -}); - -add_test(function test_accountStatus() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - // If we have no user, we have no account server-side - fxa.accountStatus().then( - (result) => { - do_check_false(result); - } - ).then( - () => { - fxa.setSignedInUser(alice).then( - () => { - fxa.accountStatus().then( - (result) => { - // FxAccounts.accountStatus() should match Client.accountStatus() - do_check_true(result); - fxa.internal.fxAccountsClient._deletedOnServer = true; - fxa.accountStatus().then( - (result) => { - do_check_false(result); - fxa.internal.fxAccountsClient._deletedOnServer = false; - fxa.signOut().then(run_next_test); - } - ); - } - ) - } - ); - } - ); -}); - -add_task(function* test_resend_email_invalid_token() { - let fxa = new MockFxAccounts(); - let sophia = getTestUser("sophia"); - do_check_neq(sophia.sessionToken, null); - - let client = fxa.internal.fxAccountsClient; - client.resendVerificationEmail = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - client.accountStatus = () => Promise.resolve(true); - - yield fxa.setSignedInUser(sophia); - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, sophia.email); - do_check_eq(user.verified, false); - log.debug("Sophia wants verification email resent"); - - try { - yield fxa.resendVerificationEmail(); - do_check_true(false, "resendVerificationEmail should reject invalid session token"); - } catch (err) { - do_check_eq(err.code, 401); - do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); - } - - user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, sophia.email); - do_check_eq(user.sessionToken, null); -}); - -add_test(function test_resend_email() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - let initialState = fxa.internal.currentAccountState; - - // Alice is the user signing in; her email is unverified. - fxa.setSignedInUser(alice).then(() => { - log.debug("Alice signing in"); - - // We're polling for the first email - do_check_true(fxa.internal.currentAccountState !== initialState); - let aliceState = fxa.internal.currentAccountState; - - // The polling timer is ticking - do_check_true(fxa.internal.currentTimer > 0); - - fxa.internal.getUserAccountData().then(user => { - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, false); - log.debug("Alice wants verification email resent"); - - fxa.resendVerificationEmail().then((result) => { - // Mock server response; ensures that the session token actually was - // passed to the client to make the hawk call - do_check_eq(result, "alice's session token"); - - // Timer was not restarted - do_check_true(fxa.internal.currentAccountState === aliceState); - - // Timer is still ticking - do_check_true(fxa.internal.currentTimer > 0); - - // Ok abort polling before we go on to the next test - fxa.internal.abortExistingFlow(); - run_next_test(); - }); - }); - }); -}); - -add_task(function* test_sign_out_with_device() { - const fxa = new MockFxAccounts(); - - const credentials = getTestUser("alice"); - yield fxa.internal.setSignedInUser(credentials); - - const user = yield fxa.internal.getUserAccountData(); - do_check_true(user); - Object.keys(credentials).forEach(key => do_check_eq(credentials[key], user[key])); - - const spy = { - signOut: { count: 0 }, - signOutAndDeviceDestroy: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.signOut = function () { - spy.signOut.count += 1; - return Promise.resolve(); - }; - client.signOutAndDestroyDevice = function () { - spy.signOutAndDeviceDestroy.count += 1; - spy.signOutAndDeviceDestroy.args.push(arguments); - return Promise.resolve(); - }; - - const promise = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, () => { - log.debug("test_sign_out_with_device observed onlogout"); - // user should be undefined after sign out - fxa.internal.getUserAccountData().then(user2 => { - do_check_eq(user2, null); - do_check_eq(spy.signOut.count, 0); - do_check_eq(spy.signOutAndDeviceDestroy.count, 1); - do_check_eq(spy.signOutAndDeviceDestroy.args[0].length, 3); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][0], credentials.sessionToken); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][1], credentials.deviceId); - do_check_true(spy.signOutAndDeviceDestroy.args[0][2]); - do_check_eq(spy.signOutAndDeviceDestroy.args[0][2].service, "sync"); - resolve(); - }); - }); - }); - - yield fxa.signOut(); - - yield promise; -}); - -add_task(function* test_sign_out_without_device() { - const fxa = new MockFxAccounts(); - - const credentials = getTestUser("alice"); - delete credentials.deviceId; - yield fxa.internal.setSignedInUser(credentials); - - const user = yield fxa.internal.getUserAccountData(); - - const spy = { - signOut: { count: 0, args: [] }, - signOutAndDeviceDestroy: { count: 0 } - }; - const client = fxa.internal.fxAccountsClient; - client.signOut = function () { - spy.signOut.count += 1; - spy.signOut.args.push(arguments); - return Promise.resolve(); - }; - client.signOutAndDestroyDevice = function () { - spy.signOutAndDeviceDestroy.count += 1; - return Promise.resolve(); - }; - - const promise = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, () => { - log.debug("test_sign_out_without_device observed onlogout"); - // user should be undefined after sign out - fxa.internal.getUserAccountData().then(user2 => { - do_check_eq(user2, null); - do_check_eq(spy.signOut.count, 1); - do_check_eq(spy.signOut.args[0].length, 2); - do_check_eq(spy.signOut.args[0][0], credentials.sessionToken); - do_check_true(spy.signOut.args[0][1]); - do_check_eq(spy.signOut.args[0][1].service, "sync"); - do_check_eq(spy.signOutAndDeviceDestroy.count, 0); - resolve(); - }); - }); - }); - - yield fxa.signOut(); - - yield promise; -}); - -add_task(function* test_sign_out_with_remote_error() { - let fxa = new MockFxAccounts(); - let client = fxa.internal.fxAccountsClient; - let remoteSignOutCalled = false; - // Force remote sign out to trigger an error - client.signOutAndDestroyDevice = function() { remoteSignOutCalled = true; throw "Remote sign out error"; }; - let promiseLogout = new Promise(resolve => { - makeObserver(ONLOGOUT_NOTIFICATION, function() { - log.debug("test_sign_out_with_remote_error observed onlogout"); - resolve(); - }); - }); - - let jane = getTestUser("jane"); - yield fxa.setSignedInUser(jane); - yield fxa.signOut(); - yield promiseLogout; - - let user = yield fxa.internal.getUserAccountData(); - do_check_eq(user, null); - do_check_true(remoteSignOutCalled); -}); - -add_test(function test_getOAuthToken() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let getTokenFromAssertionCalled = false; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - getTokenFromAssertionCalled = true; - return Promise.resolve({ access_token: "token" }); - }; - - fxa.setSignedInUser(alice).then( - () => { - fxa.getOAuthToken({ scope: "profile", client: client }).then( - (result) => { - do_check_true(getTokenFromAssertionCalled); - do_check_eq(result, "token"); - run_next_test(); - } - ) - } - ); - -}); - -add_test(function test_getOAuthTokenScoped() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let getTokenFromAssertionCalled = false; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function (assertion, scopeString) { - equal(scopeString, "foo bar"); - getTokenFromAssertionCalled = true; - return Promise.resolve({ access_token: "token" }); - }; - - fxa.setSignedInUser(alice).then( - () => { - fxa.getOAuthToken({ scope: ["foo", "bar"], client: client }).then( - (result) => { - do_check_true(getTokenFromAssertionCalled); - do_check_eq(result, "token"); - run_next_test(); - } - ) - } - ); - -}); - -add_task(function* test_getOAuthTokenCached() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let numTokenFromAssertionCalls = 0; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - numTokenFromAssertionCalls += 1; - return Promise.resolve({ access_token: "token" }); - }; - - yield fxa.setSignedInUser(alice); - let result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - - // requesting it again should not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // But requesting the same service and a different scope *will* get a new one. - result = yield fxa.getOAuthToken({ scope: "something-else", client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 2); - do_check_eq(result, "token"); -}); - -add_task(function* test_getOAuthTokenCachedScopeNormalization() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - let numTokenFromAssertionCalls = 0; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - numTokenFromAssertionCalls += 1; - return Promise.resolve({ access_token: "token" }); - }; - - yield fxa.setSignedInUser(alice); - let result = yield fxa.getOAuthToken({ scope: ["foo", "bar"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - - // requesting it again with the scope array in a different order not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: ["bar", "foo"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // requesting it again with the scope array in different case not re-fetch the token. - result = yield fxa.getOAuthToken({ scope: ["Bar", "Foo"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 1); - do_check_eq(result, "token"); - // But requesting with a new entry in the array does fetch one. - result = yield fxa.getOAuthToken({ scope: ["foo", "bar", "etc"], client: client, service: "test-service" }); - do_check_eq(numTokenFromAssertionCalls, 2); - do_check_eq(result, "token"); -}); - -Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); -add_test(function test_getOAuthToken_invalid_param() { - let fxa = new MockFxAccounts(); - - fxa.getOAuthToken() - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_invalid_scope_array() { - let fxa = new MockFxAccounts(); - - fxa.getOAuthToken({scope: []}) - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_misconfigure_oauth_uri() { - let fxa = new MockFxAccounts(); - - Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri"); - - fxa.getOAuthToken() - .then(null, err => { - do_check_eq(err.message, "INVALID_PARAMETER"); - // revert the pref - Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_no_account() { - let fxa = new MockFxAccounts(); - - fxa.internal.currentAccountState.getUserAccountData = function () { - return Promise.resolve(null); - }; - - fxa.getOAuthToken({ scope: "profile" }) - .then(null, err => { - do_check_eq(err.message, "NO_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); -}); - -add_test(function test_getOAuthToken_unverified() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile" }) - .then(null, err => { - do_check_eq(err.message, "UNVERIFIED_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - }); -}); - -add_test(function test_getOAuthToken_network_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_NETWORK, - errno: ERRNO_NETWORK - })); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "NETWORK_ERROR"); - do_check_eq(err.details.errno, ERRNO_NETWORK); - run_next_test(); - }); - }); -}); - -add_test(function test_getOAuthToken_auth_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject(new FxAccountsOAuthGrantClientError({ - error: ERROR_INVALID_FXA_ASSERTION, - errno: ERRNO_INVALID_FXA_ASSERTION - })); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "AUTH_ERROR"); - do_check_eq(err.details.errno, ERRNO_INVALID_FXA_ASSERTION); - run_next_test(); - }); - }); -}); - -add_test(function test_getOAuthToken_unknown_error() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal._d_signCertificate.resolve("cert1"); - - // create a mock oauth client - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://example.com/v1", - client_id: "abc123" - }); - client.getTokenFromAssertion = function () { - return Promise.reject("boom"); - }; - - fxa.setSignedInUser(alice).then(() => { - fxa.getOAuthToken({ scope: "profile", client: client }) - .then(null, err => { - do_check_eq(err.message, "UNKNOWN_ERROR"); - run_next_test(); - }); - }); -}); - -add_test(function test_getSignedInUserProfile() { - let alice = getTestUser("alice"); - alice.verified = true; - - let mockProfile = { - getProfile: function () { - return Promise.resolve({ avatar: "image" }); - }, - tearDown: function() {}, - }; - let fxa = new FxAccounts({ - _signOutServer() { return Promise.resolve(); }, - _registerOrUpdateDevice() { return Promise.resolve(); } - }); - - fxa.setSignedInUser(alice).then(() => { - fxa.internal._profile = mockProfile; - fxa.getSignedInUserProfile() - .then(result => { - do_check_true(!!result); - do_check_eq(result.avatar, "image"); - run_next_test(); - }); - }); -}); - -add_test(function test_getSignedInUserProfile_error_uses_account_data() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - fxa.internal.getSignedInUser = function () { - return Promise.resolve({ email: "foo@bar.com" }); - }; - - let teardownCalled = false; - fxa.setSignedInUser(alice).then(() => { - fxa.internal._profile = { - getProfile: function () { - return Promise.reject("boom"); - }, - tearDown: function() { - teardownCalled = true; - } - }; - - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "UNKNOWN_ERROR"); - fxa.signOut().then(() => { - do_check_true(teardownCalled); - run_next_test(); - }); - }); - }); -}); - -add_test(function test_getSignedInUserProfile_unverified_account() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - - fxa.setSignedInUser(alice).then(() => { - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "UNVERIFIED_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - }); - -}); - -add_test(function test_getSignedInUserProfile_no_account_data() { - let fxa = new MockFxAccounts(); - - fxa.internal.getSignedInUser = function () { - return Promise.resolve(null); - }; - - fxa.getSignedInUserProfile() - .catch(error => { - do_check_eq(error.message, "NO_ACCOUNT"); - fxa.signOut().then(run_next_test); - }); - -}); - -add_task(function* test_checkVerificationStatusFailed() { - let fxa = new MockFxAccounts(); - let alice = getTestUser("alice"); - alice.verified = true; - - let client = fxa.internal.fxAccountsClient; - client.recoveryEmailStatus = () => { - return Promise.reject({ - code: 401, - errno: ERRNO_INVALID_AUTH_TOKEN, - }); - }; - client.accountStatus = () => Promise.resolve(true); - - yield fxa.setSignedInUser(alice); - let user = yield fxa.internal.getUserAccountData(); - do_check_neq(alice.sessionToken, null); - do_check_eq(user.email, alice.email); - do_check_eq(user.verified, true); - - yield fxa.checkVerificationStatus(); - - user = yield fxa.internal.getUserAccountData(); - do_check_eq(user.email, alice.email); - do_check_eq(user.sessionToken, null); -}); - -/* - * End of tests. - * Utility functions follow. - */ - -function expandHex(two_hex) { - // Return a 64-character hex string, encoding 32 identical bytes. - let eight_hex = two_hex + two_hex + two_hex + two_hex; - let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex; - return thirtytwo_hex + thirtytwo_hex; -}; - -function expandBytes(two_hex) { - return CommonUtils.hexToBytes(expandHex(two_hex)); -}; - -function getTestUser(name) { - return { - email: name + "@example.com", - uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", - deviceId: name + "'s device id", - sessionToken: name + "'s session token", - keyFetchToken: name + "'s keyfetch token", - unwrapBKey: expandHex("44"), - verified: false - }; -} - -function makeObserver(aObserveTopic, aObserveFunc) { - let observer = { - // nsISupports provides type management in C++ - // nsIObserver is to be an observer - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), - - observe: function (aSubject, aTopic, aData) { - log.debug("observed " + aTopic + " " + aData); - if (aTopic == aObserveTopic) { - removeMe(); - aObserveFunc(aSubject, aTopic, aData); - } - } - }; - - function removeMe() { - log.debug("removing observer for " + aObserveTopic); - Services.obs.removeObserver(observer, aObserveTopic); - } - - Services.obs.addObserver(observer, aObserveTopic, false); - return removeMe; -} - -function do_check_throws(func, result, stack) -{ - if (!stack) - stack = Components.stack.caller; - - try { - func(); - } catch (ex) { - if (ex.name == result) { - return; - } - do_throw("Expected result " + result + ", caught " + ex.name, stack); - } - - if (result) { - do_throw("Expected result " + result + ", none thrown", stack); - } -} diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js deleted file mode 100644 index 9a2d2c127..000000000 --- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js +++ /dev/null @@ -1,526 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); - -initTestLogging("Trace"); - -var log = Log.repository.getLogger("Services.FxAccounts.test"); -log.level = Log.Level.Debug; - -const BOGUS_PUBLICKEY = "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc"; -const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw"; - -Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); -Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; - -Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); -Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123"); -Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", "http://example.com/v1"); -Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "http://accounts.example.com/"); - -const DEVICE_REGISTRATION_VERSION = 42; - -function MockStorageManager() { -} - -MockStorageManager.prototype = { - initialize(accountData) { - this.accountData = accountData; - }, - - finalize() { - return Promise.resolve(); - }, - - getAccountData() { - return Promise.resolve(this.accountData); - }, - - updateAccountData(updatedFields) { - for (let [name, value] of Object.entries(updatedFields)) { - if (value == null) { - delete this.accountData[name]; - } else { - this.accountData[name] = value; - } - } - return Promise.resolve(); - }, - - deleteAccountData() { - this.accountData = null; - return Promise.resolve(); - } -} - -function MockFxAccountsClient(device) { - this._email = "nobody@example.com"; - this._verified = false; - this._deletedOnServer = false; // for testing accountStatus - - // mock calls up to the auth server to determine whether the - // user account has been verified - this.recoveryEmailStatus = function (sessionToken) { - // simulate a call to /recovery_email/status - return Promise.resolve({ - email: this._email, - verified: this._verified - }); - }; - - this.accountStatus = function(uid) { - let deferred = Promise.defer(); - deferred.resolve(!!uid && (!this._deletedOnServer)); - return deferred.promise; - }; - - const { id: deviceId, name: deviceName, type: deviceType, sessionToken } = device; - - this.registerDevice = (st, name, type) => Promise.resolve({ id: deviceId, name }); - this.updateDevice = (st, id, name) => Promise.resolve({ id, name }); - this.signOutAndDestroyDevice = () => Promise.resolve({}); - this.getDeviceList = (st) => - Promise.resolve([ - { id: deviceId, name: deviceName, type: deviceType, isCurrentDevice: st === sessionToken } - ]); - - FxAccountsClient.apply(this); -} -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -} - -function MockFxAccounts(device = {}) { - return new FxAccounts({ - _getDeviceName() { - return device.name || "mock device name"; - }, - fxAccountsClient: new MockFxAccountsClient(device), - fxaPushService: { - registerPushEndpoint() { - return new Promise((resolve) => { - resolve({ - endpoint: "http://mochi.test:8888", - getKey: function(type) { - return ChromeUtils.base64URLDecode( - type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, - { padding: "ignore" }); - } - }); - }); - }, - }, - DEVICE_REGISTRATION_VERSION - }); -} - -add_task(function* test_updateDeviceRegistration_with_new_device() { - const deviceName = "foo"; - const deviceType = "bar"; - - const credentials = getTestUser("baz"); - delete credentials.deviceId; - const fxa = new MockFxAccounts({ name: deviceName }); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { - registerDevice: { count: 0, args: [] }, - updateDevice: { count: 0, args: [] }, - getDeviceList: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.registerDevice = function () { - spy.registerDevice.count += 1; - spy.registerDevice.args.push(arguments); - return Promise.resolve({ - id: "newly-generated device id", - createdAt: Date.now(), - name: deviceName, - type: deviceType - }); - }; - client.updateDevice = function () { - spy.updateDevice.count += 1; - spy.updateDevice.args.push(arguments); - return Promise.resolve({}); - }; - client.getDeviceList = function () { - spy.getDeviceList.count += 1; - spy.getDeviceList.args.push(arguments); - return Promise.resolve([]); - }; - - const result = yield fxa.updateDeviceRegistration(); - - do_check_eq(result, "newly-generated device id"); - do_check_eq(spy.updateDevice.count, 0); - do_check_eq(spy.getDeviceList.count, 0); - do_check_eq(spy.registerDevice.count, 1); - do_check_eq(spy.registerDevice.args[0].length, 4); - do_check_eq(spy.registerDevice.args[0][0], credentials.sessionToken); - do_check_eq(spy.registerDevice.args[0][1], deviceName); - do_check_eq(spy.registerDevice.args[0][2], "desktop"); - do_check_eq(spy.registerDevice.args[0][3].pushCallback, "http://mochi.test:8888"); - do_check_eq(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); - do_check_eq(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); - - const state = fxa.internal.currentAccountState; - const data = yield state.getUserAccountData(); - - do_check_eq(data.deviceId, "newly-generated device id"); - do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); -}); - -add_task(function* test_updateDeviceRegistration_with_existing_device() { - const deviceName = "phil's device"; - const deviceType = "desktop"; - - const credentials = getTestUser("pb"); - const fxa = new MockFxAccounts({ name: deviceName }); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { - registerDevice: { count: 0, args: [] }, - updateDevice: { count: 0, args: [] }, - getDeviceList: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.registerDevice = function () { - spy.registerDevice.count += 1; - spy.registerDevice.args.push(arguments); - return Promise.resolve({}); - }; - client.updateDevice = function () { - spy.updateDevice.count += 1; - spy.updateDevice.args.push(arguments); - return Promise.resolve({ - id: credentials.deviceId, - name: deviceName - }); - }; - client.getDeviceList = function () { - spy.getDeviceList.count += 1; - spy.getDeviceList.args.push(arguments); - return Promise.resolve([]); - }; - const result = yield fxa.updateDeviceRegistration(); - - do_check_eq(result, credentials.deviceId); - do_check_eq(spy.registerDevice.count, 0); - do_check_eq(spy.getDeviceList.count, 0); - do_check_eq(spy.updateDevice.count, 1); - do_check_eq(spy.updateDevice.args[0].length, 4); - do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken); - do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId); - do_check_eq(spy.updateDevice.args[0][2], deviceName); - do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888"); - do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); - do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); - - const state = fxa.internal.currentAccountState; - const data = yield state.getUserAccountData(); - - do_check_eq(data.deviceId, credentials.deviceId); - do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); -}); - -add_task(function* test_updateDeviceRegistration_with_unknown_device_error() { - const deviceName = "foo"; - const deviceType = "bar"; - - const credentials = getTestUser("baz"); - const fxa = new MockFxAccounts({ name: deviceName }); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { - registerDevice: { count: 0, args: [] }, - updateDevice: { count: 0, args: [] }, - getDeviceList: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.registerDevice = function () { - spy.registerDevice.count += 1; - spy.registerDevice.args.push(arguments); - return Promise.resolve({ - id: "a different newly-generated device id", - createdAt: Date.now(), - name: deviceName, - type: deviceType - }); - }; - client.updateDevice = function () { - spy.updateDevice.count += 1; - spy.updateDevice.args.push(arguments); - return Promise.reject({ - code: 400, - errno: ERRNO_UNKNOWN_DEVICE - }); - }; - client.getDeviceList = function () { - spy.getDeviceList.count += 1; - spy.getDeviceList.args.push(arguments); - return Promise.resolve([]); - }; - - const result = yield fxa.updateDeviceRegistration(); - - do_check_null(result); - do_check_eq(spy.getDeviceList.count, 0); - do_check_eq(spy.registerDevice.count, 0); - do_check_eq(spy.updateDevice.count, 1); - do_check_eq(spy.updateDevice.args[0].length, 4); - do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken); - do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId); - do_check_eq(spy.updateDevice.args[0][2], deviceName); - do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888"); - do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); - do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); - - - const state = fxa.internal.currentAccountState; - const data = yield state.getUserAccountData(); - - do_check_null(data.deviceId); - do_check_eq(data.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); -}); - -add_task(function* test_updateDeviceRegistration_with_device_session_conflict_error() { - const deviceName = "foo"; - const deviceType = "bar"; - - const credentials = getTestUser("baz"); - const fxa = new MockFxAccounts({ name: deviceName }); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { - registerDevice: { count: 0, args: [] }, - updateDevice: { count: 0, args: [], times: [] }, - getDeviceList: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.registerDevice = function () { - spy.registerDevice.count += 1; - spy.registerDevice.args.push(arguments); - return Promise.resolve({}); - }; - client.updateDevice = function () { - spy.updateDevice.count += 1; - spy.updateDevice.args.push(arguments); - spy.updateDevice.time = Date.now(); - if (spy.updateDevice.count === 1) { - return Promise.reject({ - code: 400, - errno: ERRNO_DEVICE_SESSION_CONFLICT - }); - } - return Promise.resolve({ - id: credentials.deviceId, - name: deviceName - }); - }; - client.getDeviceList = function () { - spy.getDeviceList.count += 1; - spy.getDeviceList.args.push(arguments); - spy.getDeviceList.time = Date.now(); - return Promise.resolve([ - { id: "ignore", name: "ignore", type: "ignore", isCurrentDevice: false }, - { id: credentials.deviceId, name: deviceName, type: deviceType, isCurrentDevice: true } - ]); - }; - - const result = yield fxa.updateDeviceRegistration(); - - do_check_eq(result, credentials.deviceId); - do_check_eq(spy.registerDevice.count, 0); - do_check_eq(spy.updateDevice.count, 1); - do_check_eq(spy.updateDevice.args[0].length, 4); - do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken); - do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId); - do_check_eq(spy.updateDevice.args[0][2], deviceName); - do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888"); - do_check_eq(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); - do_check_eq(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); - do_check_eq(spy.getDeviceList.count, 1); - do_check_eq(spy.getDeviceList.args[0].length, 1); - do_check_eq(spy.getDeviceList.args[0][0], credentials.sessionToken); - do_check_true(spy.getDeviceList.time >= spy.updateDevice.time); - - const state = fxa.internal.currentAccountState; - const data = yield state.getUserAccountData(); - - do_check_eq(data.deviceId, credentials.deviceId); - do_check_eq(data.deviceRegistrationVersion, null); -}); - -add_task(function* test_updateDeviceRegistration_with_unrecoverable_error() { - const deviceName = "foo"; - const deviceType = "bar"; - - const credentials = getTestUser("baz"); - delete credentials.deviceId; - const fxa = new MockFxAccounts({ name: deviceName }); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { - registerDevice: { count: 0, args: [] }, - updateDevice: { count: 0, args: [] }, - getDeviceList: { count: 0, args: [] } - }; - const client = fxa.internal.fxAccountsClient; - client.registerDevice = function () { - spy.registerDevice.count += 1; - spy.registerDevice.args.push(arguments); - return Promise.reject({ - code: 400, - errno: ERRNO_TOO_MANY_CLIENT_REQUESTS - }); - }; - client.updateDevice = function () { - spy.updateDevice.count += 1; - spy.updateDevice.args.push(arguments); - return Promise.resolve({}); - }; - client.getDeviceList = function () { - spy.getDeviceList.count += 1; - spy.getDeviceList.args.push(arguments); - return Promise.resolve([]); - }; - - const result = yield fxa.updateDeviceRegistration(); - - do_check_null(result); - do_check_eq(spy.getDeviceList.count, 0); - do_check_eq(spy.updateDevice.count, 0); - do_check_eq(spy.registerDevice.count, 1); - do_check_eq(spy.registerDevice.args[0].length, 4); - - const state = fxa.internal.currentAccountState; - const data = yield state.getUserAccountData(); - - do_check_null(data.deviceId); -}); - -add_task(function* test_getDeviceId_with_no_device_id_invokes_device_registration() { - const credentials = getTestUser("foo"); - credentials.verified = true; - delete credentials.deviceId; - const fxa = new MockFxAccounts(); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { count: 0, args: [] }; - fxa.internal.currentAccountState.getUserAccountData = - () => Promise.resolve({ email: credentials.email, - deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION }); - fxa.internal._registerOrUpdateDevice = function () { - spy.count += 1; - spy.args.push(arguments); - return Promise.resolve("bar"); - }; - - const result = yield fxa.internal.getDeviceId(); - - do_check_eq(spy.count, 1); - do_check_eq(spy.args[0].length, 1); - do_check_eq(spy.args[0][0].email, credentials.email); - do_check_null(spy.args[0][0].deviceId); - do_check_eq(result, "bar"); -}); - -add_task(function* test_getDeviceId_with_registration_version_outdated_invokes_device_registration() { - const credentials = getTestUser("foo"); - credentials.verified = true; - const fxa = new MockFxAccounts(); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { count: 0, args: [] }; - fxa.internal.currentAccountState.getUserAccountData = - () => Promise.resolve({ deviceId: credentials.deviceId, deviceRegistrationVersion: 0 }); - fxa.internal._registerOrUpdateDevice = function () { - spy.count += 1; - spy.args.push(arguments); - return Promise.resolve("wibble"); - }; - - const result = yield fxa.internal.getDeviceId(); - - do_check_eq(spy.count, 1); - do_check_eq(spy.args[0].length, 1); - do_check_eq(spy.args[0][0].deviceId, credentials.deviceId); - do_check_eq(result, "wibble"); -}); - -add_task(function* test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() { - const credentials = getTestUser("foo"); - credentials.verified = true; - const fxa = new MockFxAccounts(); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { count: 0 }; - fxa.internal.currentAccountState.getUserAccountData = - () => Promise.resolve({ deviceId: credentials.deviceId, deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION }); - fxa.internal._registerOrUpdateDevice = function () { - spy.count += 1; - return Promise.resolve("bar"); - }; - - const result = yield fxa.internal.getDeviceId(); - - do_check_eq(spy.count, 0); - do_check_eq(result, "foo's device id"); -}); - -add_task(function* test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() { - const credentials = getTestUser("foo"); - credentials.verified = true; - const fxa = new MockFxAccounts(); - yield fxa.internal.setSignedInUser(credentials); - - const spy = { count: 0, args: [] }; - fxa.internal.currentAccountState.getUserAccountData = - () => Promise.resolve({ deviceId: credentials.deviceId }); - fxa.internal._registerOrUpdateDevice = function () { - spy.count += 1; - spy.args.push(arguments); - return Promise.resolve("wibble"); - }; - - const result = yield fxa.internal.getDeviceId(); - - do_check_eq(spy.count, 1); - do_check_eq(spy.args[0].length, 1); - do_check_eq(spy.args[0][0].deviceId, credentials.deviceId); - do_check_eq(result, "wibble"); -}); - -function expandHex(two_hex) { - // Return a 64-character hex string, encoding 32 identical bytes. - let eight_hex = two_hex + two_hex + two_hex + two_hex; - let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex; - return thirtytwo_hex + thirtytwo_hex; -}; - -function expandBytes(two_hex) { - return CommonUtils.hexToBytes(expandHex(two_hex)); -}; - -function getTestUser(name) { - return { - email: name + "@example.com", - uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", - deviceId: name + "'s device id", - sessionToken: name + "'s session token", - keyFetchToken: name + "'s keyfetch token", - unwrapBKey: expandHex("44"), - verified: false - }; -} - diff --git a/services/fxaccounts/tests/xpcshell/test_client.js b/services/fxaccounts/tests/xpcshell/test_client.js deleted file mode 100644 index 83f42bdf5..000000000 --- a/services/fxaccounts/tests/xpcshell/test_client.js +++ /dev/null @@ -1,917 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-common/hawkrequest.js"); -Cu.import("resource://services-crypto/utils.js"); - -const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"; - -function run_test() { - run_next_test(); -} - -// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys -var ACCOUNT_KEYS = { - keyFetch: h("8081828384858687 88898a8b8c8d8e8f"+ - "9091929394959697 98999a9b9c9d9e9f"), - - response: h("ee5c58845c7c9412 b11bbd20920c2fdd"+ - "d83c33c9cd2c2de2 d66b222613364636"+ - "c2c0f8cfbb7c6304 72c0bd88451342c6"+ - "c05b14ce342c5ad4 6ad89e84464c993c"+ - "3927d30230157d08 17a077eef4b20d97"+ - "6f7a97363faf3f06 4c003ada7d01aa70"), - - kA: h("2021222324252627 28292a2b2c2d2e2f"+ - "3031323334353637 38393a3b3c3d3e3f"), - - wrapKB: h("4041424344454647 48494a4b4c4d4e4f"+ - "5051525354555657 58595a5b5c5d5e5f"), -}; - -function deferredStop(server) { - let deferred = Promise.defer(); - server.stop(deferred.resolve); - return deferred.promise; -} - -add_task(function* test_authenticated_get_request() { - let message = "{\"msg\": \"Great Success!\"}"; - let credentials = { - id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", - key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", - algorithm: "sha256" - }; - let method = "GET"; - - let server = httpd_setup({"/foo": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(message, message.length); - } - }); - - let client = new FxAccountsClient(server.baseURI); - - let result = yield client._request("/foo", method, credentials); - do_check_eq("Great Success!", result.msg); - - yield deferredStop(server); -}); - -add_task(function* test_authenticated_post_request() { - let credentials = { - id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", - key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", - algorithm: "sha256" - }; - let method = "POST"; - - let server = httpd_setup({"/foo": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.setHeader("Content-Type", "application/json"); - response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available()); - } - }); - - let client = new FxAccountsClient(server.baseURI); - - let result = yield client._request("/foo", method, credentials, {foo: "bar"}); - do_check_eq("bar", result.foo); - - yield deferredStop(server); -}); - -add_task(function* test_500_error() { - let message = "<h1>Ooops!</h1>"; - let method = "GET"; - - let server = httpd_setup({"/foo": function(request, response) { - response.setStatusLine(request.httpVersion, 500, "Internal Server Error"); - response.bodyOutputStream.write(message, message.length); - } - }); - - let client = new FxAccountsClient(server.baseURI); - - try { - yield client._request("/foo", method); - do_throw("Expected to catch an exception"); - } catch (e) { - do_check_eq(500, e.code); - do_check_eq("Internal Server Error", e.message); - } - - yield deferredStop(server); -}); - -add_task(function* test_backoffError() { - let method = "GET"; - let server = httpd_setup({ - "/retryDelay": function(request, response) { - response.setHeader("Retry-After", "30"); - response.setStatusLine(request.httpVersion, 429, "Client has sent too many requests"); - let message = "<h1>Ooops!</h1>"; - response.bodyOutputStream.write(message, message.length); - }, - "/duringDelayIShouldNotBeCalled": function(request, response) { - response.setStatusLine(request.httpVersion, 200, "OK"); - let jsonMessage = "{\"working\": \"yes\"}"; - response.bodyOutputStream.write(jsonMessage, jsonMessage.length); - }, - }); - - let client = new FxAccountsClient(server.baseURI); - - // Retry-After header sets client.backoffError - do_check_eq(client.backoffError, null); - try { - yield client._request("/retryDelay", method); - } catch (e) { - do_check_eq(429, e.code); - do_check_eq(30, e.retryAfter); - do_check_neq(typeof(client.fxaBackoffTimer), "undefined"); - do_check_neq(client.backoffError, null); - } - // While delay is in effect, client short-circuits any requests - // and re-rejects with previous error. - try { - yield client._request("/duringDelayIShouldNotBeCalled", method); - throw new Error("I should not be reached"); - } catch (e) { - do_check_eq(e.retryAfter, 30); - do_check_eq(e.message, "Client has sent too many requests"); - do_check_neq(client.backoffError, null); - } - // Once timer fires, client nulls error out and HTTP calls work again. - client._clearBackoff(); - let result = yield client._request("/duringDelayIShouldNotBeCalled", method); - do_check_eq(client.backoffError, null); - do_check_eq(result.working, "yes"); - - yield deferredStop(server); -}); - -add_task(function* test_signUp() { - let creationMessage_noKey = JSON.stringify({ - uid: "uid", - sessionToken: "sessionToken" - }); - let creationMessage_withKey = JSON.stringify({ - uid: "uid", - sessionToken: "sessionToken", - keyFetchToken: "keyFetchToken" - }); - let errorMessage = JSON.stringify({code: 400, errno: 101, error: "account exists"}); - let created = false; - - // Note these strings must be unicode and not already utf-8 encoded. - let unicodeUsername = "andr\xe9@example.org"; // 'andré@example.org' - let unicodePassword = "p\xe4ssw\xf6rd"; // 'pässwörd' - let server = httpd_setup({ - "/account/create": function(request, response) { - let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); - body = CommonUtils.decodeUTF8(body); - let jsonBody = JSON.parse(body); - - // https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors - - if (created) { - // Error trying to create same account a second time - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - return; - } - - if (jsonBody.email == unicodeUsername) { - do_check_eq("", request._queryString); - do_check_eq(jsonBody.authPW, "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375"); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(creationMessage_noKey, - creationMessage_noKey.length); - return; - } - - if (jsonBody.email == "you@example.org") { - do_check_eq("keys=true", request._queryString); - do_check_eq(jsonBody.authPW, "e5c1cdfdaa5fcee06142db865b212cc8ba8abee2a27d639d42c139f006cdb930"); - created = true; - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(creationMessage_withKey, - creationMessage_withKey.length); - return; - } - // just throwing here doesn't make any log noise, so have an assertion - // fail instead. - do_check_true(false, "unexpected email: " + jsonBody.email); - }, - }); - - // Try to create an account without retrieving optional keys. - let client = new FxAccountsClient(server.baseURI); - let result = yield client.signUp(unicodeUsername, unicodePassword); - do_check_eq("uid", result.uid); - do_check_eq("sessionToken", result.sessionToken); - do_check_eq(undefined, result.keyFetchToken); - do_check_eq(result.unwrapBKey, - "de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28"); - - // Try to create an account retrieving optional keys. - result = yield client.signUp('you@example.org', 'pässwörd', true); - do_check_eq("uid", result.uid); - do_check_eq("sessionToken", result.sessionToken); - do_check_eq("keyFetchToken", result.keyFetchToken); - do_check_eq(result.unwrapBKey, - "f589225b609e56075d76eb74f771ff9ab18a4dc0e901e131ba8f984c7fb0ca8c"); - - // Try to create an existing account. Triggers error path. - try { - result = yield client.signUp(unicodeUsername, unicodePassword); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(101, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_signIn() { - let sessionMessage_noKey = JSON.stringify({ - sessionToken: FAKE_SESSION_TOKEN - }); - let sessionMessage_withKey = JSON.stringify({ - sessionToken: FAKE_SESSION_TOKEN, - keyFetchToken: "keyFetchToken" - }); - let errorMessage_notExistent = JSON.stringify({ - code: 400, - errno: 102, - error: "doesn't exist" - }); - let errorMessage_wrongCap = JSON.stringify({ - code: 400, - errno: 120, - error: "Incorrect email case", - email: "you@example.com" - }); - - // Note this strings must be unicode and not already utf-8 encoded. - let unicodeUsername = "m\xe9@example.com" // 'mé@example.com' - let server = httpd_setup({ - "/account/login": function(request, response) { - let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); - body = CommonUtils.decodeUTF8(body); - let jsonBody = JSON.parse(body); - - if (jsonBody.email == unicodeUsername) { - do_check_eq("", request._queryString); - do_check_eq(jsonBody.authPW, "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6"); - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(sessionMessage_noKey, - sessionMessage_noKey.length); - return; - } - else if (jsonBody.email == "you@example.com") { - do_check_eq("keys=true", request._queryString); - do_check_eq(jsonBody.authPW, "93d20ec50304d496d0707ec20d7e8c89459b6396ec5dd5b9e92809c5e42856c7"); - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(sessionMessage_withKey, - sessionMessage_withKey.length); - return; - } - else if (jsonBody.email == "You@example.com") { - // Error trying to sign in with a wrong capitalization - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage_wrongCap, - errorMessage_wrongCap.length); - return; - } - else { - // Error trying to sign in to nonexistent account - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage_notExistent, - errorMessage_notExistent.length); - return; - } - }, - }); - - // Login without retrieving optional keys - let client = new FxAccountsClient(server.baseURI); - let result = yield client.signIn(unicodeUsername, 'bigsecret'); - do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken); - do_check_eq(result.unwrapBKey, - "c076ec3f4af123a615157154c6e1d0d6293e514fd7b0221e32d50517ecf002b8"); - do_check_eq(undefined, result.keyFetchToken); - - // Login with retrieving optional keys - result = yield client.signIn('you@example.com', 'bigsecret', true); - do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken); - do_check_eq(result.unwrapBKey, - "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624"); - do_check_eq("keyFetchToken", result.keyFetchToken); - - // Retry due to wrong email capitalization - result = yield client.signIn('You@example.com', 'bigsecret', true); - do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken); - do_check_eq(result.unwrapBKey, - "65970516211062112e955d6420bebe020269d6b6a91ebd288319fc8d0cb49624"); - do_check_eq("keyFetchToken", result.keyFetchToken); - - // Trigger error path - try { - result = yield client.signIn("yøü@bad.example.org", "nofear"); - do_throw("Expected to catch an exception"); - } catch (expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_signOut() { - let signoutMessage = JSON.stringify({}); - let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"}); - let signedOut = false; - - let server = httpd_setup({ - "/session/destroy": function(request, response) { - if (!signedOut) { - signedOut = true; - do_check_true(request.hasHeader("Authorization")); - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(signoutMessage, signoutMessage.length); - return; - } - - // Error trying to sign out of nonexistent account - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - return; - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result = yield client.signOut("FakeSession"); - do_check_eq(typeof result, "object"); - - // Trigger error path - try { - result = yield client.signOut("FakeSession"); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_recoveryEmailStatus() { - let emailStatus = JSON.stringify({verified: true}); - let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"}); - let tries = 0; - - let server = httpd_setup({ - "/recovery_email/status": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - do_check_eq("", request._queryString); - - if (tries === 0) { - tries += 1; - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(emailStatus, emailStatus.length); - return; - } - - // Second call gets an error trying to query a nonexistent account - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - return; - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN); - do_check_eq(result.verified, true); - - // Trigger error path - try { - result = yield client.recoveryEmailStatus("some bogus session"); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_recoveryEmailStatusWithReason() { - let emailStatus = JSON.stringify({verified: true}); - let server = httpd_setup({ - "/recovery_email/status": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - // if there is a query string then it will have a reason - do_check_eq("reason=push", request._queryString); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(emailStatus, emailStatus.length); - return; - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN, { - reason: "push", - }); - do_check_eq(result.verified, true); - yield deferredStop(server); -}); - -add_task(function* test_resendVerificationEmail() { - let emptyMessage = "{}"; - let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"}); - let tries = 0; - - let server = httpd_setup({ - "/recovery_email/resend_code": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - if (tries === 0) { - tries += 1; - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(emptyMessage, emptyMessage.length); - return; - } - - // Second call gets an error trying to query a nonexistent account - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - return; - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result = yield client.resendVerificationEmail(FAKE_SESSION_TOKEN); - do_check_eq(JSON.stringify(result), emptyMessage); - - // Trigger error path - try { - result = yield client.resendVerificationEmail("some bogus session"); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_accountKeys() { - // Four calls to accountKeys(). The first one should work correctly, and we - // should get a valid bundle back, in exchange for our keyFetch token, from - // which we correctly derive kA and wrapKB. The subsequent three calls - // should all trigger separate error paths. - let responseMessage = JSON.stringify({bundle: ACCOUNT_KEYS.response}); - let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"}); - let emptyMessage = "{}"; - let attempt = 0; - - let server = httpd_setup({ - "/account/keys": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - attempt += 1; - - switch(attempt) { - case 1: - // First time succeeds - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(responseMessage, responseMessage.length); - break; - - case 2: - // Second time, return no bundle to trigger client error - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(emptyMessage, emptyMessage.length); - break; - - case 3: - // Return gibberish to trigger client MAC error - // Tweak a byte - let garbageResponse = JSON.stringify({ - bundle: ACCOUNT_KEYS.response.slice(0, -1) + "1" - }); - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(garbageResponse, garbageResponse.length); - break; - - case 4: - // Trigger error for nonexistent account - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - break; - } - }, - }); - - let client = new FxAccountsClient(server.baseURI); - - // First try, all should be good - let result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch); - do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.kA), result.kA); - do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.wrapKB), result.wrapKB); - - // Second try, empty bundle should trigger error - try { - result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(expectedError.message, "failed to retrieve keys"); - } - - // Third try, bad bundle results in MAC error - try { - result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(expectedError.message, "error unbundling encryption keys"); - } - - // Fourth try, pretend account doesn't exist - try { - result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_signCertificate() { - let certSignMessage = JSON.stringify({cert: {bar: "baz"}}); - let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"}); - let tries = 0; - - let server = httpd_setup({ - "/certificate/sign": function(request, response) { - do_check_true(request.hasHeader("Authorization")); - - if (tries === 0) { - tries += 1; - let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); - let jsonBody = JSON.parse(body); - do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar"); - do_check_eq(jsonBody.duration, 600); - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(certSignMessage, certSignMessage.length); - return; - } - - // Second attempt, trigger error - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(errorMessage, errorMessage.length); - return; - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result = yield client.signCertificate(FAKE_SESSION_TOKEN, JSON.stringify({foo: "bar"}), 600); - do_check_eq("baz", result.bar); - - // Account doesn't exist - try { - result = yield client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600); - do_throw("Expected to catch an exception"); - } catch(expectedError) { - do_check_eq(102, expectedError.errno); - } - - yield deferredStop(server); -}); - -add_task(function* test_accountExists() { - let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN}); - let existsMessage = JSON.stringify({error: "wrong password", code: 400, errno: 103}); - let doesntExistMessage = JSON.stringify({error: "no such account", code: 400, errno: 102}); - let emptyMessage = "{}"; - - let server = httpd_setup({ - "/account/login": function(request, response) { - let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); - let jsonBody = JSON.parse(body); - - switch (jsonBody.email) { - // We'll test that these users' accounts exist - case "i.exist@example.com": - case "i.also.exist@example.com": - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(existsMessage, existsMessage.length); - break; - - // This user's account doesn't exist - case "i.dont.exist@example.com": - response.setStatusLine(request.httpVersion, 400, "Bad request"); - response.bodyOutputStream.write(doesntExistMessage, doesntExistMessage.length); - break; - - // This user throws an unexpected response - // This will reject the client signIn promise - case "i.break.things@example.com": - response.setStatusLine(request.httpVersion, 500, "Alas"); - response.bodyOutputStream.write(emptyMessage, emptyMessage.length); - break; - - default: - throw new Error("Unexpected login from " + jsonBody.email); - break; - } - }, - }); - - let client = new FxAccountsClient(server.baseURI); - let result; - - result = yield client.accountExists("i.exist@example.com"); - do_check_true(result); - - result = yield client.accountExists("i.also.exist@example.com"); - do_check_true(result); - - result = yield client.accountExists("i.dont.exist@example.com"); - do_check_false(result); - - try { - result = yield client.accountExists("i.break.things@example.com"); - do_throw("Expected to catch an exception"); - } catch(unexpectedError) { - do_check_eq(unexpectedError.code, 500); - } - - yield deferredStop(server); -}); - -add_task(function* test_registerDevice() { - const DEVICE_ID = "device id"; - const DEVICE_NAME = "device name"; - const DEVICE_TYPE = "device type"; - const ERROR_NAME = "test that the client promise rejects"; - - const server = httpd_setup({ - "/account/device": function(request, response) { - const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream)); - - if (body.id || !body.name || !body.type || Object.keys(body).length !== 2) { - response.setStatusLine(request.httpVersion, 400, "Invalid request"); - return response.bodyOutputStream.write("{}", 2); - } - - if (body.name === ERROR_NAME) { - response.setStatusLine(request.httpVersion, 500, "Alas"); - return response.bodyOutputStream.write("{}", 2); - } - - body.id = DEVICE_ID; - body.createdAt = Date.now(); - - const responseMessage = JSON.stringify(body); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(responseMessage, responseMessage.length); - }, - }); - - const client = new FxAccountsClient(server.baseURI); - const result = yield client.registerDevice(FAKE_SESSION_TOKEN, DEVICE_NAME, DEVICE_TYPE); - - do_check_true(result); - do_check_eq(Object.keys(result).length, 4); - do_check_eq(result.id, DEVICE_ID); - do_check_eq(typeof result.createdAt, 'number'); - do_check_true(result.createdAt > 0); - do_check_eq(result.name, DEVICE_NAME); - do_check_eq(result.type, DEVICE_TYPE); - - try { - yield client.registerDevice(FAKE_SESSION_TOKEN, ERROR_NAME, DEVICE_TYPE); - do_throw("Expected to catch an exception"); - } catch(unexpectedError) { - do_check_eq(unexpectedError.code, 500); - } - - yield deferredStop(server); -}); - -add_task(function* test_updateDevice() { - const DEVICE_ID = "some other id"; - const DEVICE_NAME = "some other name"; - const ERROR_ID = "test that the client promise rejects"; - - const server = httpd_setup({ - "/account/device": function(request, response) { - const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream)); - - if (!body.id || !body.name || body.type || Object.keys(body).length !== 2) { - response.setStatusLine(request.httpVersion, 400, "Invalid request"); - return response.bodyOutputStream.write("{}", 2); - } - - if (body.id === ERROR_ID) { - response.setStatusLine(request.httpVersion, 500, "Alas"); - return response.bodyOutputStream.write("{}", 2); - } - - const responseMessage = JSON.stringify(body); - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write(responseMessage, responseMessage.length); - }, - }); - - const client = new FxAccountsClient(server.baseURI); - const result = yield client.updateDevice(FAKE_SESSION_TOKEN, DEVICE_ID, DEVICE_NAME); - - do_check_true(result); - do_check_eq(Object.keys(result).length, 2); - do_check_eq(result.id, DEVICE_ID); - do_check_eq(result.name, DEVICE_NAME); - - try { - yield client.updateDevice(FAKE_SESSION_TOKEN, ERROR_ID, DEVICE_NAME); - do_throw("Expected to catch an exception"); - } catch(unexpectedError) { - do_check_eq(unexpectedError.code, 500); - } - - yield deferredStop(server); -}); - -add_task(function* test_signOutAndDestroyDevice() { - const DEVICE_ID = "device id"; - const ERROR_ID = "test that the client promise rejects"; - - const server = httpd_setup({ - "/account/device/destroy": function(request, response) { - const body = JSON.parse(CommonUtils.readBytesFromInputStream(request.bodyInputStream)); - - if (!body.id) { - response.setStatusLine(request.httpVersion, 400, "Invalid request"); - return response.bodyOutputStream.write(emptyMessage, emptyMessage.length); - } - - if (body.id === ERROR_ID) { - response.setStatusLine(request.httpVersion, 500, "Alas"); - return response.bodyOutputStream.write("{}", 2); - } - - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write("{}", 2); - }, - }); - - const client = new FxAccountsClient(server.baseURI); - const result = yield client.signOutAndDestroyDevice(FAKE_SESSION_TOKEN, DEVICE_ID); - - do_check_true(result); - do_check_eq(Object.keys(result).length, 0); - - try { - yield client.signOutAndDestroyDevice(FAKE_SESSION_TOKEN, ERROR_ID); - do_throw("Expected to catch an exception"); - } catch(unexpectedError) { - do_check_eq(unexpectedError.code, 500); - } - - yield deferredStop(server); -}); - -add_task(function* test_getDeviceList() { - let canReturnDevices; - - const server = httpd_setup({ - "/account/devices": function(request, response) { - if (canReturnDevices) { - response.setStatusLine(request.httpVersion, 200, "OK"); - response.bodyOutputStream.write("[]", 2); - } else { - response.setStatusLine(request.httpVersion, 500, "Alas"); - response.bodyOutputStream.write("{}", 2); - } - }, - }); - - const client = new FxAccountsClient(server.baseURI); - - canReturnDevices = true; - const result = yield client.getDeviceList(FAKE_SESSION_TOKEN); - do_check_true(Array.isArray(result)); - do_check_eq(result.length, 0); - - try { - canReturnDevices = false; - yield client.getDeviceList(FAKE_SESSION_TOKEN); - do_throw("Expected to catch an exception"); - } catch(unexpectedError) { - do_check_eq(unexpectedError.code, 500); - } - - yield deferredStop(server); -}); - -add_task(function* test_client_metrics() { - function writeResp(response, msg) { - if (typeof msg === "object") { - msg = JSON.stringify(msg); - } - response.bodyOutputStream.write(msg, msg.length); - } - - let server = httpd_setup( - { - "/session/destroy": function(request, response) { - response.setHeader("Content-Type", "application/json; charset=utf-8"); - response.setStatusLine(request.httpVersion, 401, "Unauthorized"); - writeResp(response, { - error: "invalid authentication timestamp", - code: 401, - errno: 111, - }); - }, - } - ); - - let client = new FxAccountsClient(server.baseURI); - - yield rejects(client.signOut(FAKE_SESSION_TOKEN, { - service: "sync", - }), function(err) { - return err.errno == 111; - }); - - yield deferredStop(server); -}); - -add_task(function* test_email_case() { - let canonicalEmail = "greta.garbo@gmail.com"; - let clientEmail = "Greta.Garbo@gmail.COM"; - let attempts = 0; - - function writeResp(response, msg) { - if (typeof msg === "object") { - msg = JSON.stringify(msg); - } - response.bodyOutputStream.write(msg, msg.length); - } - - let server = httpd_setup( - { - "/account/login": function(request, response) { - response.setHeader("Content-Type", "application/json; charset=utf-8"); - attempts += 1; - if (attempts > 2) { - response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance"); - return writeResp(response, ""); - } - - let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); - let jsonBody = JSON.parse(body); - let email = jsonBody.email; - - // If the client has the wrong case on the email, we return a 400, with - // the capitalization of the email as saved in the accounts database. - if (email == canonicalEmail) { - response.setStatusLine(request.httpVersion, 200, "Yay"); - return writeResp(response, {areWeHappy: "yes"}); - } - - response.setStatusLine(request.httpVersion, 400, "Incorrect email case"); - return writeResp(response, { - code: 400, - errno: 120, - error: "Incorrect email case", - email: canonicalEmail - }); - }, - } - ); - - let client = new FxAccountsClient(server.baseURI); - - let result = yield client.signIn(clientEmail, "123456"); - do_check_eq(result.areWeHappy, "yes"); - do_check_eq(attempts, 2); - - yield deferredStop(server); -}); - -// turn formatted test vectors into normal hex strings -function h(hexStr) { - return hexStr.replace(/\s+/g, ""); -} diff --git a/services/fxaccounts/tests/xpcshell/test_credentials.js b/services/fxaccounts/tests/xpcshell/test_credentials.js deleted file mode 100644 index cbd9e4c7a..000000000 --- a/services/fxaccounts/tests/xpcshell/test_credentials.js +++ /dev/null @@ -1,110 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -Cu.import("resource://gre/modules/Credentials.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-crypto/utils.js"); - -var {hexToBytes: h2b, - hexAsString: h2s, - stringAsHex: s2h, - bytesAsHex: b2h} = CommonUtils; - -// Test vectors for the "onepw" protocol: -// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors -var vectors = { - "client stretch-KDF": { - email: - h("616e6472c3a94065 78616d706c652e6f 7267"), - password: - h("70c3a4737377c3b6 7264"), - quickStretchedPW: - h("e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d"), - authPW: - h("247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375"), - authSalt: - h("00f0000000000000 0000000000000000 0000000000000000 0000000000000000"), - }, -}; - -// A simple test suite with no utf8 encoding madness. -add_task(function* test_onepw_setup_credentials() { - let email = "francine@example.org"; - let password = CommonUtils.encodeUTF8("i like pie"); - - let pbkdf2 = CryptoUtils.pbkdf2Generate; - let hkdf = CryptoUtils.hkdf; - - // quickStretch the email - let saltyEmail = Credentials.keyWordExtended("quickStretch", email); - - do_check_eq(b2h(saltyEmail), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a6672616e63696e65406578616d706c652e6f7267"); - - let pbkdf2Rounds = 1000; - let pbkdf2Len = 32; - - let quickStretchedPW = pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len, Ci.nsICryptoHMAC.SHA256, 32); - let quickStretchedActual = "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4"; - do_check_eq(b2h(quickStretchedPW), quickStretchedActual); - - // obtain hkdf info - let authKeyInfo = Credentials.keyWord('authPW'); - do_check_eq(b2h(authKeyInfo), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f617574685057"); - - // derive auth password - let hkdfSalt = h2b("00"); - let hkdfLen = 32; - let authPW = hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen); - - do_check_eq(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342"); - - // derive unwrap key - let unwrapKeyInfo = Credentials.keyWord('unwrapBkey'); - let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen); - - do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9"); -}); - -add_task(function* test_client_stretch_kdf() { - let pbkdf2 = CryptoUtils.pbkdf2Generate; - let hkdf = CryptoUtils.hkdf; - let expected = vectors["client stretch-KDF"]; - - let email = h2s(expected.email); - let password = h2s(expected.password); - - // Intermediate value from sjcl implementation in fxa-js-client - // The key thing is the c3a9 sequence in "andré" - let salt = Credentials.keyWordExtended("quickStretch", email); - do_check_eq(b2h(salt), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a616e6472c3a9406578616d706c652e6f7267"); - - let options = { - stretchedPassLength: 32, - pbkdf2Rounds: 1000, - hmacAlgorithm: Ci.nsICryptoHMAC.SHA256, - hmacLength: 32, - hkdfSalt: h2b("00"), - hkdfLength: 32, - }; - - let results = yield Credentials.setup(email, password, options); - - do_check_eq(expected.quickStretchedPW, b2h(results.quickStretchedPW), - "quickStretchedPW is wrong"); - - do_check_eq(expected.authPW, b2h(results.authPW), - "authPW is wrong"); -}); - -// End of tests -// Utility functions follow - -function run_test() { - run_next_test(); -} - -// turn formatted test vectors into normal hex strings -function h(hexStr) { - return hexStr.replace(/\s+/g, ""); -} diff --git a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js deleted file mode 100644 index 64ddb1fd1..000000000 --- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js +++ /dev/null @@ -1,214 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -// Tests for FxAccounts, storage and the master password. - -// Stop us hitting the real auth server. -Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost"); -// See verbose logging from FxAccounts.jsm -Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); - -// Use a backstage pass to get at our LoginManagerStorage object, so we can -// mock the prototype. -var {LoginManagerStorage} = Cu.import("resource://gre/modules/FxAccountsStorage.jsm", {}); -var isLoggedIn = true; -LoginManagerStorage.prototype.__defineGetter__("_isLoggedIn", () => isLoggedIn); - -function setLoginMgrLoggedInState(loggedIn) { - isLoggedIn = loggedIn; -} - - -initTestLogging("Trace"); - -function run_test() { - run_next_test(); -} - -function getLoginMgrData() { - let logins = Services.logins.findLogins({}, FXA_PWDMGR_HOST, null, FXA_PWDMGR_REALM); - if (logins.length == 0) { - return null; - } - Assert.equal(logins.length, 1, "only 1 login available"); - return logins[0]; -} - -function createFxAccounts() { - return new FxAccounts({ - _getDeviceName() { - return "mock device name"; - }, - fxaPushService: { - registerPushEndpoint() { - return new Promise((resolve) => { - resolve({ - endpoint: "http://mochi.test:8888" - }); - }); - }, - } - }); -} - -add_task(function* test_simple() { - let fxa = createFxAccounts(); - - let creds = { - uid: "abcd", - email: "test@example.com", - sessionToken: "sessionToken", - kA: "the kA value", - kB: "the kB value", - verified: true - }; - yield fxa.setSignedInUser(creds); - - // This should have stored stuff in both the .json file in the profile - // dir, and the login dir. - let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json"); - let data = yield CommonUtils.readJSON(path); - - Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text"); - Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text"); - Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag"); - - Assert.ok(!("kA" in data.accountData), "kA not stored in clear text"); - Assert.ok(!("kB" in data.accountData), "kB not stored in clear text"); - - let login = getLoginMgrData(); - Assert.strictEqual(login.username, creds.uid, "uid used for username"); - let loginData = JSON.parse(login.password); - Assert.strictEqual(loginData.version, data.version, "same version flag in both places"); - Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr"); - Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr"); - - Assert.ok(!("email" in loginData), "email not stored in the login mgr json"); - Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json"); - Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json"); - - yield fxa.signOut(/* localOnly = */ true); - Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout"); -}); - -add_task(function* test_MPLocked() { - let fxa = createFxAccounts(); - - let creds = { - uid: "abcd", - email: "test@example.com", - sessionToken: "sessionToken", - kA: "the kA value", - kB: "the kB value", - verified: true - }; - - Assert.strictEqual(getLoginMgrData(), null, "no login mgr at the start"); - // tell the storage that the MP is locked. - setLoginMgrLoggedInState(false); - yield fxa.setSignedInUser(creds); - - // This should have stored stuff in the .json, and the login manager stuff - // will not exist. - let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json"); - let data = yield CommonUtils.readJSON(path); - - Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text"); - Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text"); - Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag"); - - Assert.ok(!("kA" in data.accountData), "kA not stored in clear text"); - Assert.ok(!("kB" in data.accountData), "kB not stored in clear text"); - - Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist"); - yield fxa.signOut(/* localOnly = */ true) -}); - - -add_task(function* test_consistentWithMPEdgeCases() { - setLoginMgrLoggedInState(true); - - let fxa = createFxAccounts(); - - let creds1 = { - uid: "uid1", - email: "test@example.com", - sessionToken: "sessionToken", - kA: "the kA value", - kB: "the kB value", - verified: true - }; - - let creds2 = { - uid: "uid2", - email: "test2@example.com", - sessionToken: "sessionToken2", - kA: "the kA value2", - kB: "the kB value2", - verified: false, - }; - - // Log a user in while MP is unlocked. - yield fxa.setSignedInUser(creds1); - - // tell the storage that the MP is locked - this will prevent logout from - // being able to clear the data. - setLoginMgrLoggedInState(false); - - // now set the second credentials. - yield fxa.setSignedInUser(creds2); - - // We should still have creds1 data in the login manager. - let login = getLoginMgrData(); - Assert.strictEqual(login.username, creds1.uid); - // and that we do have the first kA in the login manager. - Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds1.kA, - "stale data still in login mgr"); - - // Make a new FxA instance (otherwise the values in memory will be used) - // and we want the login manager to be unlocked. - setLoginMgrLoggedInState(true); - fxa = createFxAccounts(); - - let accountData = yield fxa.getSignedInUser(); - Assert.strictEqual(accountData.email, creds2.email); - // we should have no kA at all. - Assert.strictEqual(accountData.kA, undefined, "stale kA wasn't used"); - yield fxa.signOut(/* localOnly = */ true) -}); - -// A test for the fact we will accept either a UID or email when looking in -// the login manager. -add_task(function* test_uidMigration() { - setLoginMgrLoggedInState(true); - Assert.strictEqual(getLoginMgrData(), null, "expect no logins at the start"); - - // create the login entry using email as a key. - let contents = {kA: "kA"}; - - let loginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new loginInfo(FXA_PWDMGR_HOST, - null, // aFormSubmitURL, - FXA_PWDMGR_REALM, // aHttpRealm, - "foo@bar.com", // aUsername - JSON.stringify(contents), // aPassword - "", // aUsernameField - "");// aPasswordField - Services.logins.addLogin(login); - - // ensure we read it. - let storage = new LoginManagerStorage(); - let got = yield storage.get("uid", "foo@bar.com"); - Assert.deepEqual(got, contents); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_client.js deleted file mode 100644 index 9bcb1b1ab..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_client.js +++ /dev/null @@ -1,55 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); - -function run_test() { - validationHelper(undefined, - "Error: Missing 'parameters' configuration option"); - - validationHelper({}, - "Error: Missing 'parameters' configuration option"); - - validationHelper({ parameters: {} }, - "Error: Missing 'parameters.oauth_uri' parameter"); - - validationHelper({ parameters: { - oauth_uri: "http://oauth.test/v1" - }}, - "Error: Missing 'parameters.client_id' parameter"); - - validationHelper({ parameters: { - oauth_uri: "http://oauth.test/v1", - client_id: "client_id" - }}, - "Error: Missing 'parameters.content_uri' parameter"); - - validationHelper({ parameters: { - oauth_uri: "http://oauth.test/v1", - client_id: "client_id", - content_uri: "http://content.test" - }}, - "Error: Missing 'parameters.state' parameter"); - - validationHelper({ parameters: { - oauth_uri: "http://oauth.test/v1", - client_id: "client_id", - content_uri: "http://content.test", - state: "complete", - action: "force_auth" - }}, - "Error: parameters.email is required for action 'force_auth'"); - - run_next_test(); -} - -function validationHelper(params, expected) { - try { - new FxAccountsOAuthClient(params); - } catch (e) { - return do_check_eq(e.toString(), expected); - } - throw new Error("Validation helper error"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js deleted file mode 100644 index 710a65ee5..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js +++ /dev/null @@ -1,292 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -const CLIENT_OPTIONS = { - serverURL: "http://127.0.0.1:9010/v1", - client_id: 'abc123' -}; - -const STATUS_SUCCESS = 200; - -/** - * Mock request responder - * @param {String} response - * Mocked raw response from the server - * @returns {Function} - */ -var mockResponse = function (response) { - return function () { - return { - setHeader: function () {}, - post: function () { - this.response = response; - this.onComplete(); - } - }; - }; -}; - -/** - * Mock request error responder - * @param {Error} error - * Error object - * @returns {Function} - */ -var mockResponseError = function (error) { - return function () { - return { - setHeader: function () {}, - post: function () { - this.onComplete(error); - } - }; - }; -}; - -add_test(function missingParams () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - try { - client.getTokenFromAssertion() - } catch (e) { - do_check_eq(e.message, "Missing 'assertion' parameter"); - } - - try { - client.getTokenFromAssertion("assertion") - } catch (e) { - do_check_eq(e.message, "Missing 'scope' parameter"); - } - - run_next_test(); -}); - -add_test(function successfulResponse () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "{\"access_token\":\"http://example.com/image.jpeg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}", - }; - - client._Request = new mockResponse(response); - client.getTokenFromAssertion("assertion", "scope") - .then( - function (result) { - do_check_eq(result.access_token, "http://example.com/image.jpeg"); - run_next_test(); - } - ); -}); - -add_test(function successfulDestroy () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "{}", - }; - - client._Request = new mockResponse(response); - client.destroyToken("deadbeef").then(run_next_test); -}); - -add_test(function parseErrorResponse () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "unexpected", - }; - - client._Request = new mockResponse(response); - client.getTokenFromAssertion("assertion", "scope") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, STATUS_SUCCESS); - do_check_eq(e.errno, ERRNO_PARSE); - do_check_eq(e.error, ERROR_PARSE); - do_check_eq(e.message, "unexpected"); - run_next_test(); - } - ); -}); - -add_test(function serverErrorResponse () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - let response = { - status: 400, - body: "{ \"code\": 400, \"errno\": 104, \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }", - }; - - client._Request = new mockResponse(response); - client.getTokenFromAssertion("blah", "scope") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, 400); - do_check_eq(e.errno, ERRNO_INVALID_FXA_ASSERTION); - do_check_eq(e.error, "Bad Request"); - do_check_eq(e.message, "Unauthorized"); - run_next_test(); - } - ); -}); - -add_test(function networkErrorResponse () { - let client = new FxAccountsOAuthGrantClient({ - serverURL: "http://domain.dummy", - client_id: "abc123" - }); - Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true); - client.getTokenFromAssertion("assertion", "scope") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, null); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - run_next_test(); - } - ).catch(() => {}).then(() => - Services.prefs.clearUserPref("identity.fxaccounts.skipDeviceRegistration")); -}); - -add_test(function unsupportedMethod () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - - return client._createRequest("/", "PUT") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, ERROR_CODE_METHOD_NOT_ALLOWED); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - do_check_eq(e.message, ERROR_MSG_METHOD_NOT_ALLOWED); - run_next_test(); - } - ); -}); - -add_test(function onCompleteRequestError () { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - client._Request = new mockResponseError(new Error("onComplete error")); - client.getTokenFromAssertion("assertion", "scope") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, null); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - do_check_eq(e.message, "Error: onComplete error"); - run_next_test(); - } - ); -}); - -add_test(function incorrectErrno() { - let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS); - let response = { - status: 400, - body: "{ \"code\": 400, \"errno\": \"bad errno\", \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }", - }; - - client._Request = new mockResponse(response); - client.getTokenFromAssertion("blah", "scope") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(e.code, 400); - do_check_eq(e.errno, ERRNO_UNKNOWN_ERROR); - do_check_eq(e.error, "Bad Request"); - do_check_eq(e.message, "Unauthorized"); - run_next_test(); - } - ); -}); - -add_test(function constructorTests() { - validationHelper(undefined, - "Error: Missing configuration options"); - - validationHelper({}, - "Error: Missing 'serverURL' parameter"); - - validationHelper({ serverURL: "http://example.com" }, - "Error: Missing 'client_id' parameter"); - - validationHelper({ client_id: "123ABC" }, - "Error: Missing 'serverURL' parameter"); - - validationHelper({ client_id: "123ABC", serverURL: "badUrl" }, - "Error: Invalid 'serverURL'"); - - run_next_test(); -}); - -add_test(function errorTests() { - let error1 = new FxAccountsOAuthGrantClientError(); - do_check_eq(error1.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(error1.code, null); - do_check_eq(error1.errno, ERRNO_UNKNOWN_ERROR); - do_check_eq(error1.error, ERROR_UNKNOWN); - do_check_eq(error1.message, null); - - let error2 = new FxAccountsOAuthGrantClientError({ - code: STATUS_SUCCESS, - errno: 1, - error: "Error", - message: "Something", - }); - let fields2 = error2._toStringFields(); - let statusCode = 1; - - do_check_eq(error2.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(error2.code, STATUS_SUCCESS); - do_check_eq(error2.errno, statusCode); - do_check_eq(error2.error, "Error"); - do_check_eq(error2.message, "Something"); - - do_check_eq(fields2.name, "FxAccountsOAuthGrantClientError"); - do_check_eq(fields2.code, STATUS_SUCCESS); - do_check_eq(fields2.errno, statusCode); - do_check_eq(fields2.error, "Error"); - do_check_eq(fields2.message, "Something"); - - do_check_true(error2.toString().indexOf("Something") >= 0); - run_next_test(); -}); - -function run_test() { - run_next_test(); -} - -/** - * Quick way to test the "FxAccountsOAuthGrantClient" constructor. - * - * @param {Object} options - * FxAccountsOAuthGrantClient constructor options - * @param {String} expected - * Expected error message - * @returns {*} - */ -function validationHelper(options, expected) { - try { - new FxAccountsOAuthGrantClient(options); - } catch (e) { - return do_check_eq(e.toString(), expected); - } - throw new Error("Validation helper error"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js deleted file mode 100644 index bd446513e..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client_server.js +++ /dev/null @@ -1,73 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -// A test of FxAccountsOAuthGrantClient but using a real server it can -// hit. -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); - -// handlers for our server. -var numTokenFetches; -var activeTokens; - -function authorize(request, response) { - response.setStatusLine("1.1", 200, "OK"); - let token = "token" + numTokenFetches; - numTokenFetches += 1; - activeTokens.add(token); - response.write(JSON.stringify({access_token: token})); -} - -function destroy(request, response) { - // Getting the body seems harder than it should be! - let sis = Cc["@mozilla.org/scriptableinputstream;1"] - .createInstance(Ci.nsIScriptableInputStream); - sis.init(request.bodyInputStream); - let body = JSON.parse(sis.read(sis.available())); - sis.close(); - let token = body.token; - ok(activeTokens.delete(token)); - print("after destroy have", activeTokens.size, "tokens left.") - response.setStatusLine("1.1", 200, "OK"); - response.write('{}'); -} - -function startServer() { - numTokenFetches = 0; - activeTokens = new Set(); - let srv = new HttpServer(); - srv.registerPathHandler("/v1/authorization", authorize); - srv.registerPathHandler("/v1/destroy", destroy); - srv.start(-1); - return srv; -} - -function promiseStopServer(server) { - return new Promise(resolve => { - server.stop(resolve); - }); -} - -add_task(function* getAndRevokeToken () { - let server = startServer(); - let clientOptions = { - serverURL: "http://localhost:" + server.identity.primaryPort + "/v1", - client_id: 'abc123', - } - - let client = new FxAccountsOAuthGrantClient(clientOptions); - let result = yield client.getTokenFromAssertion("assertion", "scope"); - equal(result.access_token, "token0"); - equal(numTokenFetches, 1, "we hit the server to fetch a token"); - yield client.destroyToken("token0"); - equal(activeTokens.size, 0, "We hit the server to revoke it"); - yield promiseStopServer(server); -}); - -// XXX - TODO - we should probably add more tests for unexpected responses etc. - -function run_test() { - run_next_test(); -} diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js deleted file mode 100644 index 08642846b..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js +++ /dev/null @@ -1,165 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/osfile.jsm"); - -// We grab some additional stuff via backstage passes. -var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {}); - -function promiseNotification(topic) { - return new Promise(resolve => { - let observe = () => { - Services.obs.removeObserver(observe, topic); - resolve(); - } - Services.obs.addObserver(observe, topic, false); - }); -} - -// A storage manager that doesn't actually write anywhere. -function MockStorageManager() { -} - -MockStorageManager.prototype = { - promiseInitialized: Promise.resolve(), - - initialize(accountData) { - this.accountData = accountData; - }, - - finalize() { - return Promise.resolve(); - }, - - getAccountData() { - return Promise.resolve(this.accountData); - }, - - updateAccountData(updatedFields) { - for (let [name, value] of Object.entries(updatedFields)) { - if (value == null) { - delete this.accountData[name]; - } else { - this.accountData[name] = value; - } - } - return Promise.resolve(); - }, - - deleteAccountData() { - this.accountData = null; - return Promise.resolve(); - } -} - - -// Just enough mocks so we can avoid hawk etc. -function MockFxAccountsClient() { - this._email = "nobody@example.com"; - this._verified = false; - - this.accountStatus = function(uid) { - let deferred = Promise.defer(); - deferred.resolve(!!uid && (!this._deletedOnServer)); - return deferred.promise; - }; - - this.signOut = function() { return Promise.resolve(); }; - this.registerDevice = function() { return Promise.resolve(); }; - this.updateDevice = function() { return Promise.resolve(); }; - this.signOutAndDestroyDevice = function() { return Promise.resolve(); }; - this.getDeviceList = function() { return Promise.resolve(); }; - - FxAccountsClient.apply(this); -} - -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -} - -function MockFxAccounts(device={}) { - return new FxAccounts({ - fxAccountsClient: new MockFxAccountsClient(), - newAccountState(credentials) { - // we use a real accountState but mocked storage. - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - _getDeviceName() { - return "mock device name"; - }, - fxaPushService: { - registerPushEndpoint() { - return new Promise((resolve) => { - resolve({ - endpoint: "http://mochi.test:8888" - }); - }); - }, - }, - }); -} - -function* createMockFxA() { - let fxa = new MockFxAccounts(); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - yield fxa.setSignedInUser(credentials); - return fxa; -} - -// The tests. -function run_test() { - run_next_test(); -} - -add_task(function* testCacheStorage() { - let fxa = yield createMockFxA(); - - // Hook what the impl calls to save to disk. - let cas = fxa.internal.currentAccountState; - let origPersistCached = cas._persistCachedTokens.bind(cas) - cas._persistCachedTokens = function() { - return origPersistCached().then(() => { - Services.obs.notifyObservers(null, "testhelper-fxa-cache-persist-done", null); - }); - }; - - let promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done"); - let tokenData = {token: "token1", somethingelse: "something else"}; - let scopeArray = ["foo", "bar"]; - cas.setCachedToken(scopeArray, tokenData); - deepEqual(cas.getCachedToken(scopeArray), tokenData); - - deepEqual(cas.oauthTokens, {"bar|foo": tokenData}); - // wait for background write to complete. - yield promiseWritten; - - // Check the token cache made it to our mocked storage. - deepEqual(cas.storageManager.accountData.oauthTokens, {"bar|foo": tokenData}); - - // Drop the token from the cache and ensure it is removed from the json. - promiseWritten = promiseNotification("testhelper-fxa-cache-persist-done"); - yield cas.removeCachedToken("token1"); - deepEqual(cas.oauthTokens, {}); - yield promiseWritten; - deepEqual(cas.storageManager.accountData.oauthTokens, {}); - - // sign out and the token storage should end up with null. - let storageManager = cas.storageManager; // .signOut() removes the attribute. - yield fxa.signOut( /* localOnly = */ true); - deepEqual(storageManager.accountData, null); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js deleted file mode 100644 index f758bf405..000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js +++ /dev/null @@ -1,251 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); -Cu.import("resource://services-common/utils.js"); -var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {}); - -function promiseNotification(topic) { - return new Promise(resolve => { - let observe = () => { - Services.obs.removeObserver(observe, topic); - resolve(); - } - Services.obs.addObserver(observe, topic, false); - }); -} - -// Just enough mocks so we can avoid hawk and storage. -function MockStorageManager() { -} - -MockStorageManager.prototype = { - promiseInitialized: Promise.resolve(), - - initialize(accountData) { - this.accountData = accountData; - }, - - finalize() { - return Promise.resolve(); - }, - - getAccountData() { - return Promise.resolve(this.accountData); - }, - - updateAccountData(updatedFields) { - for (let [name, value] of Object.entries(updatedFields)) { - if (value == null) { - delete this.accountData[name]; - } else { - this.accountData[name] = value; - } - } - return Promise.resolve(); - }, - - deleteAccountData() { - this.accountData = null; - return Promise.resolve(); - } -} - -function MockFxAccountsClient() { - this._email = "nobody@example.com"; - this._verified = false; - - this.accountStatus = function(uid) { - let deferred = Promise.defer(); - deferred.resolve(!!uid && (!this._deletedOnServer)); - return deferred.promise; - }; - - this.signOut = function() { return Promise.resolve(); }; - this.registerDevice = function() { return Promise.resolve(); }; - this.updateDevice = function() { return Promise.resolve(); }; - this.signOutAndDestroyDevice = function() { return Promise.resolve(); }; - this.getDeviceList = function() { return Promise.resolve(); }; - - FxAccountsClient.apply(this); -} - -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -} - -function MockFxAccounts(mockGrantClient) { - return new FxAccounts({ - fxAccountsClient: new MockFxAccountsClient(), - getAssertion: () => Promise.resolve("assertion"), - newAccountState(credentials) { - // we use a real accountState but mocked storage. - let storage = new MockStorageManager(); - storage.initialize(credentials); - return new AccountState(storage); - }, - _destroyOAuthToken: function(tokenData) { - // somewhat sad duplication of _destroyOAuthToken, but hard to avoid. - return mockGrantClient.destroyToken(tokenData.token).then( () => { - Services.obs.notifyObservers(null, "testhelper-fxa-revoke-complete", null); - }); - }, - _getDeviceName() { - return "mock device name"; - }, - fxaPushService: { - registerPushEndpoint() { - return new Promise((resolve) => { - resolve({ - endpoint: "http://mochi.test:8888" - }); - }); - }, - }, - }); -} - -function* createMockFxA(mockGrantClient) { - let fxa = new MockFxAccounts(mockGrantClient); - let credentials = { - email: "foo@example.com", - uid: "1234@lcip.org", - assertion: "foobar", - sessionToken: "dead", - kA: "beef", - kB: "cafe", - verified: true - }; - - yield fxa.setSignedInUser(credentials); - return fxa; -} - -// The tests. -function run_test() { - run_next_test(); -} - -function MockFxAccountsOAuthGrantClient() { - this.activeTokens = new Set(); -} - -MockFxAccountsOAuthGrantClient.prototype = { - serverURL: {href: "http://localhost"}, - getTokenFromAssertion(assertion, scope) { - let token = "token" + this.numTokenFetches; - this.numTokenFetches += 1; - this.activeTokens.add(token); - print("getTokenFromAssertion returning token", token); - return Promise.resolve({access_token: token}); - }, - destroyToken(token) { - ok(this.activeTokens.delete(token)); - print("after destroy have", this.activeTokens.size, "tokens left."); - return Promise.resolve({}); - }, - // and some stuff used only for tests. - numTokenFetches: 0, - activeTokens: null, -} - -add_task(function* testRevoke() { - let client = new MockFxAccountsOAuthGrantClient(); - let tokenOptions = { scope: "test-scope", client: client }; - let fxa = yield createMockFxA(client); - - // get our first token and check we hit the mock. - let token1 = yield fxa.getOAuthToken(tokenOptions); - equal(client.numTokenFetches, 1); - equal(client.activeTokens.size, 1); - ok(token1, "got a token"); - equal(token1, "token0"); - - // drop the new token from our cache. - yield fxa.removeCachedOAuthToken({token: token1}); - - // FxA fires an observer when the "background" revoke is complete. - yield promiseNotification("testhelper-fxa-revoke-complete"); - // the revoke should have been successful. - equal(client.activeTokens.size, 0); - // fetching it again hits the server. - let token2 = yield fxa.getOAuthToken(tokenOptions); - equal(client.numTokenFetches, 2); - equal(client.activeTokens.size, 1); - ok(token2, "got a token"); - notEqual(token1, token2, "got a different token"); -}); - -add_task(function* testSignOutDestroysTokens() { - let client = new MockFxAccountsOAuthGrantClient(); - let fxa = yield createMockFxA(client); - - // get our first token and check we hit the mock. - let token1 = yield fxa.getOAuthToken({ scope: "test-scope", client: client }); - equal(client.numTokenFetches, 1); - equal(client.activeTokens.size, 1); - ok(token1, "got a token"); - - // get another - let token2 = yield fxa.getOAuthToken({ scope: "test-scope-2", client: client }); - equal(client.numTokenFetches, 2); - equal(client.activeTokens.size, 2); - ok(token2, "got a token"); - notEqual(token1, token2, "got a different token"); - - // now sign out - they should be removed. - yield fxa.signOut(); - // FxA fires an observer when the "background" signout is complete. - yield promiseNotification("testhelper-fxa-signout-complete"); - // No active tokens left. - equal(client.activeTokens.size, 0); -}); - -add_task(function* testTokenRaces() { - // Here we do 2 concurrent fetches each for 2 different token scopes (ie, - // 4 token fetches in total). - // This should provoke a potential race in the token fetching but we should - // handle and detect that leaving us with one of the fetch tokens being - // revoked and the same token value returned to both calls. - let client = new MockFxAccountsOAuthGrantClient(); - let fxa = yield createMockFxA(client); - - // We should see 2 notifications as part of this - set up the listeners - // now (and wait on them later) - let notifications = Promise.all([ - promiseNotification("testhelper-fxa-revoke-complete"), - promiseNotification("testhelper-fxa-revoke-complete"), - ]); - let results = yield Promise.all([ - fxa.getOAuthToken({scope: "test-scope", client: client}), - fxa.getOAuthToken({scope: "test-scope", client: client}), - fxa.getOAuthToken({scope: "test-scope-2", client: client}), - fxa.getOAuthToken({scope: "test-scope-2", client: client}), - ]); - - equal(client.numTokenFetches, 4, "should have fetched 4 tokens."); - // We should see 2 of the 4 revoked due to the race. - yield notifications; - - // Should have 2 unique tokens - results.sort(); - equal(results[0], results[1]); - equal(results[2], results[3]); - // should be 2 active. - equal(client.activeTokens.size, 2); - // Which can each be revoked. - notifications = Promise.all([ - promiseNotification("testhelper-fxa-revoke-complete"), - promiseNotification("testhelper-fxa-revoke-complete"), - ]); - yield fxa.removeCachedOAuthToken({token: results[0]}); - equal(client.activeTokens.size, 1); - yield fxa.removeCachedOAuthToken({token: results[2]}); - equal(client.activeTokens.size, 0); - yield notifications; -}); diff --git a/services/fxaccounts/tests/xpcshell/test_profile.js b/services/fxaccounts/tests/xpcshell/test_profile.js deleted file mode 100644 index 13adf8cbb..000000000 --- a/services/fxaccounts/tests/xpcshell/test_profile.js +++ /dev/null @@ -1,409 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsProfile.jsm"); - -const URL_STRING = "https://example.com"; -Services.prefs.setCharPref("identity.fxaccounts.settings.uri", "https://example.com/settings"); - -const STATUS_SUCCESS = 200; - -/** - * Mock request responder - * @param {String} response - * Mocked raw response from the server - * @returns {Function} - */ -var mockResponse = function (response) { - let Request = function (requestUri) { - // Store the request uri so tests can inspect it - Request._requestUri = requestUri; - return { - setHeader: function () {}, - head: function () { - this.response = response; - this.onComplete(); - } - }; - }; - - return Request; -}; - -/** - * Mock request error responder - * @param {Error} error - * Error object - * @returns {Function} - */ -var mockResponseError = function (error) { - return function () { - return { - setHeader: function () {}, - head: function () { - this.onComplete(error); - } - }; - }; -}; - -var mockClient = function (fxa) { - let options = { - serverURL: "http://127.0.0.1:1111/v1", - fxa: fxa, - } - return new FxAccountsProfileClient(options); -}; - -const ACCOUNT_DATA = { - uid: "abc123" -}; - -function FxaMock() { -} -FxaMock.prototype = { - currentAccountState: { - profile: null, - get isCurrent() { - return true; - } - }, - - getSignedInUser: function () { - return Promise.resolve(ACCOUNT_DATA); - } -}; - -var mockFxa = function() { - return new FxaMock(); -}; - -function CreateFxAccountsProfile(fxa = null, client = null) { - if (!fxa) { - fxa = mockFxa(); - } - let options = { - fxa: fxa, - profileServerUrl: "http://127.0.0.1:1111/v1" - } - if (client) { - options.profileClient = client; - } - return new FxAccountsProfile(options); -} - -add_test(function getCachedProfile() { - let profile = CreateFxAccountsProfile(); - // a little pointless until bug 1157529 is fixed... - profile._cachedProfile = { avatar: "myurl" }; - - return profile._getCachedProfile() - .then(function (cached) { - do_check_eq(cached.avatar, "myurl"); - run_next_test(); - }); -}); - -add_test(function cacheProfile_change() { - let fxa = mockFxa(); -/* Saving profile data disabled - bug 1157529 - let setUserAccountDataCalled = false; - fxa.setUserAccountData = function (data) { - setUserAccountDataCalled = true; - do_check_eq(data.profile.avatar, "myurl"); - return Promise.resolve(); - }; -*/ - let profile = CreateFxAccountsProfile(fxa); - - makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { - do_check_eq(data, ACCOUNT_DATA.uid); -// do_check_true(setUserAccountDataCalled); - bug 1157529 - run_next_test(); - }); - - return profile._cacheProfile({ avatar: "myurl" }); -}); - -add_test(function cacheProfile_no_change() { - let fxa = mockFxa(); - let profile = CreateFxAccountsProfile(fxa) - profile._cachedProfile = { avatar: "myurl" }; -// XXX - saving is disabled (but we can leave that in for now as we are -// just checking it is *not* called) - fxa.setSignedInUser = function (data) { - throw new Error("should not update account data"); - }; - - return profile._cacheProfile({ avatar: "myurl" }) - .then((result) => { - do_check_false(!!result); - run_next_test(); - }); -}); - -add_test(function fetchAndCacheProfile_ok() { - let client = mockClient(mockFxa()); - client.fetchProfile = function () { - return Promise.resolve({ avatar: "myimg"}); - }; - let profile = CreateFxAccountsProfile(null, client); - - profile._cacheProfile = function (toCache) { - do_check_eq(toCache.avatar, "myimg"); - return Promise.resolve(); - }; - - return profile._fetchAndCacheProfile() - .then(result => { - do_check_eq(result.avatar, "myimg"); - run_next_test(); - }); -}); - -// Check that a second profile request when one is already in-flight reuses -// the in-flight one. -add_task(function* fetchAndCacheProfileOnce() { - // A promise that remains unresolved while we fire off 2 requests for - // a profile. - let resolveProfile; - let promiseProfile = new Promise(resolve => { - resolveProfile = resolve; - }); - let numFetches = 0; - let client = mockClient(mockFxa()); - client.fetchProfile = function () { - numFetches += 1; - return promiseProfile; - }; - let profile = CreateFxAccountsProfile(null, client); - - let request1 = profile._fetchAndCacheProfile(); - let request2 = profile._fetchAndCacheProfile(); - - // should be one request made to fetch the profile (but the promise returned - // by it remains unresolved) - do_check_eq(numFetches, 1); - - // resolve the promise. - resolveProfile({ avatar: "myimg"}); - - // both requests should complete with the same data. - let got1 = yield request1; - do_check_eq(got1.avatar, "myimg"); - let got2 = yield request1; - do_check_eq(got2.avatar, "myimg"); - - // and still only 1 request was made. - do_check_eq(numFetches, 1); -}); - -// Check that sharing a single fetch promise works correctly when the promise -// is rejected. -add_task(function* fetchAndCacheProfileOnce() { - // A promise that remains unresolved while we fire off 2 requests for - // a profile. - let rejectProfile; - let promiseProfile = new Promise((resolve,reject) => { - rejectProfile = reject; - }); - let numFetches = 0; - let client = mockClient(mockFxa()); - client.fetchProfile = function () { - numFetches += 1; - return promiseProfile; - }; - let profile = CreateFxAccountsProfile(null, client); - - let request1 = profile._fetchAndCacheProfile(); - let request2 = profile._fetchAndCacheProfile(); - - // should be one request made to fetch the profile (but the promise returned - // by it remains unresolved) - do_check_eq(numFetches, 1); - - // reject the promise. - rejectProfile("oh noes"); - - // both requests should reject. - try { - yield request1; - throw new Error("should have rejected"); - } catch (ex) { - if (ex != "oh noes") { - throw ex; - } - } - try { - yield request2; - throw new Error("should have rejected"); - } catch (ex) { - if (ex != "oh noes") { - throw ex; - } - } - - // but a new request should work. - client.fetchProfile = function () { - return Promise.resolve({ avatar: "myimg"}); - }; - - let got = yield profile._fetchAndCacheProfile(); - do_check_eq(got.avatar, "myimg"); -}); - -// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the -// last one doesn't kick off a new request to check the cached copy is fresh. -add_task(function* fetchAndCacheProfileAfterThreshold() { - let numFetches = 0; - let client = mockClient(mockFxa()); - client.fetchProfile = function () { - numFetches += 1; - return Promise.resolve({ avatar: "myimg"}); - }; - let profile = CreateFxAccountsProfile(null, client); - profile.PROFILE_FRESHNESS_THRESHOLD = 1000; - - yield profile.getProfile(); - do_check_eq(numFetches, 1); - - yield profile.getProfile(); - do_check_eq(numFetches, 1); - - yield new Promise(resolve => { - do_timeout(1000, resolve); - }); - - yield profile.getProfile(); - do_check_eq(numFetches, 2); -}); - -// Check that a new profile request within PROFILE_FRESHNESS_THRESHOLD of the -// last one *does* kick off a new request if ON_PROFILE_CHANGE_NOTIFICATION -// is sent. -add_task(function* fetchAndCacheProfileBeforeThresholdOnNotification() { - let numFetches = 0; - let client = mockClient(mockFxa()); - client.fetchProfile = function () { - numFetches += 1; - return Promise.resolve({ avatar: "myimg"}); - }; - let profile = CreateFxAccountsProfile(null, client); - profile.PROFILE_FRESHNESS_THRESHOLD = 1000; - - yield profile.getProfile(); - do_check_eq(numFetches, 1); - - Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, null); - - yield profile.getProfile(); - do_check_eq(numFetches, 2); -}); - -add_test(function tearDown_ok() { - let profile = CreateFxAccountsProfile(); - - do_check_true(!!profile.client); - do_check_true(!!profile.fxa); - - profile.tearDown(); - do_check_null(profile.fxa); - do_check_null(profile.client); - - run_next_test(); -}); - -add_test(function getProfile_ok() { - let cachedUrl = "myurl"; - let didFetch = false; - - let profile = CreateFxAccountsProfile(); - profile._getCachedProfile = function () { - return Promise.resolve({ avatar: cachedUrl }); - }; - - profile._fetchAndCacheProfile = function () { - didFetch = true; - return Promise.resolve(); - }; - - return profile.getProfile() - .then(result => { - do_check_eq(result.avatar, cachedUrl); - do_check_true(didFetch); - run_next_test(); - }); -}); - -add_test(function getProfile_no_cache() { - let fetchedUrl = "newUrl"; - let profile = CreateFxAccountsProfile(); - profile._getCachedProfile = function () { - return Promise.resolve(); - }; - - profile._fetchAndCacheProfile = function () { - return Promise.resolve({ avatar: fetchedUrl }); - }; - - return profile.getProfile() - .then(result => { - do_check_eq(result.avatar, fetchedUrl); - run_next_test(); - }); -}); - -add_test(function getProfile_has_cached_fetch_deleted() { - let cachedUrl = "myurl"; - - let fxa = mockFxa(); - let client = mockClient(fxa); - client.fetchProfile = function () { - return Promise.resolve({ avatar: null }); - }; - - let profile = CreateFxAccountsProfile(fxa, client); - profile._cachedProfile = { avatar: cachedUrl }; - -// instead of checking this in a mocked "save" function, just check after the -// observer - makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { - profile.getProfile() - .then(profileData => { - do_check_null(profileData.avatar); - run_next_test(); - }); - }); - - return profile.getProfile() - .then(result => { - do_check_eq(result.avatar, "myurl"); - }); -}); - -function run_test() { - run_next_test(); -} - -function makeObserver(aObserveTopic, aObserveFunc) { - let callback = function (aSubject, aTopic, aData) { - log.debug("observed " + aTopic + " " + aData); - if (aTopic == aObserveTopic) { - removeMe(); - aObserveFunc(aSubject, aTopic, aData); - } - }; - - function removeMe() { - log.debug("removing observer for " + aObserveTopic); - Services.obs.removeObserver(callback, aObserveTopic); - } - - Services.obs.addObserver(callback, aObserveTopic, false); - return removeMe; -} diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js deleted file mode 100644 index 20ff6efc6..000000000 --- a/services/fxaccounts/tests/xpcshell/test_profile_client.js +++ /dev/null @@ -1,411 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsProfileClient.jsm"); - -const STATUS_SUCCESS = 200; - -/** - * Mock request responder - * @param {String} response - * Mocked raw response from the server - * @returns {Function} - */ -var mockResponse = function (response) { - let Request = function (requestUri) { - // Store the request uri so tests can inspect it - Request._requestUri = requestUri; - return { - setHeader: function () {}, - get: function () { - this.response = response; - this.onComplete(); - } - }; - }; - - return Request; -}; - -// A simple mock FxA that hands out tokens without checking them and doesn't -// expect tokens to be revoked. We have specific token tests further down that -// has more checks here. -var mockFxa = { - getOAuthToken(options) { - do_check_eq(options.scope, "profile"); - return "token"; - } -} - -const PROFILE_OPTIONS = { - serverURL: "http://127.0.0.1:1111/v1", - fxa: mockFxa, -}; - -/** - * Mock request error responder - * @param {Error} error - * Error object - * @returns {Function} - */ -var mockResponseError = function (error) { - return function () { - return { - setHeader: function () {}, - get: function () { - this.onComplete(error); - } - }; - }; -}; - -add_test(function successfulResponse () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "{\"email\":\"someone@restmail.net\",\"uid\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}", - }; - - client._Request = new mockResponse(response); - client.fetchProfile() - .then( - function (result) { - do_check_eq(client._Request._requestUri, "http://127.0.0.1:1111/v1/profile"); - do_check_eq(result.email, "someone@restmail.net"); - do_check_eq(result.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); - run_next_test(); - } - ); -}); - -add_test(function parseErrorResponse () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "unexpected", - }; - - client._Request = new mockResponse(response); - client.fetchProfile() - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, STATUS_SUCCESS); - do_check_eq(e.errno, ERRNO_PARSE); - do_check_eq(e.error, ERROR_PARSE); - do_check_eq(e.message, "unexpected"); - run_next_test(); - } - ); -}); - -add_test(function serverErrorResponse () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - let response = { - status: 500, - body: "{ \"code\": 500, \"errno\": 100, \"error\": \"Bad Request\", \"message\": \"Something went wrong\", \"reason\": \"Because the internet\" }", - }; - - client._Request = new mockResponse(response); - client.fetchProfile() - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, 500); - do_check_eq(e.errno, 100); - do_check_eq(e.error, "Bad Request"); - do_check_eq(e.message, "Something went wrong"); - run_next_test(); - } - ); -}); - -// Test that we get a token, then if we get a 401 we revoke it, get a new one -// and retry. -add_test(function server401ResponseThenSuccess () { - // The last token we handed out. - let lastToken = -1; - // The number of times our removeCachedOAuthToken function was called. - let numTokensRemoved = 0; - - let mockFxa = { - getOAuthToken(options) { - do_check_eq(options.scope, "profile"); - return "" + ++lastToken; // tokens are strings. - }, - removeCachedOAuthToken(options) { - // This test never has more than 1 token alive at once, so the token - // being revoked must always be the last token we handed out. - do_check_eq(parseInt(options.token), lastToken); - ++numTokensRemoved; - } - } - let profileOptions = { - serverURL: "http://127.0.0.1:1111/v1", - fxa: mockFxa, - }; - let client = new FxAccountsProfileClient(profileOptions); - - // 2 responses - first one implying the token has expired, second works. - let responses = [ - { - status: 401, - body: "{ \"code\": 401, \"errno\": 100, \"error\": \"Token expired\", \"message\": \"That token is too old\", \"reason\": \"Because security\" }", - }, - { - success: true, - status: STATUS_SUCCESS, - body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}", - }, - ]; - - let numRequests = 0; - let numAuthHeaders = 0; - // Like mockResponse but we want access to headers etc. - client._Request = function(requestUri) { - return { - setHeader: function (name, value) { - if (name == "Authorization") { - numAuthHeaders++; - do_check_eq(value, "Bearer " + lastToken); - } - }, - get: function () { - this.response = responses[numRequests]; - ++numRequests; - this.onComplete(); - } - }; - } - - client.fetchProfile() - .then(result => { - do_check_eq(result.avatar, "http://example.com/image.jpg"); - do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a"); - // should have been exactly 2 requests and exactly 2 auth headers. - do_check_eq(numRequests, 2); - do_check_eq(numAuthHeaders, 2); - // and we should have seen one token revoked. - do_check_eq(numTokensRemoved, 1); - - run_next_test(); - } - ); -}); - -// Test that we get a token, then if we get a 401 we revoke it, get a new one -// and retry - but we *still* get a 401 on the retry, so the caller sees that. -add_test(function server401ResponsePersists () { - // The last token we handed out. - let lastToken = -1; - // The number of times our removeCachedOAuthToken function was called. - let numTokensRemoved = 0; - - let mockFxa = { - getOAuthToken(options) { - do_check_eq(options.scope, "profile"); - return "" + ++lastToken; // tokens are strings. - }, - removeCachedOAuthToken(options) { - // This test never has more than 1 token alive at once, so the token - // being revoked must always be the last token we handed out. - do_check_eq(parseInt(options.token), lastToken); - ++numTokensRemoved; - } - } - let profileOptions = { - serverURL: "http://127.0.0.1:1111/v1", - fxa: mockFxa, - }; - let client = new FxAccountsProfileClient(profileOptions); - - let response = { - status: 401, - body: "{ \"code\": 401, \"errno\": 100, \"error\": \"It's not your token, it's you!\", \"message\": \"I don't like you\", \"reason\": \"Because security\" }", - }; - - let numRequests = 0; - let numAuthHeaders = 0; - client._Request = function(requestUri) { - return { - setHeader: function (name, value) { - if (name == "Authorization") { - numAuthHeaders++; - do_check_eq(value, "Bearer " + lastToken); - } - }, - get: function () { - this.response = response; - ++numRequests; - this.onComplete(); - } - }; - } - - client.fetchProfile().then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, 401); - do_check_eq(e.errno, 100); - do_check_eq(e.error, "It's not your token, it's you!"); - // should have been exactly 2 requests and exactly 2 auth headers. - do_check_eq(numRequests, 2); - do_check_eq(numAuthHeaders, 2); - // and we should have seen both tokens revoked. - do_check_eq(numTokensRemoved, 2); - run_next_test(); - } - ); -}); - -add_test(function networkErrorResponse () { - let client = new FxAccountsProfileClient({ - serverURL: "http://domain.dummy", - fxa: mockFxa, - }); - client.fetchProfile() - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, null); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - run_next_test(); - } - ); -}); - -add_test(function unsupportedMethod () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - - return client._createRequest("/profile", "PUT") - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, ERROR_CODE_METHOD_NOT_ALLOWED); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - do_check_eq(e.message, ERROR_MSG_METHOD_NOT_ALLOWED); - run_next_test(); - } - ); -}); - -add_test(function onCompleteRequestError () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - client._Request = new mockResponseError(new Error("onComplete error")); - client.fetchProfile() - .then( - null, - function (e) { - do_check_eq(e.name, "FxAccountsProfileClientError"); - do_check_eq(e.code, null); - do_check_eq(e.errno, ERRNO_NETWORK); - do_check_eq(e.error, ERROR_NETWORK); - do_check_eq(e.message, "Error: onComplete error"); - run_next_test(); - } - ); -}); - -add_test(function fetchProfileImage_successfulResponse () { - let client = new FxAccountsProfileClient(PROFILE_OPTIONS); - let response = { - success: true, - status: STATUS_SUCCESS, - body: "{\"avatar\":\"http://example.com/image.jpg\",\"id\":\"0d5c1a89b8c54580b8e3e8adadae864a\"}", - }; - - client._Request = new mockResponse(response); - client.fetchProfileImage() - .then( - function (result) { - do_check_eq(client._Request._requestUri, "http://127.0.0.1:1111/v1/avatar"); - do_check_eq(result.avatar, "http://example.com/image.jpg"); - do_check_eq(result.id, "0d5c1a89b8c54580b8e3e8adadae864a"); - run_next_test(); - } - ); -}); - -add_test(function constructorTests() { - validationHelper(undefined, - "Error: Missing 'serverURL' configuration option"); - - validationHelper({}, - "Error: Missing 'serverURL' configuration option"); - - validationHelper({ serverURL: "badUrl" }, - "Error: Invalid 'serverURL'"); - - run_next_test(); -}); - -add_test(function errorTests() { - let error1 = new FxAccountsProfileClientError(); - do_check_eq(error1.name, "FxAccountsProfileClientError"); - do_check_eq(error1.code, null); - do_check_eq(error1.errno, ERRNO_UNKNOWN_ERROR); - do_check_eq(error1.error, ERROR_UNKNOWN); - do_check_eq(error1.message, null); - - let error2 = new FxAccountsProfileClientError({ - code: STATUS_SUCCESS, - errno: 1, - error: "Error", - message: "Something", - }); - let fields2 = error2._toStringFields(); - let statusCode = 1; - - do_check_eq(error2.name, "FxAccountsProfileClientError"); - do_check_eq(error2.code, STATUS_SUCCESS); - do_check_eq(error2.errno, statusCode); - do_check_eq(error2.error, "Error"); - do_check_eq(error2.message, "Something"); - - do_check_eq(fields2.name, "FxAccountsProfileClientError"); - do_check_eq(fields2.code, STATUS_SUCCESS); - do_check_eq(fields2.errno, statusCode); - do_check_eq(fields2.error, "Error"); - do_check_eq(fields2.message, "Something"); - - do_check_true(error2.toString().indexOf("Something") >= 0); - run_next_test(); -}); - -function run_test() { - run_next_test(); -} - -/** - * Quick way to test the "FxAccountsProfileClient" constructor. - * - * @param {Object} options - * FxAccountsProfileClient constructor options - * @param {String} expected - * Expected error message - * @returns {*} - */ -function validationHelper(options, expected) { - // add fxa to options - that missing isn't what we are testing here. - if (options) { - options.fxa = mockFxa; - } - try { - new FxAccountsProfileClient(options); - } catch (e) { - return do_check_eq(e.toString(), expected); - } - throw new Error("Validation helper error"); -} diff --git a/services/fxaccounts/tests/xpcshell/test_push_service.js b/services/fxaccounts/tests/xpcshell/test_push_service.js deleted file mode 100644 index 8d66f6fa8..000000000 --- a/services/fxaccounts/tests/xpcshell/test_push_service.js +++ /dev/null @@ -1,236 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -// Tests for the FxA push service. - -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/FxAccountsPush.js"); -Cu.import("resource://gre/modules/Log.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "pushService", - "@mozilla.org/push/Service;1", "nsIPushService"); - -initTestLogging("Trace"); -log.level = Log.Level.Trace; - -const MOCK_ENDPOINT = "http://mochi.test:8888"; - -// tests do not allow external connections, mock the PushService -let mockPushService = { - pushTopic: this.pushService.pushTopic, - subscriptionChangeTopic: this.pushService.subscriptionChangeTopic, - subscribe(scope, principal, cb) { - cb(Components.results.NS_OK, { - endpoint: MOCK_ENDPOINT - }); - }, - unsubscribe(scope, principal, cb) { - cb(Components.results.NS_OK, true); - } -}; - -let mockFxAccounts = { - checkVerificationStatus() {}, - updateDeviceRegistration() {} -}; - -let mockLog = { - trace() {}, - debug() {}, - warn() {}, - error() {} -}; - - -add_task(function* initialize() { - let pushService = new FxAccountsPushService(); - equal(pushService.initialize(), false); -}); - -add_task(function* registerPushEndpointSuccess() { - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - fxAccounts: mockFxAccounts, - }); - - let subscription = yield pushService.registerPushEndpoint(); - equal(subscription.endpoint, MOCK_ENDPOINT); -}); - -add_task(function* registerPushEndpointFailure() { - let failPushService = Object.assign(mockPushService, { - subscribe(scope, principal, cb) { - cb(Components.results.NS_ERROR_ABORT); - } - }); - - let pushService = new FxAccountsPushService({ - pushService: failPushService, - fxAccounts: mockFxAccounts, - }); - - let subscription = yield pushService.registerPushEndpoint(); - equal(subscription, null); -}); - -add_task(function* unsubscribeSuccess() { - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - fxAccounts: mockFxAccounts, - }); - - let result = yield pushService.unsubscribe(); - equal(result, true); -}); - -add_task(function* unsubscribeFailure() { - let failPushService = Object.assign(mockPushService, { - unsubscribe(scope, principal, cb) { - cb(Components.results.NS_ERROR_ABORT); - } - }); - - let pushService = new FxAccountsPushService({ - pushService: failPushService, - fxAccounts: mockFxAccounts, - }); - - let result = yield pushService.unsubscribe(); - equal(result, null); -}); - -add_test(function observeLogout() { - let customLog = Object.assign(mockLog, { - trace: function (msg) { - if (msg === "FxAccountsPushService unsubscribe") { - // logout means we unsubscribe - run_next_test(); - } - } - }); - - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - log: customLog - }); - - pushService.observe(null, ONLOGOUT_NOTIFICATION); -}); - -add_test(function observePushTopicVerify() { - let emptyMsg = { - QueryInterface: function() { - return this; - } - }; - let customAccounts = Object.assign(mockFxAccounts, { - checkVerificationStatus: function () { - // checking verification status on push messages without data - run_next_test(); - } - }); - - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - fxAccounts: customAccounts, - }); - - pushService.observe(emptyMsg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE); -}); - -add_test(function observePushTopicDeviceDisconnected() { - const deviceId = "bogusid"; - let msg = { - data: { - json: () => ({ - command: ON_DEVICE_DISCONNECTED_NOTIFICATION, - data: { - id: deviceId - } - }) - }, - QueryInterface: function() { - return this; - } - }; - let customAccounts = Object.assign(mockFxAccounts, { - handleDeviceDisconnection: function () { - // checking verification status on push messages without data - run_next_test(); - } - }); - - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - fxAccounts: customAccounts, - }); - - pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE); -}); - -add_test(function observePushTopicPasswordChanged() { - let msg = { - data: { - json: () => ({ - command: ON_PASSWORD_CHANGED_NOTIFICATION - }) - }, - QueryInterface: function() { - return this; - } - }; - - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - }); - - pushService._onPasswordChanged = function () { - run_next_test(); - } - - pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE); -}); - -add_test(function observePushTopicPasswordReset() { - let msg = { - data: { - json: () => ({ - command: ON_PASSWORD_RESET_NOTIFICATION - }) - }, - QueryInterface: function() { - return this; - } - }; - - let pushService = new FxAccountsPushService({ - pushService: mockPushService - }); - - pushService._onPasswordChanged = function () { - run_next_test(); - } - - pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE); -}); - -add_test(function observeSubscriptionChangeTopic() { - let customAccounts = Object.assign(mockFxAccounts, { - updateDeviceRegistration: function () { - // subscription change means updating the device registration - run_next_test(); - } - }); - - let pushService = new FxAccountsPushService({ - pushService: mockPushService, - fxAccounts: customAccounts, - }); - - pushService.observe(null, mockPushService.subscriptionChangeTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE); -}); diff --git a/services/fxaccounts/tests/xpcshell/test_storage_manager.js b/services/fxaccounts/tests/xpcshell/test_storage_manager.js deleted file mode 100644 index 6a293a0ff..000000000 --- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js +++ /dev/null @@ -1,477 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -// Tests for the FxA storage manager. - -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://gre/modules/Log.jsm"); - -initTestLogging("Trace"); -log.level = Log.Level.Trace; - -const DEVICE_REGISTRATION_VERSION = 42; - -// A couple of mocks we can use. -function MockedPlainStorage(accountData) { - let data = null; - if (accountData) { - data = { - version: DATA_FORMAT_VERSION, - accountData: accountData, - } - } - this.data = data; - this.numReads = 0; -} -MockedPlainStorage.prototype = { - get: Task.async(function* () { - this.numReads++; - Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data"); - return this.data; - }), - - set: Task.async(function* (data) { - this.data = data; - }), -}; - -function MockedSecureStorage(accountData) { - let data = null; - if (accountData) { - data = { - version: DATA_FORMAT_VERSION, - accountData: accountData, - } - } - this.data = data; - this.numReads = 0; -} - -MockedSecureStorage.prototype = { - fetchCount: 0, - locked: false, - STORAGE_LOCKED: function() {}, - get: Task.async(function* (uid, email) { - this.fetchCount++; - if (this.locked) { - throw new this.STORAGE_LOCKED(); - } - this.numReads++; - Assert.equal(this.numReads, 1, "should only ever be 1 read of unlocked data"); - return this.data; - }), - - set: Task.async(function* (uid, contents) { - this.data = contents; - }), -} - -function add_storage_task(testFunction) { - add_task(function* () { - print("Starting test with secure storage manager"); - yield testFunction(new FxAccountsStorageManager()); - }); - add_task(function* () { - print("Starting test with simple storage manager"); - yield testFunction(new FxAccountsStorageManager({useSecure: false})); - }); -} - -// initialized without account data and there's nothing to read. Not logged in. -add_storage_task(function* checkInitializedEmpty(sm) { - if (sm.secureStorage) { - sm.secureStorage = new MockedSecureStorage(null); - } - yield sm.initialize(); - Assert.strictEqual((yield sm.getAccountData()), null); - Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in") -}); - -// Initialized with account data (ie, simulating a new user being logged in). -// Should reflect the initial data and be written to storage. -add_storage_task(function* checkNewUser(sm) { - let initialAccountData = { - uid: "uid", - email: "someone@somewhere.com", - kA: "kA", - deviceId: "device id" - }; - sm.plainStorage = new MockedPlainStorage() - if (sm.secureStorage) { - sm.secureStorage = new MockedSecureStorage(null); - } - yield sm.initialize(initialAccountData); - let accountData = yield sm.getAccountData(); - Assert.equal(accountData.uid, initialAccountData.uid); - Assert.equal(accountData.email, initialAccountData.email); - Assert.equal(accountData.kA, initialAccountData.kA); - Assert.equal(accountData.deviceId, initialAccountData.deviceId); - - // and it should have been written to storage. - Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid); - Assert.equal(sm.plainStorage.data.accountData.email, initialAccountData.email); - Assert.equal(sm.plainStorage.data.accountData.deviceId, initialAccountData.deviceId); - // check secure - if (sm.secureStorage) { - Assert.equal(sm.secureStorage.data.accountData.kA, initialAccountData.kA); - } else { - Assert.equal(sm.plainStorage.data.accountData.kA, initialAccountData.kA); - } -}); - -// Initialized without account data but storage has it available. -add_storage_task(function* checkEverythingRead(sm) { - sm.plainStorage = new MockedPlainStorage({ - uid: "uid", - email: "someone@somewhere.com", - deviceId: "wibble", - deviceRegistrationVersion: null - }); - if (sm.secureStorage) { - sm.secureStorage = new MockedSecureStorage(null); - } - yield sm.initialize(); - let accountData = yield sm.getAccountData(); - Assert.ok(accountData, "read account data"); - Assert.equal(accountData.uid, "uid"); - Assert.equal(accountData.email, "someone@somewhere.com"); - Assert.equal(accountData.deviceId, "wibble"); - Assert.equal(accountData.deviceRegistrationVersion, null); - // Update the data - we should be able to fetch it back and it should appear - // in our storage. - yield sm.updateAccountData({ - verified: true, - kA: "kA", - kB: "kB", - deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION - }); - accountData = yield sm.getAccountData(); - Assert.equal(accountData.kB, "kB"); - Assert.equal(accountData.kA, "kA"); - Assert.equal(accountData.deviceId, "wibble"); - Assert.equal(accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); - // Check the new value was written to storage. - yield sm._promiseStorageComplete; // storage is written in the background. - // "verified", "deviceId" and "deviceRegistrationVersion" are plain-text fields. - Assert.equal(sm.plainStorage.data.accountData.verified, true); - Assert.equal(sm.plainStorage.data.accountData.deviceId, "wibble"); - Assert.equal(sm.plainStorage.data.accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION); - // "kA" and "foo" are secure - if (sm.secureStorage) { - Assert.equal(sm.secureStorage.data.accountData.kA, "kA"); - Assert.equal(sm.secureStorage.data.accountData.kB, "kB"); - } else { - Assert.equal(sm.plainStorage.data.accountData.kA, "kA"); - Assert.equal(sm.plainStorage.data.accountData.kB, "kB"); - } -}); - -add_storage_task(function* checkInvalidUpdates(sm) { - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - if (sm.secureStorage) { - sm.secureStorage = new MockedSecureStorage(null); - } - Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change"); - Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change"); -}); - -add_storage_task(function* checkNullUpdatesRemovedUnlocked(sm) { - if (sm.secureStorage) { - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); - } else { - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com", - kA: "kA", kB: "kB"}); - } - yield sm.initialize(); - - yield sm.updateAccountData({kA: null}); - let accountData = yield sm.getAccountData(); - Assert.ok(!accountData.kA); - Assert.equal(accountData.kB, "kB"); -}); - -add_storage_task(function* checkDelete(sm) { - if (sm.secureStorage) { - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); - } else { - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com", - kA: "kA", kB: "kB"}); - } - yield sm.initialize(); - - yield sm.deleteAccountData(); - // Storage should have been reset to null. - Assert.equal(sm.plainStorage.data, null); - if (sm.secureStorage) { - Assert.equal(sm.secureStorage.data, null); - } - // And everything should reflect no user. - Assert.equal((yield sm.getAccountData()), null); -}); - -// Some tests only for the secure storage manager. -add_task(function* checkNullUpdatesRemovedLocked() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"}); - sm.secureStorage.locked = true; - yield sm.initialize(); - - yield sm.updateAccountData({kA: null}); - let accountData = yield sm.getAccountData(); - Assert.ok(!accountData.kA); - // still no kB as we are locked. - Assert.ok(!accountData.kB); - - // now unlock - should still be no kA but kB should appear. - sm.secureStorage.locked = false; - accountData = yield sm.getAccountData(); - Assert.ok(!accountData.kA); - Assert.equal(accountData.kB, "kB"); - // And secure storage should have been written with our previously-cached - // data. - Assert.strictEqual(sm.secureStorage.data.accountData.kA, undefined); - Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB"); -}); - -add_task(function* checkEverythingReadSecure() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA"}); - yield sm.initialize(); - - let accountData = yield sm.getAccountData(); - Assert.ok(accountData, "read account data"); - Assert.equal(accountData.uid, "uid"); - Assert.equal(accountData.email, "someone@somewhere.com"); - Assert.equal(accountData.kA, "kA"); -}); - -add_task(function* checkMemoryFieldsNotReturnedByDefault() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA"}); - yield sm.initialize(); - - // keyPair is a memory field. - yield sm.updateAccountData({keyPair: "the keypair value"}); - let accountData = yield sm.getAccountData(); - - // Requesting everything should *not* return in memory fields. - Assert.strictEqual(accountData.keyPair, undefined); - // But requesting them specifically does get them. - accountData = yield sm.getAccountData("keyPair"); - Assert.strictEqual(accountData.keyPair, "the keypair value"); -}); - -add_task(function* checkExplicitGet() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA"}); - yield sm.initialize(); - - let accountData = yield sm.getAccountData(["uid", "kA"]); - Assert.ok(accountData, "read account data"); - Assert.equal(accountData.uid, "uid"); - Assert.equal(accountData.kA, "kA"); - // We didn't ask for email so shouldn't have got it. - Assert.strictEqual(accountData.email, undefined); -}); - -add_task(function* checkExplicitGetNoSecureRead() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA"}); - yield sm.initialize(); - - Assert.equal(sm.secureStorage.fetchCount, 0); - // request 2 fields in secure storage - it should have caused a single fetch. - let accountData = yield sm.getAccountData(["email", "uid"]); - Assert.ok(accountData, "read account data"); - Assert.equal(accountData.uid, "uid"); - Assert.equal(accountData.email, "someone@somewhere.com"); - Assert.strictEqual(accountData.kA, undefined); - Assert.equal(sm.secureStorage.fetchCount, 1); -}); - -add_task(function* checkLockedUpdates() { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"}); - sm.secureStorage.locked = true; - yield sm.initialize(); - - let accountData = yield sm.getAccountData(); - // requesting kA and kB will fail as storage is locked. - Assert.ok(!accountData.kA); - Assert.ok(!accountData.kB); - // While locked we can still update it and see the updated value. - sm.updateAccountData({kA: "new-kA"}); - accountData = yield sm.getAccountData(); - Assert.equal(accountData.kA, "new-kA"); - // unlock. - sm.secureStorage.locked = false; - accountData = yield sm.getAccountData(); - // should reflect the value we updated and the one we didn't. - Assert.equal(accountData.kA, "new-kA"); - Assert.equal(accountData.kB, "kB"); - // And storage should also reflect it. - Assert.strictEqual(sm.secureStorage.data.accountData.kA, "new-kA"); - Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB"); -}); - -// Some tests for the "storage queue" functionality. - -// A helper for our queued tests. It creates a StorageManager and then queues -// an unresolved promise. The tests then do additional setup and checks, then -// resolves or rejects the blocked promise. -var setupStorageManagerForQueueTest = Task.async(function* () { - let sm = new FxAccountsStorageManager(); - sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"}) - sm.secureStorage = new MockedSecureStorage({kA: "kA"}); - sm.secureStorage.locked = true; - yield sm.initialize(); - - let resolveBlocked, rejectBlocked; - let blockedPromise = new Promise((resolve, reject) => { - resolveBlocked = resolve; - rejectBlocked = reject; - }); - - sm._queueStorageOperation(() => blockedPromise); - return {sm, blockedPromise, resolveBlocked, rejectBlocked} -}); - -// First the general functionality. -add_task(function* checkQueueSemantics() { - let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); - - // We've one unresolved promise in the queue - add another promise. - let resolveSubsequent; - let subsequentPromise = new Promise(resolve => { - resolveSubsequent = resolve; - }); - let subsequentCalled = false; - - sm._queueStorageOperation(() => { - subsequentCalled = true; - resolveSubsequent(); - return subsequentPromise; - }); - - // Our "subsequent" function should not have been called yet. - Assert.ok(!subsequentCalled); - - // Release our blocked promise. - resolveBlocked(); - - // Our subsequent promise should end up resolved. - yield subsequentPromise; - Assert.ok(subsequentCalled); - yield sm.finalize(); -}); - -// Check that a queued promise being rejected works correctly. -add_task(function* checkQueueSemanticsOnError() { - let { sm, blockedPromise, rejectBlocked } = yield setupStorageManagerForQueueTest(); - - let resolveSubsequent; - let subsequentPromise = new Promise(resolve => { - resolveSubsequent = resolve; - }); - let subsequentCalled = false; - - sm._queueStorageOperation(() => { - subsequentCalled = true; - resolveSubsequent(); - return subsequentPromise; - }); - - // Our "subsequent" function should not have been called yet. - Assert.ok(!subsequentCalled); - - // Reject our blocked promise - the subsequent operations should still work - // correctly. - rejectBlocked("oh no"); - - // Our subsequent promise should end up resolved. - yield subsequentPromise; - Assert.ok(subsequentCalled); - - // But the first promise should reflect the rejection. - try { - yield blockedPromise; - Assert.ok(false, "expected this promise to reject"); - } catch (ex) { - Assert.equal(ex, "oh no"); - } - yield sm.finalize(); -}); - - -// And some tests for the specific operations that are queued. -add_task(function* checkQueuedReadAndUpdate() { - let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); - // Mock the underlying operations - // _doReadAndUpdateSecure is queued by _maybeReadAndUpdateSecure - let _doReadCalled = false; - sm._doReadAndUpdateSecure = () => { - _doReadCalled = true; - return Promise.resolve(); - } - - let resultPromise = sm._maybeReadAndUpdateSecure(); - Assert.ok(!_doReadCalled); - - resolveBlocked(); - yield resultPromise; - Assert.ok(_doReadCalled); - yield sm.finalize(); -}); - -add_task(function* checkQueuedWrite() { - let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); - // Mock the underlying operations - let __writeCalled = false; - sm.__write = () => { - __writeCalled = true; - return Promise.resolve(); - } - - let writePromise = sm._write(); - Assert.ok(!__writeCalled); - - resolveBlocked(); - yield writePromise; - Assert.ok(__writeCalled); - yield sm.finalize(); -}); - -add_task(function* checkQueuedDelete() { - let { sm, resolveBlocked } = yield setupStorageManagerForQueueTest(); - // Mock the underlying operations - let _deleteCalled = false; - sm._deleteAccountData = () => { - _deleteCalled = true; - return Promise.resolve(); - } - - let resultPromise = sm.deleteAccountData(); - Assert.ok(!_deleteCalled); - - resolveBlocked(); - yield resultPromise; - Assert.ok(_deleteCalled); - yield sm.finalize(); -}); - -function run_test() { - run_next_test(); -} diff --git a/services/fxaccounts/tests/xpcshell/test_web_channel.js b/services/fxaccounts/tests/xpcshell/test_web_channel.js deleted file mode 100644 index 3cf566278..000000000 --- a/services/fxaccounts/tests/xpcshell/test_web_channel.js +++ /dev/null @@ -1,499 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } = - Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm"); - -const URL_STRING = "https://example.com"; - -const mockSendingContext = { - browser: {}, - principal: {}, - eventTarget: {} -}; - -add_test(function () { - validationHelper(undefined, - "Error: Missing configuration options"); - - validationHelper({ - channel_id: WEBCHANNEL_ID - }, - "Error: Missing 'content_uri' option"); - - validationHelper({ - content_uri: 'bad uri', - channel_id: WEBCHANNEL_ID - }, - /NS_ERROR_MALFORMED_URI/); - - validationHelper({ - content_uri: URL_STRING - }, - 'Error: Missing \'channel_id\' option'); - - run_next_test(); -}); - -add_task(function* test_rejection_reporting() { - let mockMessage = { - command: 'fxaccounts:login', - messageId: '1234', - data: { email: 'testuser@testuser.com' }, - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - login(accountData) { - equal(accountData.email, 'testuser@testuser.com', - 'Should forward incoming message data to the helper'); - return Promise.reject(new Error('oops')); - }, - }, - }); - - let promiseSend = new Promise(resolve => { - channel._channel.send = (message, context) => { - resolve({ message, context }); - }; - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); - - let { message, context } = yield promiseSend; - - equal(context, mockSendingContext, 'Should forward the original context'); - equal(message.command, 'fxaccounts:login', - 'Should include the incoming command'); - equal(message.messageId, '1234', 'Should include the message ID'); - equal(message.data.error.message, 'Error: oops', - 'Should convert the error message to a string'); - notStrictEqual(message.data.error.stack, null, - 'Should include the stack for JS error rejections'); -}); - -add_test(function test_exception_reporting() { - let mockMessage = { - command: 'fxaccounts:sync_preferences', - messageId: '5678', - data: { entryPoint: 'fxa:verification_complete' } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - openSyncPreferences(browser, entryPoint) { - equal(entryPoint, 'fxa:verification_complete', - 'Should forward incoming message data to the helper'); - throw new TypeError('splines not reticulated'); - }, - }, - }); - - channel._channel.send = (message, context) => { - equal(context, mockSendingContext, 'Should forward the original context'); - equal(message.command, 'fxaccounts:sync_preferences', - 'Should include the incoming command'); - equal(message.messageId, '5678', 'Should include the message ID'); - equal(message.data.error.message, 'TypeError: splines not reticulated', - 'Should convert the exception to a string'); - notStrictEqual(message.data.error.stack, null, - 'Should include the stack for JS exceptions'); - - run_next_test(); - }; - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_profile_image_change_message() { - var mockMessage = { - command: "profile:change", - data: { uid: "foo" } - }; - - makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { - do_check_eq(data, "foo"); - run_next_test(); - }); - - var channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_login_message() { - let mockMessage = { - command: 'fxaccounts:login', - data: { email: 'testuser@testuser.com' } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - login: function (accountData) { - do_check_eq(accountData.email, 'testuser@testuser.com'); - run_next_test(); - return Promise.resolve(); - } - } - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_logout_message() { - let mockMessage = { - command: 'fxaccounts:logout', - data: { uid: "foo" } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - logout: function (uid) { - do_check_eq(uid, 'foo'); - run_next_test(); - return Promise.resolve(); - } - } - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_delete_message() { - let mockMessage = { - command: 'fxaccounts:delete', - data: { uid: "foo" } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - logout: function (uid) { - do_check_eq(uid, 'foo'); - run_next_test(); - return Promise.resolve(); - } - } - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_can_link_account_message() { - let mockMessage = { - command: 'fxaccounts:can_link_account', - data: { email: 'testuser@testuser.com' } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - shouldAllowRelink: function (email) { - do_check_eq(email, 'testuser@testuser.com'); - run_next_test(); - } - } - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_sync_preferences_message() { - let mockMessage = { - command: 'fxaccounts:sync_preferences', - data: { entryPoint: 'fxa:verification_complete' } - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING, - helpers: { - openSyncPreferences: function (browser, entryPoint) { - do_check_eq(entryPoint, 'fxa:verification_complete'); - do_check_eq(browser, mockSendingContext.browser); - run_next_test(); - } - } - }); - - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); -}); - -add_test(function test_unrecognized_message() { - let mockMessage = { - command: 'fxaccounts:unrecognized', - data: {} - }; - - let channel = new FxAccountsWebChannel({ - channel_id: WEBCHANNEL_ID, - content_uri: URL_STRING - }); - - // no error is expected. - channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext); - run_next_test(); -}); - - -add_test(function test_helpers_should_allow_relink_same_email() { - let helpers = new FxAccountsWebChannelHelpers(); - - helpers.setPreviousAccountNameHashPref('testuser@testuser.com'); - do_check_true(helpers.shouldAllowRelink('testuser@testuser.com')); - - run_next_test(); -}); - -add_test(function test_helpers_should_allow_relink_different_email() { - let helpers = new FxAccountsWebChannelHelpers(); - - helpers.setPreviousAccountNameHashPref('testuser@testuser.com'); - - helpers._promptForRelink = (acctName) => { - return acctName === 'allowed_to_relink@testuser.com'; - }; - - do_check_true(helpers.shouldAllowRelink('allowed_to_relink@testuser.com')); - do_check_false(helpers.shouldAllowRelink('not_allowed_to_relink@testuser.com')); - - run_next_test(); -}); - -add_task(function* test_helpers_login_without_customize_sync() { - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - setSignedInUser: function(accountData) { - return new Promise(resolve => { - // ensure fxAccounts is informed of the new user being signed in. - do_check_eq(accountData.email, 'testuser@testuser.com'); - - // verifiedCanLinkAccount should be stripped in the data. - do_check_false('verifiedCanLinkAccount' in accountData); - - // the customizeSync pref should not update - do_check_false(helpers.getShowCustomizeSyncPref()); - - // previously signed in user preference is updated. - do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com')); - - resolve(); - }); - } - } - }); - - // the show customize sync pref should stay the same - helpers.setShowCustomizeSyncPref(false); - - // ensure the previous account pref is overwritten. - helpers.setPreviousAccountNameHashPref('lastuser@testuser.com'); - - yield helpers.login({ - email: 'testuser@testuser.com', - verifiedCanLinkAccount: true, - customizeSync: false - }); -}); - -add_task(function* test_helpers_login_with_customize_sync() { - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - setSignedInUser: function(accountData) { - return new Promise(resolve => { - // ensure fxAccounts is informed of the new user being signed in. - do_check_eq(accountData.email, 'testuser@testuser.com'); - - // customizeSync should be stripped in the data. - do_check_false('customizeSync' in accountData); - - // the customizeSync pref should not update - do_check_true(helpers.getShowCustomizeSyncPref()); - - resolve(); - }); - } - } - }); - - // the customize sync pref should be overwritten - helpers.setShowCustomizeSyncPref(false); - - yield helpers.login({ - email: 'testuser@testuser.com', - verifiedCanLinkAccount: true, - customizeSync: true - }); -}); - -add_task(function* test_helpers_login_with_customize_sync_and_declined_engines() { - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - setSignedInUser: function(accountData) { - return new Promise(resolve => { - // ensure fxAccounts is informed of the new user being signed in. - do_check_eq(accountData.email, 'testuser@testuser.com'); - - // customizeSync should be stripped in the data. - do_check_false('customizeSync' in accountData); - do_check_false('declinedSyncEngines' in accountData); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true); - - // the customizeSync pref should be disabled - do_check_false(helpers.getShowCustomizeSyncPref()); - - resolve(); - }); - } - } - }); - - // the customize sync pref should be overwritten - helpers.setShowCustomizeSyncPref(true); - - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true); - do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true); - yield helpers.login({ - email: 'testuser@testuser.com', - verifiedCanLinkAccount: true, - customizeSync: true, - declinedSyncEngines: ['addons', 'prefs'] - }); -}); - -add_test(function test_helpers_open_sync_preferences() { - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - } - }); - - let mockBrowser = { - loadURI(uri) { - do_check_eq(uri, "about:preferences?entrypoint=fxa%3Averification_complete#sync"); - run_next_test(); - } - }; - - helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete"); -}); - -add_task(function* test_helpers_change_password() { - let wasCalled = { - updateUserAccountData: false, - updateDeviceRegistration: false - }; - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - updateUserAccountData(credentials) { - return new Promise(resolve => { - do_check_true(credentials.hasOwnProperty("email")); - do_check_true(credentials.hasOwnProperty("uid")); - do_check_true(credentials.hasOwnProperty("kA")); - do_check_true(credentials.hasOwnProperty("deviceId")); - do_check_null(credentials.deviceId); - // "foo" isn't a field known by storage, so should be dropped. - do_check_false(credentials.hasOwnProperty("foo")); - wasCalled.updateUserAccountData = true; - - resolve(); - }); - }, - - updateDeviceRegistration() { - do_check_eq(arguments.length, 0); - wasCalled.updateDeviceRegistration = true; - return Promise.resolve() - } - } - }); - yield helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" }); - do_check_true(wasCalled.updateUserAccountData); - do_check_true(wasCalled.updateDeviceRegistration); -}); - -add_task(function* test_helpers_change_password_with_error() { - let wasCalled = { - updateUserAccountData: false, - updateDeviceRegistration: false - }; - let helpers = new FxAccountsWebChannelHelpers({ - fxAccounts: { - updateUserAccountData() { - wasCalled.updateUserAccountData = true; - return Promise.reject(); - }, - - updateDeviceRegistration() { - wasCalled.updateDeviceRegistration = true; - return Promise.resolve() - } - } - }); - try { - yield helpers.changePassword({}); - do_check_false('changePassword should have rejected'); - } catch (_) { - do_check_true(wasCalled.updateUserAccountData); - do_check_false(wasCalled.updateDeviceRegistration); - } -}); - -function run_test() { - run_next_test(); -} - -function makeObserver(aObserveTopic, aObserveFunc) { - let callback = function (aSubject, aTopic, aData) { - log.debug("observed " + aTopic + " " + aData); - if (aTopic == aObserveTopic) { - removeMe(); - aObserveFunc(aSubject, aTopic, aData); - } - }; - - function removeMe() { - log.debug("removing observer for " + aObserveTopic); - Services.obs.removeObserver(callback, aObserveTopic); - } - - Services.obs.addObserver(callback, aObserveTopic, false); - return removeMe; -} - -function validationHelper(params, expected) { - try { - new FxAccountsWebChannel(params); - } catch (e) { - if (typeof expected === 'string') { - return do_check_eq(e.toString(), expected); - } else { - return do_check_true(e.toString().match(expected)); - } - } - throw new Error("Validation helper error"); -} diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini deleted file mode 100644 index 56a3d2947..000000000 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ /dev/null @@ -1,23 +0,0 @@ -[DEFAULT] -head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js -tail = -skip-if = (toolkit == 'android' || appname == 'thunderbird') -support-files = - !/services/common/tests/unit/head_helpers.js - !/services/common/tests/unit/head_http.js - -[test_accounts.js] -[test_accounts_device_registration.js] -[test_client.js] -[test_credentials.js] -[test_loginmgr_storage.js] -[test_oauth_client.js] -[test_oauth_grant_client.js] -[test_oauth_grant_client_server.js] -[test_oauth_tokens.js] -[test_oauth_token_storage.js] -[test_profile_client.js] -[test_push_service.js] -[test_web_channel.js] -[test_profile.js] -[test_storage_manager.js] diff --git a/services/moz.build b/services/moz.build index 91f1e285e..e98d15275 100644 --- a/services/moz.build +++ b/services/moz.build @@ -9,8 +9,5 @@ DIRS += [ 'crypto', ] -if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': - DIRS += ['fxaccounts'] - if CONFIG['MOZ_SERVICES_SYNC']: DIRS += ['sync'] diff --git a/services/sync/modules-testing/fxa_utils.js b/services/sync/modules-testing/fxa_utils.js deleted file mode 100644 index 70aa17b03..000000000 --- a/services/sync/modules-testing/fxa_utils.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict";
-
-this.EXPORTED_SYMBOLS = [
- "initializeIdentityWithTokenServerResponse",
-];
-
-var {utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://services-common/tokenserverclient.js");
-Cu.import("resource://testing-common/services/common/logging.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-// Create a new browserid_identity object and initialize it with a
-// mocked TokenServerClient which always receives the specified response.
-this.initializeIdentityWithTokenServerResponse = function(response) {
- // First create a mock "request" object that well' hack into the token server.
- // A log for it
- let requestLog = Log.repository.getLogger("testing.mock-rest");
- if (!requestLog.appenders.length) { // might as well see what it says :)
- requestLog.addAppender(new Log.DumpAppender());
- requestLog.level = Log.Level.Trace;
- }
-
- // A mock request object.
- function MockRESTRequest(url) {};
- MockRESTRequest.prototype = {
- _log: requestLog,
- setHeader: function() {},
- get: function(callback) {
- this.response = response;
- callback.call(this);
- }
- }
- // The mocked TokenServer client which will get the response.
- function MockTSC() { }
- MockTSC.prototype = new TokenServerClient();
- MockTSC.prototype.constructor = MockTSC;
- MockTSC.prototype.newRESTRequest = function(url) {
- return new MockRESTRequest(url);
- }
- // Arrange for the same observerPrefix as browserid_identity uses.
- MockTSC.prototype.observerPrefix = "weave:service";
-
- // tie it all together.
- Weave.Status.__authManager = Weave.Service.identity = new BrowserIDManager();
- Weave.Service._clusterManager = Weave.Service.identity.createClusterManager(Weave.Service);
- let browseridManager = Weave.Service.identity;
- // a sanity check
- if (!(browseridManager instanceof BrowserIDManager)) {
- throw new Error("sync isn't configured for browserid_identity");
- }
- let mockTSC = new MockTSC()
- configureFxAccountIdentity(browseridManager);
- browseridManager._tokenServerClient = mockTSC;
-}
diff --git a/services/sync/modules-testing/utils.js b/services/sync/modules-testing/utils.js index fc14f2fbd..64c9b163d 100644 --- a/services/sync/modules-testing/utils.js +++ b/services/sync/modules-testing/utils.js @@ -28,8 +28,6 @@ Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/browserid_identity.js"); Cu.import("resource://testing-common/services/common/logging.js"); Cu.import("resource://testing-common/services/sync/fakeservices.js"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://gre/modules/Promise.jsm"); /** @@ -77,27 +75,8 @@ this.setBasicCredentials = this.makeIdentityConfig = function(overrides) { // first setup the defaults. let result = { - // Username used in both fxaccount and sync identity configs. + // Username used in sync identity config. username: "foo", - // fxaccount specific credentials. - fxaccount: { - user: { - assertion: 'assertion', - email: 'email', - kA: 'kA', - kB: 'kB', - sessionToken: 'sessionToken', - uid: 'user_uid', - verified: true, - }, - token: { - endpoint: Svc.Prefs.get("tokenServerURI"), - duration: 300, - id: "id", - key: "key", - // uid will be set to the username. - } - }, sync: { // username will come from the top-level username password: "whatever", @@ -114,64 +93,15 @@ this.makeIdentityConfig = function(overrides) { // TODO: allow just some attributes to be specified result.sync = overrides.sync; } - if (overrides.fxaccount) { - // TODO: allow just some attributes to be specified - result.fxaccount = overrides.fxaccount; - } } return result; } -// Configure an instance of an FxAccount identity provider with the specified -// config (or the default config if not specified). -this.configureFxAccountIdentity = function(authService, - config = makeIdentityConfig()) { - let MockInternal = {}; - let fxa = new FxAccounts(MockInternal); - - // until we get better test infrastructure for bid_identity, we set the - // signedin user's "email" to the username, simply as many tests rely on this. - config.fxaccount.user.email = config.username; - fxa.internal.currentAccountState.signedInUser = { - version: DATA_FORMAT_VERSION, - accountData: config.fxaccount.user - }; - fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { - this.cert = { - validUntil: fxa.internal.now() + CERT_LIFETIME, - cert: "certificate", - }; - return Promise.resolve(this.cert.cert); - }; - - let mockTSC = { // TokenServerClient - getTokenFromBrowserIDAssertion: function(uri, assertion, cb) { - config.fxaccount.token.uid = config.username; - cb(null, config.fxaccount.token); - }, - }; - authService._fxaService = fxa; - authService._tokenServerClient = mockTSC; - // Set the "account" of the browserId manager to be the "email" of the - // logged in user of the mockFXA service. - authService._signedInUser = fxa.internal.currentAccountState.signedInUser.accountData; - authService._account = config.fxaccount.user.email; -} - this.configureIdentity = function(identityOverrides) { let config = makeIdentityConfig(identityOverrides); let ns = {}; Cu.import("resource://services-sync/service.js", ns); - if (ns.Service.identity instanceof BrowserIDManager) { - // do the FxAccounts thang... - configureFxAccountIdentity(ns.Service.identity, config); - return ns.Service.identity.initializeWithCurrentIdentity().then(() => { - // need to wait until this identity manager is readyToAuthenticate. - return ns.Service.identity.whenReadyToAuthenticate.promise; - }); - } - // old style identity provider. setBasicCredentials(config.username, config.sync.password, config.sync.syncKey); let deferred = Promise.defer(); deferred.resolve(); @@ -184,7 +114,6 @@ this.SyncTestingInfrastructure = function (server, username, password, syncKey) ensureLegacyIdentityManager(); let config = makeIdentityConfig(); - // XXX - hacks for the sync identity provider. if (username) config.username = username; if (password) @@ -223,10 +152,10 @@ this.encryptPayload = function encryptPayload(cleartext) { }; } -// This helper can be used instead of 'add_test' or 'add_task' to run the +// This helper was used instead of 'add_test' or 'add_task' to run the // specified test function twice - once with the old-style sync identity // manager and once with the new-style BrowserID identity manager, to ensure -// it works in both cases. +// it worked in both cases. Currently it's equal to just one. XXX: cleanup? // // * The test itself should be passed as 'test' - ie, test code will generally // pass |this|. @@ -248,12 +177,4 @@ this.add_identity_test = function(test, testFunction) { yield testFunction(); Status.__authManager = ns.Service.identity = oldIdentity; }); - // another task for the FxAccounts identity manager. - test.add_task(function() { - note("FxAccounts"); - let oldIdentity = Status._authManager; - Status.__authManager = ns.Service.identity = new BrowserIDManager(); - yield testFunction(); - Status.__authManager = ns.Service.identity = oldIdentity; - }); } diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 12496d23a..7fd5a7971 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -19,13 +19,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm", this); -// FxAccountsCommon.js doesn't use a "namespace", so create one here. -XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() { - let FxAccountsCommon = {}; - Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon); - return FxAccountsCommon; -}); - /* * Utility functions */ @@ -599,9 +592,6 @@ this.Utils = { */ getSyncCredentialsHosts: function() { let result = new Set(this.getSyncCredentialsHostsLegacy()); - for (let host of this.getSyncCredentialsHostsFxA()) { - result.add(host); - } return result; }, @@ -613,36 +603,6 @@ this.Utils = { return new Set([PWDMGR_HOST]); }, - /* - * Get the FxA identity hosts. - */ - getSyncCredentialsHostsFxA: function() { - // This is somewhat expensive and the result static, so we cache the result. - if (this._syncCredentialsHostsFxA) { - return this._syncCredentialsHostsFxA; - } - let result = new Set(); - // the FxA host - result.add(FxAccountsCommon.FXA_PWDMGR_HOST); - // - // The FxA hosts - these almost certainly all have the same hostname, but - // better safe than sorry... - for (let prefName of ["identity.fxaccounts.remote.force_auth.uri", - "identity.fxaccounts.remote.signup.uri", - "identity.fxaccounts.remote.signin.uri", - "identity.fxaccounts.settings.uri"]) { - let prefVal; - try { - prefVal = Services.prefs.getCharPref(prefName); - } catch (_) { - continue; - } - let uri = Services.io.newURI(prefVal, null, null); - result.add(uri.prePath); - } - return this._syncCredentialsHostsFxA = result; - }, - getDefaultDeviceName() { // Generate a client name if we don't have a useful one yet let env = Cc["@mozilla.org/process/environment;1"] diff --git a/services/sync/moz.build b/services/sync/moz.build index ceb4eb502..56421a03e 100644 --- a/services/sync/moz.build +++ b/services/sync/moz.build @@ -54,7 +54,6 @@ EXTRA_JS_MODULES['services-sync'].stages += [ TESTING_JS_MODULES.services.sync += [ 'modules-testing/fakeservices.js', - 'modules-testing/fxa_utils.js', 'modules-testing/rotaryengine.js', 'modules-testing/utils.js', ] diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js deleted file mode 100644 index f3cde9f8f..000000000 --- a/services/sync/tests/unit/test_browserid_identity.js +++ /dev/null @@ -1,682 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://services-sync/browserid_identity.js"); -Cu.import("resource://services-sync/rest.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://testing-common/services/sync/utils.js"); -Cu.import("resource://testing-common/services/sync/fxa_utils.js"); -Cu.import("resource://services-common/hawkclient.js"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://gre/modules/FxAccountsCommon.js"); -Cu.import("resource://services-sync/service.js"); -Cu.import("resource://services-sync/status.js"); -Cu.import("resource://services-sync/constants.js"); - -const SECOND_MS = 1000; -const MINUTE_MS = SECOND_MS * 60; -const HOUR_MS = MINUTE_MS * 60; - -let identityConfig = makeIdentityConfig(); -let browseridManager = new BrowserIDManager(); -configureFxAccountIdentity(browseridManager, identityConfig); - -/** - * Mock client clock and skew vs server in FxAccounts signed-in user module and - * API client. browserid_identity.js queries these values to construct HAWK - * headers. We will use this to test clock skew compensation in these headers - * below. - */ -let MockFxAccountsClient = function() { - FxAccountsClient.apply(this); -}; -MockFxAccountsClient.prototype = { - __proto__: FxAccountsClient.prototype -}; - -function MockFxAccounts() { - let fxa = new FxAccounts({ - _now_is: Date.now(), - - now: function () { - return this._now_is; - }, - - fxAccountsClient: new MockFxAccountsClient() - }); - fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { - this.cert = { - validUntil: fxa.internal.now() + CERT_LIFETIME, - cert: "certificate", - }; - return Promise.resolve(this.cert.cert); - }; - return fxa; -} - -function run_test() { - initTestLogging("Trace"); - Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace; - Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace; - run_next_test(); -}; - -add_test(function test_initial_state() { - _("Verify initial state"); - do_check_false(!!browseridManager._token); - do_check_false(browseridManager.hasValidToken()); - run_next_test(); - } -); - -add_task(function test_initialializeWithCurrentIdentity() { - _("Verify start after initializeWithCurrentIdentity"); - browseridManager.initializeWithCurrentIdentity(); - yield browseridManager.whenReadyToAuthenticate.promise; - do_check_true(!!browseridManager._token); - do_check_true(browseridManager.hasValidToken()); - do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email); - } -); - -add_task(function test_initialializeWithNoKeys() { - _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken"); - let identityConfig = makeIdentityConfig(); - delete identityConfig.fxaccount.user.kA; - delete identityConfig.fxaccount.user.kB; - // there's no keyFetchToken by default, so the initialize should fail. - configureFxAccountIdentity(browseridManager, identityConfig); - - yield browseridManager.initializeWithCurrentIdentity(); - yield browseridManager.whenReadyToAuthenticate.promise; - do_check_eq(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys"); - do_check_false(browseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys"); - do_check_eq(browseridManager._token, null, "we don't have a token"); -}); - -add_test(function test_getResourceAuthenticator() { - _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."); - configureFxAccountIdentity(browseridManager); - let authenticator = browseridManager.getResourceAuthenticator(); - do_check_true(!!authenticator); - let req = {uri: CommonUtils.makeURI( - "https://example.net/somewhere/over/the/rainbow"), - method: 'GET'}; - let output = authenticator(req, 'GET'); - do_check_true('headers' in output); - do_check_true('authorization' in output.headers); - do_check_true(output.headers.authorization.startsWith('Hawk')); - _("Expected internal state after successful call."); - do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid); - run_next_test(); - } -); - -add_test(function test_getRESTRequestAuthenticator() { - _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object."); - let request = new SyncStorageRequest( - "https://example.net/somewhere/over/the/rainbow"); - let authenticator = browseridManager.getRESTRequestAuthenticator(); - do_check_true(!!authenticator); - let output = authenticator(request, 'GET'); - do_check_eq(request.uri, output.uri); - do_check_true(output._headers.authorization.startsWith('Hawk')); - do_check_true(output._headers.authorization.includes('nonce')); - do_check_true(browseridManager.hasValidToken()); - run_next_test(); - } -); - -add_test(function test_resourceAuthenticatorSkew() { - _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."); - - // Clock is skewed 12 hours into the future - // We pick a date in the past so we don't risk concealing bugs in code that - // uses new Date() instead of our given date. - let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; - let browseridManager = new BrowserIDManager(); - let hawkClient = new HawkClient("https://example.net/v1", "/foo"); - - // mock fxa hawk client skew - hawkClient.now = function() { - dump("mocked client now: " + now + '\n'); - return now; - } - // Imagine there's already been one fxa request and the hawk client has - // already detected skew vs the fxa auth server. - let localtimeOffsetMsec = -1 * 12 * HOUR_MS; - hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; - - let fxaClient = new MockFxAccountsClient(); - fxaClient.hawk = hawkClient; - - // Sanity check - do_check_eq(hawkClient.now(), now); - do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); - - // Properly picked up by the client - do_check_eq(fxaClient.now(), now); - do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); - - let fxa = new MockFxAccounts(); - fxa.internal._now_is = now; - fxa.internal.fxAccountsClient = fxaClient; - - // Picked up by the signed-in user module - do_check_eq(fxa.internal.now(), now); - do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec); - - do_check_eq(fxa.now(), now); - do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec); - - // Mocks within mocks... - configureFxAccountIdentity(browseridManager, identityConfig); - - // Ensure the new FxAccounts mock has a signed-in user. - fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; - - browseridManager._fxaService = fxa; - - do_check_eq(browseridManager._fxaService.internal.now(), now); - do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec, - localtimeOffsetMsec); - - do_check_eq(browseridManager._fxaService.now(), now); - do_check_eq(browseridManager._fxaService.localtimeOffsetMsec, - localtimeOffsetMsec); - - let request = new SyncStorageRequest("https://example.net/i/like/pie/"); - let authenticator = browseridManager.getResourceAuthenticator(); - let output = authenticator(request, 'GET'); - dump("output" + JSON.stringify(output)); - let authHeader = output.headers.authorization; - do_check_true(authHeader.startsWith('Hawk')); - - // Skew correction is applied in the header and we're within the two-minute - // window. - do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); - do_check_true( - (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); - - run_next_test(); -}); - -add_test(function test_RESTResourceAuthenticatorSkew() { - _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."); - - // Clock is skewed 12 hours into the future from our arbitary date - let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; - let browseridManager = new BrowserIDManager(); - let hawkClient = new HawkClient("https://example.net/v1", "/foo"); - - // mock fxa hawk client skew - hawkClient.now = function() { - return now; - } - // Imagine there's already been one fxa request and the hawk client has - // already detected skew vs the fxa auth server. - hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; - - let fxaClient = new MockFxAccountsClient(); - fxaClient.hawk = hawkClient; - let fxa = new MockFxAccounts(); - fxa.internal._now_is = now; - fxa.internal.fxAccountsClient = fxaClient; - - configureFxAccountIdentity(browseridManager, identityConfig); - - // Ensure the new FxAccounts mock has a signed-in user. - fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; - - browseridManager._fxaService = fxa; - - do_check_eq(browseridManager._fxaService.internal.now(), now); - - let request = new SyncStorageRequest("https://example.net/i/like/pie/"); - let authenticator = browseridManager.getResourceAuthenticator(); - let output = authenticator(request, 'GET'); - dump("output" + JSON.stringify(output)); - let authHeader = output.headers.authorization; - do_check_true(authHeader.startsWith('Hawk')); - - // Skew correction is applied in the header and we're within the two-minute - // window. - do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); - do_check_true( - (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); - - run_next_test(); -}); - -add_task(function test_ensureLoggedIn() { - configureFxAccountIdentity(browseridManager); - yield browseridManager.initializeWithCurrentIdentity(); - yield browseridManager.whenReadyToAuthenticate.promise; - Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); - yield browseridManager.ensureLoggedIn(); - Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked"); - Assert.ok(browseridManager._shouldHaveSyncKeyBundle, - "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); - - // arrange for no logged in user. - let fxa = browseridManager._fxaService - let signedInUser = fxa.internal.currentAccountState.signedInUser; - fxa.internal.currentAccountState.signedInUser = null; - browseridManager.initializeWithCurrentIdentity(); - Assert.ok(!browseridManager._shouldHaveSyncKeyBundle, - "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are."); - Status.login = LOGIN_FAILED_NO_USERNAME; - yield Assert.rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user"); - Assert.ok(browseridManager._shouldHaveSyncKeyBundle, - "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); - fxa.internal.currentAccountState.signedInUser = signedInUser; - Status.login = LOGIN_FAILED_LOGIN_REJECTED; - yield Assert.rejects(browseridManager.ensureLoggedIn(), - "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection"); - Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, - "status should remain LOGIN_FAILED_LOGIN_REJECTED"); - Status.login = LOGIN_FAILED_NETWORK_ERROR; - yield browseridManager.ensureLoggedIn(); - Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); -}); - -add_test(function test_tokenExpiration() { - _("BrowserIDManager notices token expiration:"); - let bimExp = new BrowserIDManager(); - configureFxAccountIdentity(bimExp, identityConfig); - - let authenticator = bimExp.getResourceAuthenticator(); - do_check_true(!!authenticator); - let req = {uri: CommonUtils.makeURI( - "https://example.net/somewhere/over/the/rainbow"), - method: 'GET'}; - authenticator(req, 'GET'); - - // Mock the clock. - _("Forcing the token to expire ..."); - Object.defineProperty(bimExp, "_now", { - value: function customNow() { - return (Date.now() + 3000001); - }, - writable: true, - }); - do_check_true(bimExp._token.expiration < bimExp._now()); - _("... means BrowserIDManager knows to re-fetch it on the next call."); - do_check_false(bimExp.hasValidToken()); - run_next_test(); - } -); - -add_test(function test_sha256() { - // Test vectors from http://www.bichlmeier.info/sha256test.html - let vectors = [ - ["", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], - ["abc", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], - ["message digest", - "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"], - ["secure hash algorithm", - "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"], - ["SHA256 is considered to be safe", - "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"], - ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", - "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"], - ["For this sample, this 63-byte string will be used as input data", - "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"], - ["This is exactly 64 bytes long, not counting the terminating byte", - "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"] - ]; - let bidUser = new BrowserIDManager(); - for (let [input,output] of vectors) { - do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output); - } - run_next_test(); -}); - -add_test(function test_computeXClientStateHeader() { - let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d"; - let kB = CommonUtils.hexToBytes(kBhex); - - let bidUser = new BrowserIDManager(); - let header = bidUser._computeXClientState(kB); - - do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f"); - run_next_test(); -}); - -add_task(function test_getTokenErrors() { - _("BrowserIDManager correctly handles various failures to get a token."); - - _("Arrange for a 401 - Sync should reflect an auth error."); - initializeIdentityWithTokenServerResponse({ - status: 401, - headers: {"content-type": "application/json"}, - body: JSON.stringify({}), - }); - let browseridManager = Service.identity; - - yield browseridManager.initializeWithCurrentIdentity(); - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to 401"); - Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); - - // XXX - other interesting responses to return? - - // And for good measure, some totally "unexpected" errors - we generally - // assume these problems are going to magically go away at some point. - _("Arrange for an empty body with a 200 response - should reflect a network error."); - initializeIdentityWithTokenServerResponse({ - status: 200, - headers: [], - body: "", - }); - browseridManager = Service.identity; - yield browseridManager.initializeWithCurrentIdentity(); - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to non-JSON response"); - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); -}); - -add_task(function test_getTokenErrorWithRetry() { - _("tokenserver sends an observer notification on various backoff headers."); - - // Set Sync's backoffInterval to zero - after we simulated the backoff header - // it should reflect the value we sent. - Status.backoffInterval = 0; - _("Arrange for a 503 with a Retry-After header."); - initializeIdentityWithTokenServerResponse({ - status: 503, - headers: {"content-type": "application/json", - "retry-after": "100"}, - body: JSON.stringify({}), - }); - let browseridManager = Service.identity; - - yield browseridManager.initializeWithCurrentIdentity(); - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to 503"); - - // The observer should have fired - check it got the value in the response. - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); - // Sync will have the value in ms with some slop - so check it is at least that. - Assert.ok(Status.backoffInterval >= 100000); - - _("Arrange for a 200 with an X-Backoff header."); - Status.backoffInterval = 0; - initializeIdentityWithTokenServerResponse({ - status: 503, - headers: {"content-type": "application/json", - "x-backoff": "200"}, - body: JSON.stringify({}), - }); - browseridManager = Service.identity; - - yield browseridManager.initializeWithCurrentIdentity(); - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to no token in response"); - - // The observer should have fired - check it got the value in the response. - Assert.ok(Status.backoffInterval >= 200000); -}); - -add_task(function test_getKeysErrorWithBackoff() { - _("Auth server (via hawk) sends an observer notification on backoff headers."); - - // Set Sync's backoffInterval to zero - after we simulated the backoff header - // it should reflect the value we sent. - Status.backoffInterval = 0; - _("Arrange for a 503 with a X-Backoff header."); - - let config = makeIdentityConfig(); - // We want no kA or kB so we attempt to fetch them. - delete config.fxaccount.user.kA; - delete config.fxaccount.user.kB; - config.fxaccount.user.keyFetchToken = "keyfetchtoken"; - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "get"); - Assert.equal(uri, "http://mockedserver:9999/account/keys") - return { - status: 503, - headers: {"content-type": "application/json", - "x-backoff": "100"}, - body: "{}", - } - }); - - let browseridManager = Service.identity; - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to 503"); - - // The observer should have fired - check it got the value in the response. - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); - // Sync will have the value in ms with some slop - so check it is at least that. - Assert.ok(Status.backoffInterval >= 100000); -}); - -add_task(function test_getKeysErrorWithRetry() { - _("Auth server (via hawk) sends an observer notification on retry headers."); - - // Set Sync's backoffInterval to zero - after we simulated the backoff header - // it should reflect the value we sent. - Status.backoffInterval = 0; - _("Arrange for a 503 with a Retry-After header."); - - let config = makeIdentityConfig(); - // We want no kA or kB so we attempt to fetch them. - delete config.fxaccount.user.kA; - delete config.fxaccount.user.kB; - config.fxaccount.user.keyFetchToken = "keyfetchtoken"; - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "get"); - Assert.equal(uri, "http://mockedserver:9999/account/keys") - return { - status: 503, - headers: {"content-type": "application/json", - "retry-after": "100"}, - body: "{}", - } - }); - - let browseridManager = Service.identity; - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "should reject due to 503"); - - // The observer should have fired - check it got the value in the response. - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); - // Sync will have the value in ms with some slop - so check it is at least that. - Assert.ok(Status.backoffInterval >= 100000); -}); - -add_task(function test_getHAWKErrors() { - _("BrowserIDManager correctly handles various HAWK failures."); - - _("Arrange for a 401 - Sync should reflect an auth error."); - let config = makeIdentityConfig(); - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "post"); - Assert.equal(uri, "http://mockedserver:9999/certificate/sign") - return { - status: 401, - headers: {"content-type": "application/json"}, - body: JSON.stringify({}), - } - }); - Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); - - // XXX - other interesting responses to return? - - // And for good measure, some totally "unexpected" errors - we generally - // assume these problems are going to magically go away at some point. - _("Arrange for an empty body with a 200 response - should reflect a network error."); - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "post"); - Assert.equal(uri, "http://mockedserver:9999/certificate/sign") - return { - status: 200, - headers: [], - body: "", - } - }); - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); -}); - -add_task(function test_getGetKeysFailing401() { - _("BrowserIDManager correctly handles 401 responses fetching keys."); - - _("Arrange for a 401 - Sync should reflect an auth error."); - let config = makeIdentityConfig(); - // We want no kA or kB so we attempt to fetch them. - delete config.fxaccount.user.kA; - delete config.fxaccount.user.kB; - config.fxaccount.user.keyFetchToken = "keyfetchtoken"; - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "get"); - Assert.equal(uri, "http://mockedserver:9999/account/keys") - return { - status: 401, - headers: {"content-type": "application/json"}, - body: "{}", - } - }); - Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); -}); - -add_task(function test_getGetKeysFailing503() { - _("BrowserIDManager correctly handles 5XX responses fetching keys."); - - _("Arrange for a 503 - Sync should reflect a network error."); - let config = makeIdentityConfig(); - // We want no kA or kB so we attempt to fetch them. - delete config.fxaccount.user.kA; - delete config.fxaccount.user.kB; - config.fxaccount.user.keyFetchToken = "keyfetchtoken"; - yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { - Assert.equal(method, "get"); - Assert.equal(uri, "http://mockedserver:9999/account/keys") - return { - status: 503, - headers: {"content-type": "application/json"}, - body: "{}", - } - }); - Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); -}); - -add_task(function test_getKeysMissing() { - _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); - - let browseridManager = new BrowserIDManager(); - let identityConfig = makeIdentityConfig(); - // our mock identity config already has kA and kB - remove them or we never - // try and fetch them. - delete identityConfig.fxaccount.user.kA; - delete identityConfig.fxaccount.user.kB; - identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken'; - - configureFxAccountIdentity(browseridManager, identityConfig); - - // Mock a fxAccounts object that returns no keys - let fxa = new FxAccounts({ - fetchAndUnwrapKeys: function () { - return Promise.resolve({}); - }, - fxAccountsClient: new MockFxAccountsClient() - }); - - // Add a mock to the currentAccountState object. - fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { - this.cert = { - validUntil: fxa.internal.now() + CERT_LIFETIME, - cert: "certificate", - }; - return Promise.resolve(this.cert.cert); - }; - - // Ensure the new FxAccounts mock has a signed-in user. - fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; - - browseridManager._fxaService = fxa; - - yield browseridManager.initializeWithCurrentIdentity(); - - let ex; - try { - yield browseridManager.whenReadyToAuthenticate.promise; - } catch (e) { - ex = e; - } - - Assert.ok(ex.message.indexOf("missing kA or kB") >= 0); -}); - -// End of tests -// Utility functions follow - -// Create a new browserid_identity object and initialize it with a -// hawk mock that simulates HTTP responses. -// The callback function will be called each time the mocked hawk server wants -// to make a request. The result of the callback should be the mock response -// object that will be returned to hawk. -// A token server mock will be used that doesn't hit a server, so we move -// directly to a hawk request. -function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { - // A mock request object. - function MockRESTRequest(uri, credentials, extra) { - this._uri = uri; - this._credentials = credentials; - this._extra = extra; - }; - MockRESTRequest.prototype = { - setHeader: function() {}, - post: function(data, callback) { - this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); - callback.call(this); - }, - get: function(callback) { - this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); - callback.call(this); - } - } - - // The hawk client. - function MockedHawkClient() {} - MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); - MockedHawkClient.prototype.constructor = MockedHawkClient; - MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { - return new MockRESTRequest(uri, credentials, extra); - } - // Arrange for the same observerPrefix as FxAccountsClient uses - MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; - - // tie it all together - configureFxAccountIdentity isn't useful here :( - let fxaClient = new MockFxAccountsClient(); - fxaClient.hawk = new MockedHawkClient(); - let internal = { - fxAccountsClient: fxaClient, - } - let fxa = new FxAccounts(internal); - fxa.internal.currentAccountState.signedInUser = { - accountData: config.fxaccount.user, - }; - - browseridManager._fxaService = fxa; - browseridManager._signedInUser = null; - yield browseridManager.initializeWithCurrentIdentity(); - yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise, - "expecting rejection due to hawk error"); -} - - -function getTimestamp(hawkAuthHeader) { - return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; -} - -function getTimestampDelta(hawkAuthHeader, now=Date.now()) { - return Math.abs(getTimestamp(hawkAuthHeader) - now); -} - diff --git a/services/sync/tests/unit/test_errorhandler.js b/services/sync/tests/unit/test_errorhandler.js index c087acc9f..25d79002c 100644 --- a/services/sync/tests/unit/test_errorhandler.js +++ b/services/sync/tests/unit/test_errorhandler.js @@ -486,8 +486,6 @@ add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() { do_check_false(errorHandler.shouldReportError()); }); -// XXX - how to arrange for 'Service.identity.basicPassword = null;' in -// an fxaccounts environment? add_task(function test_login_syncAndReportErrors_non_network_error() { // Test non-network errors are reported // when calling syncAndReportErrors @@ -536,8 +534,6 @@ add_identity_test(this, function test_sync_syncAndReportErrors_non_network_error yield deferred.promise; }); -// XXX - how to arrange for 'Service.identity.basicPassword = null;' in -// an fxaccounts environment? add_task(function test_login_syncAndReportErrors_prolonged_non_network_error() { // Test prolonged, non-network errors are // reported when calling syncAndReportErrors. diff --git a/services/sync/tests/unit/test_load_modules.js b/services/sync/tests/unit/test_load_modules.js index 4f561bae6..8e3fcf1f3 100644 --- a/services/sync/tests/unit/test_load_modules.js +++ b/services/sync/tests/unit/test_load_modules.js @@ -37,7 +37,6 @@ const testingModules = [ "fakeservices.js", "rotaryengine.js", "utils.js", - "fxa_utils.js", ]; function run_test() { diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index 28424129b..2f9884751 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -50,7 +50,6 @@ skip-if = os == "win" || os == "android" [test_syncstoragerequest.js] # Generic Sync types. -[test_browserid_identity.js] [test_collection_inc_get.js] [test_collections_recovery.js] [test_identity_manager.js] diff --git a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm deleted file mode 100644 index f5daa14be..000000000 --- a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm +++ /dev/null @@ -1,96 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "Authentication", -]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/FxAccounts.jsm"); -Cu.import("resource://gre/modules/FxAccountsClient.jsm"); -Cu.import("resource://services-common/async.js"); -Cu.import("resource://services-sync/main.js"); -Cu.import("resource://tps/logger.jsm"); - - -/** - * Helper object for Firefox Accounts authentication - */ -var Authentication = { - - /** - * Check if an user has been logged in - */ - get isLoggedIn() { - return !!this.getSignedInUser(); - }, - - /** - * Wrapper to retrieve the currently signed in user - * - * @returns Information about the currently signed in user - */ - getSignedInUser: function getSignedInUser() { - let cb = Async.makeSpinningCallback(); - - fxAccounts.getSignedInUser().then(user => { - cb(null, user); - }, error => { - cb(error); - }) - - try { - return cb.wait(); - } catch (error) { - Logger.logError("getSignedInUser() failed with: " + JSON.stringify(error)); - throw error; - } - }, - - /** - * Wrapper to synchronize the login of a user - * - * @param account - * Account information of the user to login - * @param account.username - * The username for the account (utf8) - * @param account.password - * The user's password - */ - signIn: function signIn(account) { - let cb = Async.makeSpinningCallback(); - - Logger.AssertTrue(account["username"], "Username has been found"); - Logger.AssertTrue(account["password"], "Password has been found"); - - Logger.logInfo("Login user: " + account["username"] + '\n'); - - let client = new FxAccountsClient(); - client.signIn(account["username"], account["password"], true).then(credentials => { - return fxAccounts.setSignedInUser(credentials); - }).then(() => { - cb(null, true); - }, error => { - cb(error, false); - }); - - try { - cb.wait(); - - if (Weave.Status.login !== Weave.LOGIN_SUCCEEDED) { - Logger.logInfo("Logging into Weave."); - Weave.Service.login(); - Logger.AssertEqual(Weave.Status.login, Weave.LOGIN_SUCCEEDED, - "Weave logged in"); - } - - return true; - } catch (error) { - throw new Error("signIn() failed with: " + error.message); - } - } -}; diff --git a/services/sync/tps/extensions/tps/resource/tps.jsm b/services/sync/tps/extensions/tps/resource/tps.jsm index ca3e4d578..c94112a6f 100644 --- a/services/sync/tps/extensions/tps/resource/tps.jsm +++ b/services/sync/tps/extensions/tps/resource/tps.jsm @@ -74,9 +74,7 @@ const ACTIONS = [ ACTION_VERIFY_NOT, ]; -const OBSERVER_TOPICS = ["fxaccounts:onlogin", - "fxaccounts:onlogout", - "private-browsing", +const OBSERVER_TOPICS = ["private-browsing", "quit-application-requested", "sessionstore-windows-restored", "weave:engine:start-tracking", diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index bf1534c12..35680ca43 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -258,21 +258,6 @@ user_pref('toolkit.telemetry.server', 'https://%(server)s/telemetry-dummy/'); user_pref('toolkit.telemetry.test.pref1', true); user_pref('toolkit.telemetry.test.pref2', false); -// We don't want to hit the real Firefox Accounts server for tests. We don't -// actually need a functioning FxA server, so just set it to something that -// resolves and accepts requests, even if they all fail. -user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/'); - -// Ditto for all the other Firefox accounts URIs used for about:accounts et al.: -user_pref("identity.fxaccounts.remote.signup.uri", "https://%(server)s/fxa-signup"); -user_pref("identity.fxaccounts.remote.force_auth.uri", "https://%(server)s/fxa-force-auth"); -user_pref("identity.fxaccounts.remote.signin.uri", "https://%(server)s/fxa-signin"); -user_pref("identity.fxaccounts.settings.uri", "https://%(server)s/fxa-settings"); -user_pref('identity.fxaccounts.remote.webchannel.uri', 'https://%(server)s/'); - -// We don't want browser tests to perform FxA device registration. -user_pref('identity.fxaccounts.skipDeviceRegistration', true); - // Increase the APZ content response timeout in tests to 1 minute. // This is to accommodate the fact that test environments tends to be slower // than production environments (with the b2g emulator being the slowest of them diff --git a/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json b/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json index 211e98e05..8525931bd 100644 --- a/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json +++ b/testing/runtimes/mochitest-browser-chrome-e10s.runtimes.json @@ -76,7 +76,6 @@ "browser/base/content/test/general/browser_fullscreen-window-open.js": 4312, "browser/base/content/test/general/browser_fxa_oauth.js": 5410, "browser/base/content/test/general/browser_fxa_web_channel.js": 4727, - "browser/base/content/test/general/browser_fxaccounts.js": 2909, "browser/base/content/test/general/browser_getshortcutoruri.js": 3083, "browser/base/content/test/general/browser_identity_UI.js": 20930, "browser/base/content/test/general/browser_insecureLoginForms.js": 4482, diff --git a/testing/runtimes/mochitest-browser-chrome.runtimes.json b/testing/runtimes/mochitest-browser-chrome.runtimes.json index 73b2437a2..73efc2b26 100644 --- a/testing/runtimes/mochitest-browser-chrome.runtimes.json +++ b/testing/runtimes/mochitest-browser-chrome.runtimes.json @@ -82,7 +82,6 @@ "browser/base/content/test/general/browser_fullscreen-window-open.js": 2830, "browser/base/content/test/general/browser_fxa_oauth.js": 4120, "browser/base/content/test/general/browser_fxa_web_channel.js": 3535, - "browser/base/content/test/general/browser_fxaccounts.js": 3175, "browser/base/content/test/general/browser_getshortcutoruri.js": 3344, "browser/base/content/test/general/browser_identity_UI.js": 19308, "browser/base/content/test/general/browser_insecureLoginForms.js": 3538, diff --git a/testing/talos/talos/config.py b/testing/talos/talos/config.py index 59b6123d3..828e68a15 100644 --- a/testing/talos/talos/config.py +++ b/testing/talos/talos/config.py @@ -153,7 +153,6 @@ DEFAULTS = dict( 'browser.contentHandlers.types.3.uri': 'http://127.0.0.1/rss?url=%s', 'browser.contentHandlers.types.4.uri': 'http://127.0.0.1/rss?url=%s', 'browser.contentHandlers.types.5.uri': 'http://127.0.0.1/rss?url=%s', - 'identity.fxaccounts.auth.uri': 'https://127.0.0.1/fxa-dummy/', 'datareporting.healthreport.about.reportUrl': 'http://127.0.0.1/abouthealthreport/', 'datareporting.healthreport.documentServerURI': @@ -176,7 +175,6 @@ DEFAULTS = dict( 'devtools.debugger.remote-enabled': False, 'devtools.theme': "light", 'devtools.timeline.enabled': False, - 'identity.fxaccounts.migrateToDevEdition': False, 'media.libavcodec.allow-obsolete': True } ) diff --git a/toolkit/components/passwordmgr/LoginHelper.jsm b/toolkit/components/passwordmgr/LoginHelper.jsm index e0c4d872b..c6cd40915 100644 --- a/toolkit/components/passwordmgr/LoginHelper.jsm +++ b/toolkit/components/passwordmgr/LoginHelper.jsm @@ -202,7 +202,7 @@ this.LoginHelper = { return true; } } catch (ex) { - // newURI will throw for some values e.g. chrome://FirefoxAccounts + // newURI will throw for some values return false; } } @@ -406,7 +406,7 @@ this.LoginHelper = { try { preferredOriginScheme = Services.io.newURI(preferredOrigin, null, null).scheme; } catch (ex) { - // Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts + // Handle strings that aren't valid URIs } } @@ -457,7 +457,7 @@ this.LoginHelper = { return loginURI.scheme == preferredOriginScheme; } catch (ex) { - // Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts) + // Some URLs aren't valid nsIURI log.debug("dedupeLogins/shouldReplaceExisting: Error comparing schemes:", existingLogin.hostname, login.hostname, "preferredOrigin:", preferredOrigin, ex); diff --git a/toolkit/components/passwordmgr/storage-mozStorage.js b/toolkit/components/passwordmgr/storage-mozStorage.js index 7fc9e57fd..9da244f7a 100644 --- a/toolkit/components/passwordmgr/storage-mozStorage.js +++ b/toolkit/components/passwordmgr/storage-mozStorage.js @@ -471,7 +471,7 @@ LoginManagerStorage_mozStorage.prototype = { params["http" + field] = "http://" + valueURI.hostPort; } } catch (ex) { - // newURI will throw for some values (e.g. chrome://FirefoxAccounts) + // newURI will throw for some values // but those URLs wouldn't support upgrades anyways. } break; diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm deleted file mode 100644 index 1d2ed0439..000000000 --- a/toolkit/identity/FirefoxAccounts.jsm +++ /dev/null @@ -1,313 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["FirefoxAccounts"]; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/identity/LogUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "objectCopy", - "resource://gre/modules/identity/IdentityUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject", - "resource://gre/modules/identity/IdentityUtils.jsm"); - -// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO", -// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by -// default. -const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; -try { - this.LOG_LEVEL = - Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL); -} catch (e) { - this.LOG_LEVEL = Log.Level.Error; -} - -var log = Log.repository.getLogger("Identity.FxAccounts"); -log.level = LOG_LEVEL; -log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); - -log.warn("The FxAccountsManager has been removed."); -var FxAccountsManager = null; -var ONVERIFIED_NOTIFICATION = null; -var ONLOGIN_NOTIFICATION = null; -var ONLOGOUT_NOTIFICATION = null; - -function FxAccountsService() { - Services.obs.addObserver(this, "quit-application-granted", false); - if (ONVERIFIED_NOTIFICATION) { - Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false); - Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false); - Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); - } - - // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm - this.RP = this; - - this._rpFlows = new Map(); - - // Enable us to mock FxAccountsManager service in testing - this.fxAccountsManager = FxAccountsManager; -} - -FxAccountsService.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), - - observe: function observe(aSubject, aTopic, aData) { - switch (aTopic) { - case null: - // Guard against matching null ON*_NOTIFICATION - break; - case ONVERIFIED_NOTIFICATION: - log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s"); - for (let [rpId,] of this._rpFlows) { - this.request(rpId); - } - break; - case ONLOGIN_NOTIFICATION: - log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired"); - for (let [rpId,] of this._rpFlows) { - this.request(rpId); - } - break; - case ONLOGOUT_NOTIFICATION: - log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired"); - for (let [rpId,] of this._rpFlows) { - this.doLogout(rpId); - } - break; - case "quit-application-granted": - Services.obs.removeObserver(this, "quit-application-granted"); - if (ONVERIFIED_NOTIFICATION) { - Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION); - Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION); - Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION); - } - break; - } - }, - - cleanupRPRequest: function(aRp) { - aRp.pendingRequest = false; - this._rpFlows.set(aRp.id, aRp); - }, - - /** - * Register a listener for a given windowID as a result of a call to - * navigator.id.watch(). - * - * @param aRPCaller - * (Object) an object that represents the caller document, and - * is expected to have properties: - * - id (unique, e.g. uuid) - * - origin (string) - * - * and a bunch of callbacks - * - doReady() - * - doLogin() - * - doLogout() - * - doError() - * - doCancel() - * - */ - watch: function watch(aRpCaller) { - this._rpFlows.set(aRpCaller.id, aRpCaller); - log.debug("watch: " + aRpCaller.id); - log.debug("Current rp flows: " + this._rpFlows.size); - - // Log the user in, if possible, and then call ready(). - let runnable = { - run: () => { - this.fxAccountsManager.getAssertion(aRpCaller.audience, - aRpCaller.principal, - { silent:true }).then( - data => { - if (data) { - this.doLogin(aRpCaller.id, data); - } else { - this.doLogout(aRpCaller.id); - } - this.doReady(aRpCaller.id); - }, - error => { - log.error("get silent assertion failed: " + JSON.stringify(error)); - this.doError(aRpCaller.id, error); - } - ); - } - }; - Services.tm.currentThread.dispatch(runnable, - Ci.nsIThread.DISPATCH_NORMAL); - }, - - /** - * Delete the flow when the screen is unloaded - */ - unwatch: function(aRpCallerId, aTargetMM) { - log.debug("unwatching: " + aRpCallerId); - this._rpFlows.delete(aRpCallerId); - }, - - /** - * Initiate a login with user interaction as a result of a call to - * navigator.id.request(). - * - * @param aRPId - * (integer) the id of the doc object obtained in .watch() - * - * @param aOptions - * (Object) options including privacyPolicy, termsOfService - */ - request: function request(aRPId, aOptions) { - aOptions = aOptions || {}; - let rp = this._rpFlows.get(aRPId); - if (!rp) { - log.error("request() called before watch()"); - return; - } - - // We check if we already have a pending request for this RP and in that - // case we just bail out. We don't want duplicated onlogin or oncancel - // events. - if (rp.pendingRequest) { - log.debug("request() already called"); - return; - } - - // Otherwise, we set the RP flow with the pending request flag. - rp.pendingRequest = true; - this._rpFlows.set(rp.id, rp); - - let options = makeMessageObject(rp); - objectCopy(aOptions, options); - - log.debug("get assertion for " + rp.audience); - - this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options) - .then( - data => { - log.debug("got assertion for " + rp.audience + ": " + data); - this.doLogin(aRPId, data); - }, - error => { - log.debug("get assertion failed: " + JSON.stringify(error)); - // Cancellation is passed through an error channel; here we reroute. - if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) || - (error.details == "DIALOG_CLOSED_BY_USER")) { - return this.doCancel(aRPId); - } - this.doError(aRPId, error); - } - ) - .then( - () => { - this.cleanupRPRequest(rp); - } - ) - .catch( - () => { - this.cleanupRPRequest(rp); - } - ); - }, - - /** - * Invoked when a user wishes to logout of a site (for instance, when clicking - * on an in-content logout button). - * - * @param aRpCallerId - * (integer) the id of the doc object obtained in .watch() - * - */ - logout: function logout(aRpCallerId) { - // XXX Bug 945363 - Resolve the SSO story for FXA and implement - // logout accordingly. - // - // For now, it makes no sense to logout from a specific RP in - // Firefox Accounts, so just directly call the logout callback. - if (!this._rpFlows.has(aRpCallerId)) { - log.error("logout() called before watch()"); - return; - } - - // Call logout() on the next tick - let runnable = { - run: () => { - this.fxAccountsManager.signOut().then(() => { - this.doLogout(aRpCallerId); - }); - } - }; - Services.tm.currentThread.dispatch(runnable, - Ci.nsIThread.DISPATCH_NORMAL); - }, - - childProcessShutdown: function childProcessShutdown(messageManager) { - for (let [key,] of this._rpFlows) { - if (this._rpFlows.get(key)._mm === messageManager) { - this._rpFlows.delete(key); - } - } - }, - - doLogin: function doLogin(aRpCallerId, aAssertion) { - let rp = this._rpFlows.get(aRpCallerId); - if (!rp) { - log.warn("doLogin found no rp to go with callerId " + aRpCallerId); - return; - } - - rp.doLogin(aAssertion); - }, - - doLogout: function doLogout(aRpCallerId) { - let rp = this._rpFlows.get(aRpCallerId); - if (!rp) { - log.warn("doLogout found no rp to go with callerId " + aRpCallerId); - return; - } - - rp.doLogout(); - }, - - doReady: function doReady(aRpCallerId) { - let rp = this._rpFlows.get(aRpCallerId); - if (!rp) { - log.warn("doReady found no rp to go with callerId " + aRpCallerId); - return; - } - - rp.doReady(); - }, - - doCancel: function doCancel(aRpCallerId) { - let rp = this._rpFlows.get(aRpCallerId); - if (!rp) { - log.warn("doCancel found no rp to go with callerId " + aRpCallerId); - return; - } - - rp.doCancel(); - }, - - doError: function doError(aRpCallerId, aError) { - let rp = this._rpFlows.get(aRpCallerId); - if (!rp) { - log.warn("doError found no rp to go with callerId " + aRpCallerId); - return; - } - - rp.doError(aError); - } -}; - -this.FirefoxAccounts = new FxAccountsService(); - diff --git a/toolkit/identity/moz.build b/toolkit/identity/moz.build index 4c0dc8190..ba9697bd6 100644 --- a/toolkit/identity/moz.build +++ b/toolkit/identity/moz.build @@ -29,8 +29,4 @@ EXTRA_JS_MODULES.identity += [ 'Sandbox.jsm', ] -EXTRA_PP_JS_MODULES.identity += [ - 'FirefoxAccounts.jsm', -] - FINAL_LIBRARY = 'xul' diff --git a/toolkit/identity/tests/unit/head_identity.js b/toolkit/identity/tests/unit/head_identity.js index a266e7aee..c63261b95 100644 --- a/toolkit/identity/tests/unit/head_identity.js +++ b/toolkit/identity/tests/unit/head_identity.js @@ -239,18 +239,10 @@ try { } catch (noPref) {} Services.prefs.setBoolPref("toolkit.identity.debug", true); -// Switch on firefox accounts -var initialPrefFXAValue = false; -try { - initialPrefFXAValue = Services.prefs.getBoolPref("identity.fxaccounts.enabled"); -} catch (noPref) {} -Services.prefs.setBoolPref("identity.fxaccounts.enabled", true); - // after execution, restore prefs do_register_cleanup(function() { log("restoring prefs to their initial values"); Services.prefs.setBoolPref("toolkit.identity.debug", initialPrefDebugValue); - Services.prefs.setBoolPref("identity.fxaccounts.enabled", initialPrefFXAValue); }); diff --git a/toolkit/identity/tests/unit/test_firefox_accounts.js b/toolkit/identity/tests/unit/test_firefox_accounts.js deleted file mode 100644 index c0c63deb6..000000000 --- a/toolkit/identity/tests/unit/test_firefox_accounts.js +++ /dev/null @@ -1,270 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/DOMIdentity.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts", - "resource://gre/modules/identity/FirefoxAccounts.jsm"); - -// Make the profile dir available; this is necessary so that -// services/fxaccounts/FxAccounts.jsm can read and write its signed-in user -// data. -do_get_profile(); - -function MockFXAManager() { - this.signedInUser = true; -} -MockFXAManager.prototype = { - getAssertion: function(audience) { - let result = this.signedInUser ? TEST_ASSERTION : null; - return Promise.resolve(result); - }, - - signOut: function() { - this.signedInUser = false; - return Promise.resolve(null); - }, - - signIn: function(user) { - this.signedInUser = user; - return Promise.resolve(user); - }, -} - -var originalManager = FirefoxAccounts.fxAccountsManager; -FirefoxAccounts.fxAccountsManager = new MockFXAManager(); -do_register_cleanup(() => { - log("restoring fxaccountsmanager"); - FirefoxAccounts.fxAccountsManager = originalManager; -}); - -function withNobodySignedIn() { - return FirefoxAccounts.fxAccountsManager.signOut(); -} - -function withSomebodySignedIn() { - return FirefoxAccounts.fxAccountsManager.signIn('Pertelote'); -} - -function test_overall() { - do_check_neq(FirefoxAccounts, null); - run_next_test(); -} - -function test_mock() { - do_test_pending(); - - withSomebodySignedIn().then(() => { - FirefoxAccounts.fxAccountsManager.getAssertion().then(assertion => { - do_check_eq(assertion, TEST_ASSERTION); - do_test_finished(); - run_next_test(); - }); - }); -} - -function test_watch_signed_in() { - do_test_pending(); - - let received = []; - - let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) { - received.push([method, data]); - - if (method == "ready") { - // confirm that we were signed in and then ready was called - do_check_eq(received.length, 2); - do_check_eq(received[0][0], "login"); - do_check_eq(received[0][1], TEST_ASSERTION); - do_check_eq(received[1][0], "ready"); - do_test_finished(); - run_next_test(); - } - }); - - withSomebodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); -} - -function test_watch_signed_out() { - do_test_pending(); - - let received = []; - - let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) { - received.push(method); - - if (method == "ready") { - // confirm that we were signed out and then ready was called - do_check_eq(received.length, 2); - do_check_eq(received[0], "logout"); - do_check_eq(received[1], "ready"); - - do_test_finished(); - run_next_test(); - } - }); - - withNobodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); -} - -function test_request() { - do_test_pending(); - - let received = []; - - let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) { - received.push([method, data]); - - // On watch(), we are signed out. Then we call request(). - if (received.length === 2) { - do_check_eq(received[0][0], "logout"); - do_check_eq(received[1][0], "ready"); - - // Pretend request() showed ux and the user signed in - withSomebodySignedIn().then(() => { - FirefoxAccounts.RP.request(mockedRP.id); - }); - } - - if (received.length === 3) { - do_check_eq(received[2][0], "login"); - do_check_eq(received[2][1], TEST_ASSERTION); - - do_test_finished(); - run_next_test(); - } - }); - - // First, call watch() with nobody signed in - withNobodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); -} - -function test_logout() { - do_test_pending(); - - let received = []; - - let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) { - received.push(method); - - // At first, watch() signs us in automatically. Then we sign out. - if (received.length === 2) { - do_check_eq(received[0], "login"); - do_check_eq(received[1], "ready"); - - FirefoxAccounts.RP.logout(mockedRP.id); - } - - if (received.length === 3) { - do_check_eq(received[2], "logout"); - do_test_finished(); - run_next_test(); - } - }); - - // First, call watch() - withSomebodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); -} - -function test_error() { - do_test_pending(); - - let received = []; - - // Mock the fxAccountsManager so that getAssertion rejects its promise and - // triggers our onerror handler. (This is the method that's used internally - // by FirefoxAccounts.RP.request().) - let originalGetAssertion = FirefoxAccounts.fxAccountsManager.getAssertion; - FirefoxAccounts.fxAccountsManager.getAssertion = function(audience) { - return Promise.reject(new Error("barf!")); - }; - - let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, message) { - // We will immediately receive an error, due to watch()'s attempt - // to getAssertion(). - do_check_eq(method, "error"); - do_check_true(/barf/.test(message)); - - // Put things back the way they were - FirefoxAccounts.fxAccountsManager.getAssertion = originalGetAssertion; - - do_test_finished(); - run_next_test(); - }); - - // First, call watch() - withSomebodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); -} - -function test_child_process_shutdown() { - do_test_pending(); - let rpCount = FirefoxAccounts.RP._rpFlows.size; - - makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => { - // Last of all, the shutdown observer message will be fired. - // This takes place after the RP has a chance to delete flows - // and clean up. - do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount); - do_test_finished(); - run_next_test(); - }); - - let mockedRP = mock_fxa_rp(null, TEST_URL, (method) => { - // We should enter this function for 'ready' and 'child-process-shutdown'. - // After we have a chance to do our thing, the shutdown observer message - // will fire and be caught by the function above. - do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount + 1); - switch (method) { - case "ready": - DOMIdentity._childProcessShutdown("my message manager"); - break; - - case "child-process-shutdown": - // We have to call this explicitly because there's no real - // dom window here. - FirefoxAccounts.RP.childProcessShutdown(mockedRP._mm); - break; - - default: - break; - } - }); - - mockedRP._mm = "my message manager"; - withSomebodySignedIn().then(() => { - FirefoxAccounts.RP.watch(mockedRP); - }); - - // fake a dom window context - DOMIdentity.newContext(mockedRP, mockedRP._mm); -} - -var TESTS = [ - test_overall, - test_mock, - test_watch_signed_in, - test_watch_signed_out, - test_request, - test_logout, - test_error, - test_child_process_shutdown, -]; - -TESTS.forEach(add_test); - -function run_test() { - run_next_test(); -} diff --git a/toolkit/identity/tests/unit/xpcshell.ini b/toolkit/identity/tests/unit/xpcshell.ini index 38b37402c..309e4791c 100644 --- a/toolkit/identity/tests/unit/xpcshell.ini +++ b/toolkit/identity/tests/unit/xpcshell.ini @@ -9,7 +9,6 @@ support-files = # Test load modules first so syntax failures are caught early. [test_load_modules.js] [test_minimalidentity.js] -[test_firefox_accounts.js] [test_identity_utils.js] [test_log_utils.js] diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js index 313af2d71..00a48f359 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js @@ -48,8 +48,7 @@ const SCRIPTS = [ "browser/base/content/browser-tabsintitlebar.js", "browser/base/content/browser-thumbnails.js", "browser/base/content/browser-trackingprotection.js", - "browser/base/content/browser-data-submission-info-bar.js", - "browser/base/content/browser-fxaccounts.js" + "browser/base/content/browser-data-submission-info-bar.js" ]; module.exports = function(context) { diff --git a/tools/lint/eslint/modules.json b/tools/lint/eslint/modules.json index 767b43db0..30b5d3968 100644 --- a/tools/lint/eslint/modules.json +++ b/tools/lint/eslint/modules.json @@ -82,15 +82,6 @@ "forms.jsm": ["FormData"], "frame.js": ["Collector", "Runner", "events", "runTestFile", "log", "timers", "persisted", "shutdownApplication"], "FrameScriptManager.jsm": ["getNewLoaderID"], - "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"], - "fxaccounts.jsm": ["Authentication"], - "FxAccounts.jsm": ["fxAccounts", "FxAccounts"], - "FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"], - "FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"], - "FxAccountsPush.js": ["FxAccountsPushService"], - "FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"], - "FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"], - "FxaMigrator.jsm": ["fxaMigrator"], "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"], "gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"], "Geometry.jsm": ["Point", "Rect"], @@ -228,7 +219,7 @@ "UpdateTelemetry.jsm": ["AUSTLMY"], "userapi.js": ["UserAPI10Client"], "util.js": ["getChromeWindow", "XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"], - "utils.js": ["applicationName", "assert", "Copy", "getBrowserObject", "getChromeWindow", "getWindows", "getWindowByTitle", "getWindowByType", "getWindowId", "getMethodInWindows", "getPreference", "saveDataURL", "setPreference", "sleep", "startTimer", "stopTimer", "takeScreenshot", "unwrapNode", "waitFor", "btoa", "encryptPayload", "isConfiguredWithLegacyIdentity", "ensureLegacyIdentityManager", "setBasicCredentials", "makeIdentityConfig", "makeFxAccountsInternalMock", "configureFxAccountIdentity", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "Promise", "add_identity_test", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils"], + "utils.js": ["applicationName", "assert", "Copy", "getBrowserObject", "getChromeWindow", "getWindows", "getWindowByTitle", "getWindowByType", "getWindowId", "getMethodInWindows", "getPreference", "saveDataURL", "setPreference", "sleep", "startTimer", "stopTimer", "takeScreenshot", "unwrapNode", "waitFor", "btoa", "encryptPayload", "isConfiguredWithLegacyIdentity", "ensureLegacyIdentityManager", "setBasicCredentials", "makeIdentityConfig", "configureIdentity", "SyncTestingInfrastructure", "waitForZeroTimer", "Promise", "add_identity_test", "MockFxaStorageManager", "AccountState", "sumHistogram", "CommonUtils", "CryptoUtils", "TestingUtils"], "Utils.jsm": ["Utils", "Logger", "PivotContext", "PrefCache", "SettingCache"], "VariablesView.jsm": ["VariablesView", "escapeHTML"], "VariablesViewController.jsm": ["VariablesViewController", "StackFrameUtils"], |