diff options
author | Ascrod <32915892+Ascrod@users.noreply.github.com> | 2019-04-18 20:35:10 -0400 |
---|---|---|
committer | Ascrod <32915892+Ascrod@users.noreply.github.com> | 2019-04-18 20:35:10 -0400 |
commit | af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec (patch) | |
tree | 4aac6c4383fb9e279fccb13c65a4e44595fd4cf6 | |
parent | 40fc72376411587e7bf9985fb9545eca1c9aaa8e (diff) | |
parent | 51722cd4fecb5c8c79a302f2771cad71535df5ea (diff) | |
download | UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.gz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.lz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.xz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.zip |
Merge branch 'master' into default-pref
569 files changed, 1179 insertions, 62592 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/aboutaccounts/aboutaccounts.css b/application/basilisk/base/content/aboutaccounts/aboutaccounts.css deleted file mode 100644 index a2c5cb8f0..000000000 --- a/application/basilisk/base/content/aboutaccounts/aboutaccounts.css +++ /dev/null @@ -1,24 +0,0 @@ -html, body { - height: 100%; -} - -#remote { - width: 100%; - height: 100%; - border: 0; - display: none; -} - -#networkError, #manage, #intro, #stage, #configError { - display: none; -} - -#oldsync { - background: none; - border: 0; - color: #0095dd; -} - -#oldsync:focus { - outline: 1px dotted #0095dd; -} diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.js b/application/basilisk/base/content/aboutaccounts/aboutaccounts.js deleted file mode 100644 index f03029b1d..000000000 --- a/application/basilisk/base/content/aboutaccounts/aboutaccounts.js +++ /dev/null @@ -1,538 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/FxAccounts.jsm"); - -var fxAccountsCommon = {}; -Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); - -// for master-password utilities -Cu.import("resource://services-sync/util.js"); - -const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; -const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog"; - -const ACTION_URL_PARAM = "action"; - -const OBSERVER_TOPICS = [ - fxAccountsCommon.ONVERIFIED_NOTIFICATION, - fxAccountsCommon.ONLOGOUT_NOTIFICATION, -]; - -function log(msg) { - // dump("FXA: " + msg + "\n"); -} - -function error(msg) { - console.log("Firefox Account Error: " + msg + "\n"); -} - -function getPreviousAccountNameHash() { - try { - return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; - } catch (_) { - return ""; - } -} - -function setPreviousAccountNameHash(acctName) { - let string = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - string.data = sha256(acctName); - Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); -} - -function needRelinkWarning(acctName) { - let prevAcctHash = getPreviousAccountNameHash(); - return prevAcctHash && prevAcctHash != sha256(acctName); -} - -// Given a string, returns the SHA265 hash in base64 -function 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); -} - -function 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; - let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags, - continueLabel, null, null, null, - {}); - return pressed == 0; // 0 is the "continue" button -} - -// 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... -function shouldAllowRelink(acctName) { - return !needRelinkWarning(acctName) || promptForRelink(acctName); -} - -function updateDisplayedEmail(user) { - let emailDiv = document.getElementById("email"); - if (emailDiv && user) { - emailDiv.textContent = user.email; - } -} - -var wrapper = { - iframe: null, - - init: function (url, urlParams) { - // If a master-password is enabled, we want to encourage the user to - // unlock it. Things still work if not, but the user will probably need - // to re-auth next startup (in which case we will get here again and - // re-prompt) - Utils.ensureMPUnlocked(); - - 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); - iframe.addEventListener("load", this); - - // 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; - } - this.url = url; - // Set the iframe's location with loadURI/LOAD_FLAGS_REPLACE_HISTORY to - // avoid having a new history entry being added. REPLACE_HISTORY is used - // to replace the current entry, which is `about:blank`. - let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation); - webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, null, null, null); - }, - - retry: function () { - 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); - setErrorPage("networkError"); - } - }, - - onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { - if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { - aRequest.cancel(Components.results.NS_BINDING_ABORTED); - setErrorPage("networkError"); - } - }, - - onProgressChange: function() {}, - onStatusChange: function() {}, - onSecurityChange: function() {}, - }, - - handleEvent: function (evt) { - switch (evt.type) { - case "load": - this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this); - this.iframe.removeEventListener("load", this); - break; - case "FirefoxAccountsCommand": - this.handleRemoteCommand(evt); - break; - } - }, - - /** - * onLogin handler receives user credentials from the jelly after a - * sucessful login and stores it in the fxaccounts service - * - * @param accountData the user's account data and credentials - */ - onLogin: function (accountData) { - log("Received: 'login'. Data:" + JSON.stringify(accountData)); - - if (accountData.customizeSync) { - Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); - } - delete accountData.customizeSync; - // sessionTokenContext is erroneously sent by the content server. - // https://github.com/mozilla/fxa-content-server/issues/2766 - // To avoid having the FxA storage manager not knowing what to do with - // it we delete it here. - delete accountData.sessionTokenContext; - - // We need to confirm a relink - see shouldAllowRelink for more - let newAccountEmail = accountData.email; - // The hosted code may have already checked for the relink situation - // by sending the can_link_account command. If it did, then - // it will indicate we don't need to ask twice. - if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) { - // we need to tell the page we successfully received the message, but - // then bail without telling fxAccounts - this.injectData("message", { status: "login" }); - // after a successful login we return to preferences - openPrefs(); - return; - } - delete accountData.verifiedCanLinkAccount; - - // Remember who it was so we can log out next time. - setPreviousAccountNameHash(newAccountEmail); - - // 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; - xps.whenLoaded().then(() => { - updateDisplayedEmail(accountData); - return fxAccounts.setSignedInUser(accountData); - }).then(() => { - // If the user data is verified, we want it to immediately look like - // they are signed in without waiting for messages to bounce around. - if (accountData.verified) { - openPrefs(); - } - this.injectData("message", { status: "login" }); - // until we sort out a better UX, just leave the jelly page in place. - // If the account email is not yet verified, it will tell the user to - // go check their email, but then it will *not* change state after - // the verification completes (the browser will begin syncing, but - // won't notify the user). If the email has already been verified, - // the jelly will say "Welcome! You are successfully signed in as - // EMAIL", but it won't then say "syncing started". - }, (err) => this.injectData("message", { status: "error", error: err }) - ); - }, - - onCanLinkAccount: function(accountData) { - // We need to confirm a relink - see shouldAllowRelink for more - let ok = shouldAllowRelink(accountData.email); - this.injectData("message", { status: "can_link_account", data: { ok: ok } }); - }, - - /** - * onSignOut handler erases the current user's session from the fxaccounts service - */ - onSignOut: function () { - log("Received: 'sign_out'."); - - fxAccounts.signOut().then( - () => this.injectData("message", { status: "sign_out" }), - (err) => this.injectData("message", { status: "error", error: err }) - ); - }, - - handleRemoteCommand: function (evt) { - log('command: ' + evt.detail.command); - let data = evt.detail.data; - - switch (evt.detail.command) { - case "login": - this.onLogin(data); - break; - case "can_link_account": - this.onCanLinkAccount(data); - break; - case "sign_out": - this.onSignOut(data); - break; - default: - log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); - break; - } - }, - - injectData: function (type, content) { - return fxAccounts.promiseAccountsSignUpURI().then(authUrl => { - let data = { - type: type, - content: content - }; - this.iframe.contentWindow.postMessage(data, authUrl); - }) - .catch(e => { - console.log("Failed to inject data", e); - setErrorPage("configError"); - }); - }, -}; - - -// Button onclick handlers -function handleOldSync() { - let chromeWin = window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) - .QueryInterface(Ci.nsIDOMChromeWindow); - let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; - chromeWin.switchToTabHavingURI(url, true); -} - -function getStarted() { - show("remote"); -} - -function retry() { - show("remote"); - wrapper.retry(); -} - -function openPrefs() { - // Bug 1199303 calls for this tab to always be replaced with Preferences - // rather than it opening in a different tab. - window.location = "about:preferences#sync"; -} - -function init() { - fxAccounts.getSignedInUser().then(user => { - // tests in particular might cause the window to start closing before - // getSignedInUser has returned. - if (window.closed) { - return Promise.resolve(); - } - - 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 "signin": - if (user) { - // asking to sign-in when already signed in just shows manage. - show("stage", "manage"); - } else { - return fxAccounts.promiseAccountsSignInURI().then(url => { - show("remote"); - wrapper.init(url, urlParams); - }); - } - break; - case "signup": - if (user) { - // asking to sign-up when already signed in just shows manage. - show("stage", "manage"); - } else { - return fxAccounts.promiseAccountsSignUpURI().then(url => { - show("remote"); - wrapper.init(url, urlParams); - }); - } - break; - case "reauth": - // ideally we would only show this when we know the user is in a - // "must reauthenticate" state - but we don't. - // As the email address will be included in the URL returned from - // promiseAccountsForceSigninURI, just always show it. - return fxAccounts.promiseAccountsForceSigninURI().then(url => { - show("remote"); - wrapper.init(url, urlParams); - }); - default: - // No action specified. - if (user) { - show("stage", "manage"); - } else { - // Attempt a migration if enabled or show the introductory page - // otherwise. - return migrateToDevEdition(urlParams).then(migrated => { - if (!migrated) { - show("stage", "intro"); - // load the remote frame in the background - return fxAccounts.promiseAccountsSignUpURI().then(uri => - wrapper.init(uri, urlParams)); - } - return Promise.resolve(); - }); - } - break; - } - return Promise.resolve(); - }).catch(err => { - console.log("Configuration or sign in error", err); - setErrorPage("configError"); - }); -} - -function setErrorPage(errorType) { - show("stage", errorType); -} - -// Causes the "top-level" element with |id| to be shown - all other top-level -// elements are hidden. Optionally, ensures that only 1 "second-level" element -// inside the top-level one is shown. -function show(id, childId) { - // top-level items are either <div> or <iframe> - let allTop = document.querySelectorAll("body > div, iframe"); - for (let elt of allTop) { - if (elt.getAttribute("id") == id) { - elt.style.display = 'block'; - } else { - elt.style.display = 'none'; - } - } - if (childId) { - // child items are all <div> - let allSecond = document.querySelectorAll("#" + id + " > div"); - for (let elt of allSecond) { - if (elt.getAttribute("id") == childId) { - elt.style.display = 'block'; - } else { - elt.style.display = 'none'; - } - } - } -} - -// Migrate sync data from the default profile to the dev-edition profile. -// Returns a promise of a true value if migration succeeded, or false if it -// failed. -function migrateToDevEdition(urlParams) { - let defaultProfilePath; - try { - defaultProfilePath = window.getDefaultProfilePath(); - } catch (e) {} // no default profile. - let migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition", false); - - if (!defaultProfilePath || !migrateSyncCreds) { - return Promise.resolve(false); - } - - Cu.import("resource://gre/modules/osfile.jsm"); - let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME); - return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => { - let accountData = JSON.parse(text).accountData; - updateDisplayedEmail(accountData); - return fxAccounts.setSignedInUser(accountData); - }).then(() => { - return fxAccounts.promiseAccountsForceSigninURI().then(url => { - show("remote"); - wrapper.init(url, urlParams); - }); - }).then(null, error => { - log("Failed to migrate FX Account: " + error); - show("stage", "intro"); - // load the remote frame in the background - fxAccounts.promiseAccountsSignUpURI().then(uri => { - wrapper.init(uri, urlParams) - }).catch(e => { - console.log("Failed to load signup page", e); - setErrorPage("configError"); - }); - }).then(() => { - // Reset the pref after migration. - Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false); - return true; - }).then(null, err => { - Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err); - return false; - }); -} - -// Helper function that returns the path of the default profile on disk. Will be -// overridden in tests. -function getDefaultProfilePath() { - let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"] - .getService(Ci.nsIToolkitProfileService) - .defaultProfile; - return defaultProfile.rootDir.path; -} - -document.addEventListener("DOMContentLoaded", function onload() { - document.removeEventListener("DOMContentLoaded", onload, true); - init(); - var buttonGetStarted = document.getElementById('buttonGetStarted'); - buttonGetStarted.addEventListener('click', getStarted); - - var buttonRetry = document.getElementById('buttonRetry'); - buttonRetry.addEventListener('click', retry); - - var oldsync = document.getElementById('oldsync'); - oldsync.addEventListener('click', handleOldSync); - - var buttonOpenPrefs = document.getElementById('buttonOpenPrefs') - buttonOpenPrefs.addEventListener('click', openPrefs); -}, true); - -function initObservers() { - function observe(subject, topic, data) { - log("about:accounts observed " + topic); - if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) { - // All about:account windows get changed to action=signin on logout. - window.location = "about:accounts?action=signin"; - return; - } - - // must be onverified - we want to open preferences. - openPrefs(); - } - - for (let topic of OBSERVER_TOPICS) { - Services.obs.addObserver(observe, topic, false); - } - window.addEventListener("unload", function(event) { - log("about:accounts unloading") - for (let topic of OBSERVER_TOPICS) { - Services.obs.removeObserver(observe, topic); - } - }); -} -initObservers(); diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml b/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml deleted file mode 100644 index 475f0e86f..000000000 --- a/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml +++ /dev/null @@ -1,112 +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 [ - <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> - %htmlDTD; - <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> - %brandDTD; - <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> - %globalDTD; - <!ENTITY % aboutAccountsDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd"> - %aboutAccountsDTD; - <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> - %syncBrandDTD; -]> - -<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;"> - <head> - <title>&syncBrand.fullName.label;</title> - <meta name="viewport" content="width=device-width"/> - - - <link rel="icon" type="image/png" id="favicon" - href="chrome://branding/content/icon32.png"/> - <link rel="stylesheet" - href="chrome://browser/content/aboutaccounts/normalize.css" - type="text/css" /> - <link rel="stylesheet" - href="chrome://browser/content/aboutaccounts/main.css" - type="text/css" /> - <link rel="stylesheet" - href="chrome://browser/content/aboutaccounts/aboutaccounts.css" - type="text/css" /> - </head> - <body> - <div id="stage"> - - <div id="manage"> - <header> - <h1>&aboutAccounts.connected;</h1> - <div id="email"></div> - </header> - - <section> - <div class="graphic graphic-sync-intro"> </div> - - <div class="button-row"> - <button id="buttonOpenPrefs" class="button" href="#" tabindex="0">&aboutAccountsConfig.syncPreferences.label;</button> - </div> - </section> - </div> - - <div id="intro"> - <header> - <h1>&aboutAccounts.welcome;</h1> - </header> - - <section> - <div class="graphic graphic-sync-intro"> </div> - - <div class="description">&aboutAccountsConfig.description;</div> - - <div class="button-row"> - <button id="buttonGetStarted" class="button" tabindex="1">&aboutAccountsConfig.startButton.label;</button> - </div> - - <div class="links"> - <button id="oldsync" tabindex="2">&aboutAccountsConfig.useOldSync.label;</button> - </div> - </section> - </div> - - <div id="networkError"> - <header> - <h1>&aboutAccounts.noConnection.title;</h1> - </header> - - <section> - <div class="graphic graphic-sync-intro"> </div> - - <div class="description">&aboutAccounts.noConnection.description;</div> - - <div class="button-row"> - <button id="buttonRetry" class="button" tabindex="3">&aboutAccounts.noConnection.retry;</button> - </div> - </section> - </div> - - <div id="configError"> - <header> - <h1>&aboutAccounts.badConfig.title;</h1> - </header> - - <section> - <div class="graphic graphic-sync-intro"> </div> - - <div class="description">&aboutAccounts.badConfig.description;</div> - - </section> - </div> - - </div> - - <iframe mozframetype="content" id="remote" /> - - <script type="application/javascript;version=1.8" - src="chrome://browser/content/utilityOverlay.js"/> - <script type="text/javascript;version=1.8" - src="chrome://browser/content/aboutaccounts/aboutaccounts.js" /> - </body> -</html> diff --git a/application/basilisk/base/content/aboutaccounts/images/fox.png b/application/basilisk/base/content/aboutaccounts/images/fox.png Binary files differdeleted file mode 100644 index 83af78d6c..000000000 --- a/application/basilisk/base/content/aboutaccounts/images/fox.png +++ /dev/null diff --git a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png Binary files differdeleted file mode 100644 index ff5f482f0..000000000 --- a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png +++ /dev/null diff --git a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png Binary files differdeleted file mode 100644 index 89fda0681..000000000 --- a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png +++ /dev/null diff --git a/application/basilisk/base/content/aboutaccounts/main.css b/application/basilisk/base/content/aboutaccounts/main.css deleted file mode 100644 index 8f4c3b34e..000000000 --- a/application/basilisk/base/content/aboutaccounts/main.css +++ /dev/null @@ -1,166 +0,0 @@ -*, -*:before, -*:after { - box-sizing: border-box; -} - -html { - background-color: #F2F2F2; - height: 100%; -} - -body { - color: #424f59; - font: message-box; - font-size: 14px; - height: 100%; -} - -a { - color: #0095dd; - cursor: pointer; /* Use the correct cursor for anchors without an href */ -} - -a:active { - outline: none; -} - -a:focus { - outline: 1px dotted #0095dd; -} - - -a.no-underline { - text-decoration: none; -} - -#stage { - background:#fff; - border-radius: 5px; - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25); - margin: 0 auto; - min-height: 300px; - padding: 60px 40px 40px 40px; - position: relative; - text-align: center; - top: 80px; - width: 420px; -} - -header h1 -{ - font-size: 24px; - font-weight: 200; - line-height: 1em; -} - -#intro header h1 { - margin: 0 0 32px 0; -} - -#manage header h1 { - margin: 0 0 12px 0; -} - -#manage header #email { - margin-bottom: 23px; - color: rgb(138, 155, 168); - font-size: 19px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.description { - font-size: 18px; -} - -.button-row { - margin-top: 45px; - margin-bottom:20px; -} - -.button-row button, -.button-row a.button { - background: #0095dd; - border: none; - border-radius: 5px; - color: #FFFFFF; - cursor: pointer; - font-size: 24px; - padding: 15px 0; - transition-duration: 150ms; - transition-property: background-color; - width: 100%; -} - -.button-row a.button { - display: inline-block; - text-decoration: none; -} - -.button-row a.button:active, -.button-row a.button:hover, -.button-row a.button:focus, -.button-row button:active, -.button-row button:hover, -.button-row button:focus { - background: #08c; -} - - -.graphic-sync-intro { - background-image: url(images/graphic_sync_intro.png); - background-repeat: no-repeat; - background-size: 150px 195px; - height: 195px; - margin: 0 auto; - overflow: hidden; - text-indent: 100%; - white-space: nowrap; - width: 150px; -} - -.description, -.button-row { - margin-top: 30px; -} - -.links { - margin: 20px 0; -} - -@media only screen and (max-width: 500px) { - html { - background: #fff; - } - - #stage { - box-shadow: none; - margin: 30px auto 0 auto; - min-height: none; - min-width: 320px; - padding: 0 10px; - width: 100%; - } - - .button-row { - margin-top: 20px; - } - - .button-row button, - .button-row a.button { - padding: 10px 0; - } - -} - -/* Retina */ -@media -only screen and (min-device-pixel-ratio: 2), -only screen and ( min-resolution: 192dpi), -only screen and ( min-resolution: 2dppx) { - .graphic-sync-intro { - background-image: url(images/graphic_sync_intro@2x.png); - } -} diff --git a/application/basilisk/base/content/aboutaccounts/normalize.css b/application/basilisk/base/content/aboutaccounts/normalize.css deleted file mode 100644 index c02ab25de..000000000 --- a/application/basilisk/base/content/aboutaccounts/normalize.css +++ /dev/null @@ -1,402 +0,0 @@ -/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
-
-/* ==========================================================================
- HTML5 display definitions
- ========================================================================== */
-
-/**
- * Correct `block` display not defined in IE 8/9.
- */
-
-article,
-aside,
-details,
-figcaption,
-figure,
-footer,
-header,
-hgroup,
-main,
-nav,
-section,
-summary {
- display: block;
-}
-
-/**
- * Correct `inline-block` display not defined in IE 8/9.
- */
-
-audio,
-canvas,
-video {
- display: inline-block;
-}
-
-/**
- * Prevent modern browsers from displaying `audio` without controls.
- * Remove excess height in iOS 5 devices.
- */
-
-audio:not([controls]) {
- display: none;
- height: 0;
-}
-
-/**
- * Address `[hidden]` styling not present in IE 8/9.
- * Hide the `template` element in IE, Safari, and Firefox < 22.
- */
-
-[hidden],
-template {
- display: none;
-}
-
-/* ==========================================================================
- Base
- ========================================================================== */
-
-/**
- * 1. Set default font family to sans-serif.
- * 2. Prevent iOS text size adjust after orientation change, without disabling
- * user zoom.
- */
-
-html {
- font-family: sans-serif; /* 1 */
- -ms-text-size-adjust: 100%; /* 2 */
- -webkit-text-size-adjust: 100%; /* 2 */
-}
-
-/**
- * Remove default margin.
- */
-
-body {
- margin: 0;
-}
-
-/* ==========================================================================
- Links
- ========================================================================== */
-
-/**
- * Remove the gray background color from active links in IE 10.
- */
-
-a {
- background: transparent;
-}
-
-/**
- * Address `outline` inconsistency between Chrome and other browsers.
- */
-
-a:focus {
- outline: thin dotted;
-}
-
-/**
- * Improve readability when focused and also mouse hovered in all browsers.
- */
-
-a:active,
-a:hover {
- outline: 0;
-}
-
-/* ==========================================================================
- Typography
- ========================================================================== */
-
-/**
- * Address variable `h1` font-size and margin within `section` and `article`
- * contexts in Firefox 4+, Safari 5, and Chrome.
- */
-
-h1 {
- font-size: 2em;
- margin: 0.67em 0;
-}
-
-/**
- * Address styling not present in IE 8/9, Safari 5, and Chrome.
- */
-
-abbr[title] {
- border-bottom: 1px dotted;
-}
-
-/**
- * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
- */
-
-b,
-strong {
- font-weight: bold;
-}
-
-/**
- * Address styling not present in Safari 5 and Chrome.
- */
-
-dfn {
- font-style: italic;
-}
-
-/**
- * Address differences between Firefox and other browsers.
- */
-
-hr {
- box-sizing: content-box;
- height: 0;
-}
-
-/**
- * Address styling not present in IE 8/9.
- */
-
-mark {
- background: #ff0;
- color: #000;
-}
-
-/**
- * Correct font family set oddly in Safari 5 and Chrome.
- */
-
-code,
-kbd,
-pre,
-samp {
- font-family: monospace, serif;
- font-size: 1em;
-}
-
-/**
- * Improve readability of pre-formatted text in all browsers.
- */
-
-pre {
- white-space: pre-wrap;
-}
-
-/**
- * Set consistent quote types.
- */
-
-q {
- quotes: "\201C" "\201D" "\2018" "\2019";
-}
-
-/**
- * Address inconsistent and variable font size in all browsers.
- */
-
-small {
- font-size: 80%;
-}
-
-/**
- * Prevent `sub` and `sup` affecting `line-height` in all browsers.
- */
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sup {
- top: -0.5em;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-/* ==========================================================================
- Embedded content
- ========================================================================== */
-
-/**
- * Remove border when inside `a` element in IE 8/9.
- */
-
-img {
- border: 0;
-}
-
-/**
- * Correct overflow displayed oddly in IE 9.
- */
-
-svg:not(:root) {
- overflow: hidden;
-}
-
-/* ==========================================================================
- Figures
- ========================================================================== */
-
-/**
- * Address margin not present in IE 8/9 and Safari 5.
- */
-
-figure {
- margin: 0;
-}
-
-/* ==========================================================================
- Forms
- ========================================================================== */
-
-/**
- * Define consistent border, margin, and padding.
- */
-
-fieldset {
- border: 1px solid #c0c0c0;
- margin: 0 2px;
- padding: 0.35em 0.625em 0.75em;
-}
-
-/**
- * 1. Correct `color` not being inherited in IE 8/9.
- * 2. Remove padding so people aren't caught out if they zero out fieldsets.
- */
-
-legend {
- border: 0; /* 1 */
- padding: 0; /* 2 */
-}
-
-/**
- * 1. Correct font family not being inherited in all browsers.
- * 2. Correct font size not being inherited in all browsers.
- * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
- */
-
-button,
-input,
-select,
-textarea {
- font-family: inherit; /* 1 */
- font-size: 100%; /* 2 */
- margin: 0; /* 3 */
-}
-
-/**
- * Address Firefox 4+ setting `line-height` on `input` using `!important` in
- * the UA stylesheet.
- */
-
-button,
-input {
- line-height: normal;
-}
-
-/**
- * Address inconsistent `text-transform` inheritance for `button` and `select`.
- * All other form control elements do not inherit `text-transform` values.
- * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
- * Correct `select` style inheritance in Firefox 4+ and Opera.
- */
-
-button,
-select {
- text-transform: none;
-}
-
-/**
- * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
- * and `video` controls.
- * 2. Correct inability to style clickable `input` types in iOS.
- * 3. Improve usability and consistency of cursor style between image-type
- * `input` and others.
- */
-
-button,
-html input[type="button"], /* 1 */
-input[type="reset"],
-input[type="submit"] {
- -webkit-appearance: button; /* 2 */
- cursor: pointer; /* 3 */
-}
-
-/**
- * Re-set default cursor for disabled elements.
- */
-
-button[disabled],
-html input[disabled] {
- cursor: default;
-}
-
-/**
- * 1. Address box sizing set to `content-box` in IE 8/9/10.
- * 2. Remove excess padding in IE 8/9/10.
- */
-
-input[type="checkbox"],
-input[type="radio"] {
- box-sizing: border-box; /* 1 */
- padding: 0; /* 2 */
-}
-
-/**
- * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
- * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome.
- */
-
-input[type="search"] {
- -webkit-appearance: textfield; /* 1 */
- box-sizing: content-box; /* 2 */
-}
-
-/**
- * Remove inner padding and search cancel button in Safari 5 and Chrome
- * on OS X.
- */
-
-input[type="search"]::-webkit-search-cancel-button,
-input[type="search"]::-webkit-search-decoration {
- -webkit-appearance: none;
-}
-
-/**
- * Remove inner padding and border in Firefox 4+.
- */
-
-button::-moz-focus-inner,
-input::-moz-focus-inner {
- border: 0;
- padding: 0;
-}
-
-/**
- * 1. Remove default vertical scrollbar in IE 8/9.
- * 2. Improve readability and alignment in all browsers.
- */
-
-textarea {
- overflow: auto; /* 1 */
- vertical-align: top; /* 2 */
-}
-
-/* ==========================================================================
- Tables
- ========================================================================== */
-
-/**
- * Remove most spacing between table cells.
- */
-
-table {
- border-collapse: collapse;
- border-spacing: 0;
-}
diff --git a/application/basilisk/base/content/abouthome/aboutHome.css b/application/basilisk/base/content/abouthome/aboutHome.css index bc3f9882c..86f74727f 100644 --- a/application/basilisk/base/content/abouthome/aboutHome.css +++ b/application/basilisk/base/content/abouthome/aboutHome.css @@ -283,9 +283,11 @@ body[narrow] #restorePreviousSession { content: url("chrome://browser/content/abouthome/addons.png"); } +%ifdef MOZ_SERVICES_SYNC #sync::before { content: url("chrome://browser/content/abouthome/sync.png"); } +%endif #settings::before { content: url("chrome://browser/content/abouthome/settings.png"); @@ -369,9 +371,11 @@ body[narrow] #restorePreviousSession::before { content: url("chrome://browser/content/abouthome/addons@2x.png"); } +%ifdef MOZ_SERVICES_SYNC #sync::before { content: url("chrome://browser/content/abouthome/sync@2x.png"); } +%endif #settings::before { content: url("chrome://browser/content/abouthome/settings@2x.png"); diff --git a/application/basilisk/base/content/abouthome/aboutHome.xhtml b/application/basilisk/base/content/abouthome/aboutHome.xhtml index 22bf2e7e8..90daad2dc 100644 --- a/application/basilisk/base/content/abouthome/aboutHome.xhtml +++ b/application/basilisk/base/content/abouthome/aboutHome.xhtml @@ -54,7 +54,9 @@ <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button> <button class="launchButton" id="history">&abouthome.historyButton.label;</button> <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button> +#ifdef MOZ_SERVICES_SYNC <button class="launchButton" id="sync">&abouthome.syncButton.label;</button> +#endif #ifdef XP_WIN <button class="launchButton" id="settings">&abouthome.preferencesButtonWin.label;</button> #else diff --git a/application/basilisk/base/content/browser-context.inc b/application/basilisk/base/content/browser-context.inc index 2f6b19da0..d400cd0b2 100644 --- a/application/basilisk/base/content/browser-context.inc +++ b/application/basilisk/base/content/browser-context.inc @@ -249,13 +249,6 @@ accesskey="&savePageCmd.accesskey2;" oncommand="gContextMenu.savePageAs();"/> <menuseparator id="context-sep-sendpagetodevice" hidden="true"/> - <menu id="context-sendpagetodevice" - label="&sendPageToDevice.label;" - accesskey="&sendPageToDevice.accesskey;" - hidden="true"> - <menupopup id="context-sendpagetodevice-popup" - onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/> - </menu> <menuseparator id="context-sep-viewbgimage"/> <menuitem id="context-viewbgimage" label="&viewBGImageCmd.label;" @@ -296,13 +289,6 @@ <menuitem id="context-searchselect" oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/> <menuseparator id="context-sep-sendlinktodevice" hidden="true"/> - <menu id="context-sendlinktodevice" - label="&sendLinkToDevice.label;" - accesskey="&sendLinkToDevice.accesskey;" - hidden="true"> - <menupopup id="context-sendlinktodevice-popup" - onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/> - </menu> <menuseparator id="frame-sep"/> <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;"> <menupopup> diff --git a/application/basilisk/base/content/browser-doctype.inc b/application/basilisk/base/content/browser-doctype.inc index ad08f4b03..30d70ccea 100644 --- a/application/basilisk/base/content/browser-doctype.inc +++ b/application/basilisk/base/content/browser-doctype.inc @@ -19,7 +19,9 @@ #endif <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> %aboutHomeDTD; +#ifdef MOZ_SERVICES_SYNC <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> %syncBrandDTD; +#endif ]> diff --git a/application/basilisk/base/content/browser-fxaccounts.js b/application/basilisk/base/content/browser-fxaccounts.js deleted file mode 100644 index e1d556bff..000000000 --- a/application/basilisk/base/content/browser-fxaccounts.js +++ /dev/null @@ -1,314 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 gFxAccounts = { - - _initialized: false, - _inCustomizationMode: false, - _cachedProfile: null, - - get weave() { - delete this.weave; - return this.weave = Cc["@mozilla.org/weave/service;1"] - .getService(Ci.nsISupports) - .wrappedJSObject; - }, - - get topics() { - // Do all this dance to lazy-load FxAccountsCommon. - delete this.topics; - return this.topics = [ - "weave:service:ready", - "weave:service:login:change", - "weave:service:setup-complete", - "weave:service:sync:error", - "weave:ui:login:error", - this.FxAccountsCommon.ONLOGIN_NOTIFICATION, - this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, - this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, - ]; - }, - - get panelUIFooter() { - delete this.panelUIFooter; - return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa"); - }, - - get panelUIStatus() { - delete this.panelUIStatus; - return this.panelUIStatus = document.getElementById("PanelUI-fxa-status"); - }, - - get panelUIAvatar() { - delete this.panelUIAvatar; - return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar"); - }, - - get panelUILabel() { - delete this.panelUILabel; - return this.panelUILabel = document.getElementById("PanelUI-fxa-label"); - }, - - get panelUIIcon() { - delete this.panelUIIcon; - return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon"); - }, - - get strings() { - delete this.strings; - return this.strings = Services.strings.createBundle( - "chrome://browser/locale/accounts.properties" - ); - }, - - get loginFailed() { - // Referencing Weave.Service will implicitly initialize sync, and we don't - // want to force that - so first check if it is ready. - let service = Cc["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - if (!service.ready) { - return false; - } - // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". - // All other login failures are assumed to be transient and should go - // away by themselves, so aren't reflected here. - return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; - }, - - init: function () { - // Bail out if we're already initialized and for pop-up windows. - if (this._initialized || !window.toolbar.visible) { - return; - } - - for (let topic of this.topics) { - Services.obs.addObserver(this, topic, false); - } - - gNavToolbox.addEventListener("customizationstarting", this); - gNavToolbox.addEventListener("customizationending", this); - - EnsureFxAccountsWebChannel(); - this._initialized = true; - - this.updateUI(); - }, - - uninit: function () { - if (!this._initialized) { - return; - } - - for (let topic of this.topics) { - Services.obs.removeObserver(this, topic); - } - - this._initialized = false; - }, - - observe: function (subject, topic, data) { - switch (topic) { - case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION: - this._cachedProfile = null; - // Fallthrough intended - default: - this.updateUI(); - break; - } - }, - - handleEvent: function (event) { - this._inCustomizationMode = event.type == "customizationstarting"; - this.updateAppMenuItem(); - }, - - updateUI: function () { - // It's possible someone signed in to FxA after seeing our notification - // about "Legacy Sync migration" (which now is actually "Legacy Sync - // auto-disconnect") so kill that notification if it still exists. - let nb = window.document.getElementById("global-notificationbox"); - let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE); - if (n) { - nb.removeNotification(n, true); - } - - this.updateAppMenuItem(); - }, - - // Note that updateAppMenuItem() returns a Promise that's only used by tests. - updateAppMenuItem: function () { - let profileInfoEnabled = false; - try { - profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled"); - } catch (e) { } - - this.panelUIFooter.hidden = false; - - // Make sure the button is disabled in customization mode. - if (this._inCustomizationMode) { - this.panelUIStatus.setAttribute("disabled", "true"); - this.panelUILabel.setAttribute("disabled", "true"); - this.panelUIAvatar.setAttribute("disabled", "true"); - this.panelUIIcon.setAttribute("disabled", "true"); - } else { - this.panelUIStatus.removeAttribute("disabled"); - this.panelUILabel.removeAttribute("disabled"); - this.panelUIAvatar.removeAttribute("disabled"); - this.panelUIIcon.removeAttribute("disabled"); - } - - let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel"); - let errorLabel = this.panelUIStatus.getAttribute("errorlabel"); - let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel"); - let settingslabel = this.panelUIStatus.getAttribute("settingslabel"); - // The localization string is for the signed in text, but it's the default text as well - let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext"); - - let updateWithUserData = (userData) => { - // Window might have been closed while fetching data. - if (window.closed) { - return; - } - - // Reset the button to its original state. - this.panelUILabel.setAttribute("label", defaultLabel); - this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext); - this.panelUIFooter.removeAttribute("fxastatus"); - this.panelUIFooter.removeAttribute("fxaprofileimage"); - this.panelUIAvatar.style.removeProperty("list-style-image"); - - if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED) { - // Leave the default state - return; - } - - if (this.loginFailed) { - this.panelUIFooter.setAttribute("fxastatus", "error"); - this.panelUILabel.setAttribute("label", errorLabel); - } else { - this.panelUIFooter.setAttribute("fxastatus", "signedin"); - this.panelUILabel.setAttribute("label", settingslabel); - this.panelUIStatus.setAttribute("tooltiptext", ""); - } - } - - let updateWithProfile = (profile) => { - if (profileInfoEnabled) { - if (profile.displayName) { - this.panelUILabel.setAttribute("label", profile.displayName); - } - if (profile.avatar) { - this.panelUIFooter.setAttribute("fxaprofileimage", "set"); - let bgImage = "url(\"" + profile.avatar + "\")"; - this.panelUIAvatar.style.listStyleImage = bgImage; - - let img = new Image(); - img.onerror = () => { - // Clear the image if it has trouble loading. Since this callback is asynchronous - // we check to make sure the image is still the same before we clear it. - if (this.panelUIAvatar.style.listStyleImage === bgImage) { - this.panelUIFooter.removeAttribute("fxaprofileimage"); - this.panelUIAvatar.style.removeProperty("list-style-image"); - } - }; - img.src = profile.avatar; - } - } - } - - return fxAccounts.getSignedInUser().then(userData => { - // userData may be null here when the user is not signed-in, but that's expected - updateWithUserData(userData); - // unverified users cause us to spew log errors fetching an OAuth token - // to fetch the profile, so don't even try in that case. - if (!userData || !userData.verified || !profileInfoEnabled) { - return null; // don't even try to grab the profile. - } - if (this._cachedProfile) { - return this._cachedProfile; - } - return fxAccounts.getSignedInUserProfile().catch(err => { - // Not fetching the profile is sad but the FxA logs will already have noise. - return null; - }); - }).then(profile => { - if (!profile) { - return; - } - updateWithProfile(profile); - this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update - }).catch(error => { - // This is most likely in tests, were we quickly log users in and out. - // The most likely scenario is a user logged out, so reflect that. - // Bug 995134 calls for better errors so we could retry if we were - // sure this was the failure reason. - this.FxAccountsCommon.log.error("Error updating FxA account info", error); - updateWithUserData(null); - }); - }, - - onMenuPanelCommand: function () { - - switch (this.panelUIFooter.getAttribute("fxastatus")) { - case "signedin": - this.openPreferences(); - break; - case "error": - if (this.panelUIFooter.getAttribute("unverified")) { - this.openPreferences(); - } else { - this.openSignInAgainPage("menupanel"); - } - break; - default: - this.openPreferences(); - break; - } - - PanelUI.hide(); - }, - - openPreferences: function () { - openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } }); - }, - - openAccountsPage: function (action, urlParams={}) { - let params = new URLSearchParams(); - if (action) { - params.set("action", action); - } - for (let name in urlParams) { - if (urlParams[name] !== undefined) { - params.set(name, urlParams[name]); - } - } - let url = "about:accounts?" + params; - switchToTabHavingURI(url, true, { - replaceQueryString: true - }); - }, - - openSignInAgainPage: function (entryPoint) { - this.openAccountsPage("reauth", { entrypoint: entryPoint }); - }, - - updateTabContextMenu: function (aPopupMenu) { - // STUB - }, - - initPageContextMenu: function (contextMenu) { - // STUB - } -}; - -XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () { - return Cu.import("resource://gre/modules/FxAccountsCommon.js", {}); -}); - -XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator", - "resource://services-sync/FxaMigrator.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel", - "resource://gre/modules/FxAccountsWebChannel.jsm"); diff --git a/application/basilisk/base/content/browser-menubar.inc b/application/basilisk/base/content/browser-menubar.inc index 0549ad915..b6ab23be5 100644 --- a/application/basilisk/base/content/browser-menubar.inc +++ b/application/basilisk/base/content/browser-menubar.inc @@ -195,9 +195,6 @@ key="key_gotoHistory" observes="viewHistorySidebar" label="&historyButton.label;"/> - <menuitem id="menu_tabsSidebar" - observes="viewTabsSidebar" - label="&syncedTabs.sidebar.label;"/> </menupopup> </menu> <menuseparator/> @@ -317,11 +314,13 @@ key="key_sanitize" command="Tools:Sanitize"/> <menuseparator id="sanitizeSeparator"/> +#ifdef MOZ_SERVICES_SYNC <menuitem id="sync-tabs-menuitem" class="syncTabsMenuItem" - label="&syncTabsMenu3.label;" + label="&syncTabsMenu2.label;" oncommand="BrowserOpenSyncTabs();" - hidden="true"/> + disabled="true"/> +#endif <menuitem id="historyRestoreLastSession" label="&historyRestoreLastSession.label;" command="Browser:RestoreLastSession"/> @@ -440,11 +439,10 @@ accesskey="&toolsMenu.accesskey;" onpopupshowing="mirrorShow(this)"> <menupopup id="menu_ToolsPopup" -# We have to use setTimeout() here to avoid a flickering menu bar when opening -# the Tools menu, see bug 970769. This can be removed once we got rid of the -# event loop spinning in Weave.Status._authManager. - onpopupshowing="setTimeout(() => gSyncUI.updateUI());" - > +#ifdef MOZ_SERVICES_SYNC + onpopupshowing="gSyncUI.updateUI();" +#endif + > <menuitem id="menu_openDownloads" label="&downloads.label;" accesskey="&downloads.accesskey;" @@ -455,23 +453,19 @@ accesskey="&addons.accesskey;" key="key_openAddons" command="Tools:Addons"/> - - <!-- only one of sync-setup, sync-syncnowitem or sync-reauthitem will be showing at once --> +#ifdef MOZ_SERVICES_SYNC + <!-- only one of sync-setup or sync-menu will be showing at once --> <menuitem id="sync-setup" - label="&syncSignIn.label;" - accesskey="&syncSignIn.accesskey;" + label="&syncSetup.label;" + accesskey="&syncSetup.accesskey;" observes="sync-setup-state" - oncommand="gSyncUI.openSetup(null, 'menubar')"/> + oncommand="gSyncUI.openSetup()"/> <menuitem id="sync-syncnowitem" label="&syncSyncNowItem.label;" accesskey="&syncSyncNowItem.accesskey;" observes="sync-syncnow-state" oncommand="gSyncUI.doSync(event);"/> - <menuitem id="sync-reauthitem" - label="&syncReAuthItem.label;" - accesskey="&syncReAuthItem.accesskey;" - observes="sync-reauth-state" - oncommand="gSyncUI.openSignInAgainPage('menubar');"/> +#endif <menuseparator id="devToolsSeparator"/> <menu id="webDeveloperMenu" label="&webDeveloperMenu.label;" diff --git a/application/basilisk/base/content/browser-places.js b/application/basilisk/base/content/browser-places.js index 83c737977..87734140f 100644 --- a/application/basilisk/base/content/browser-places.js +++ b/application/basilisk/base/content/browser-places.js @@ -804,18 +804,29 @@ HistoryMenu.prototype = { }, toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() { + // This is a no-op if MOZ_SERVICES_SYNC isn't defined +#ifdef MOZ_SERVICES_SYNC // Enable/disable the Tabs From Other Computers menu. Some of the menus handled // by HistoryMenu do not have this menuitem. let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0]; if (!menuitem) return; - if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) { + // If Sync isn't configured yet, then don't show the menuitem. + if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { menuitem.setAttribute("hidden", true); return; } + // The tabs engine might never be inited (if services.sync.registerEngines + // is modified), so make sure we avoid undefined errors. + let enabled = Weave.Service.isLoggedIn && + Weave.Service.engineManager.get("tabs") && + Weave.Service.engineManager.get("tabs").enabled; + menuitem.setAttribute("disabled", !enabled); menuitem.setAttribute("hidden", false); +#endif }, _onPopupShowing: function HM__onPopupShowing(aEvent) { diff --git a/application/basilisk/base/content/browser-sets.inc b/application/basilisk/base/content/browser-sets.inc index 6ea057d93..d6a9310ed 100644 --- a/application/basilisk/base/content/browser-sets.inc +++ b/application/basilisk/base/content/browser-sets.inc @@ -163,16 +163,13 @@ <!-- A broadcaster of a number of attributes suitable for "sync now" UI - A 'syncstatus' attribute is set while actively syncing, and the label attribute which changes from "sync now" to "syncing" etc. --> +#ifdef MOZ_SERVICES_SYNC <broadcaster id="sync-status"/> <!-- broadcasters of the "hidden" attribute to reflect setup state for menus --> <broadcaster id="sync-setup-state"/> <broadcaster id="sync-syncnow-state" hidden="true"/> - <broadcaster id="sync-reauth-state" hidden="true"/> - <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;" - type="checkbox" group="sidebar" - sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml" - oncommand="SidebarUI.toggle('viewTabsSidebar');"/> +#endif <broadcaster id="workOfflineMenuitemState"/> <broadcaster id="devtoolsMenuBroadcaster_ErrorConsole" diff --git a/application/basilisk/base/content/browser-syncui.js b/application/basilisk/base/content/browser-syncui.js index 7a80be87e..d0f46247a 100644 --- a/application/basilisk/base/content/browser-syncui.js +++ b/application/basilisk/base/content/browser-syncui.js @@ -1,63 +1,37 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -#ifdef MOZ_SERVICES_CLOUDSYNC -XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", - "resource://gre/modules/CloudSync.jsm"); -#endif - -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - -const MIN_STATUS_ANIMATION_DURATION = 1600; - -// gSyncUI handles updating the tools menu and displaying notifications. +// gSyncUI handles updating the tools menu var gSyncUI = { _obs: ["weave:service:sync:start", - "weave:service:sync:finish", - "weave:service:sync:error", + "weave:service:sync:delayed", "weave:service:quota:remaining", "weave:service:setup-complete", "weave:service:login:start", "weave:service:login:finish", - "weave:service:login:error", "weave:service:logout:finish", "weave:service:start-over", - "weave:service:start-over:finish", "weave:ui:login:error", "weave:ui:sync:error", "weave:ui:sync:finish", "weave:ui:clear-error", - "weave:engine:sync:finish" ], _unloaded: false, - // The last sync start time. Used to calculate the leftover animation time - // once syncing completes (bug 1239042). - _syncStartTime: 0, - _syncAnimationTimer: 0, - - init: function () { - Cu.import("resource://services-common/stringbundle.js"); + init: function SUI_init() { // Proceed to set up the UI if Sync has already started up. // Otherwise we'll do it when Sync is firing up. - if (this.weaveService.ready) { + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + if (xps.ready) { this.initUI(); return; } - // Sync isn't ready yet, but we can still update the UI with an initial - // state - we haven't called initUI() yet, but that's OK - that's more - // about observers for state changes, and will be called once Sync is - // ready to start sending notifications. - this.updateUI(); - Services.obs.addObserver(this, "weave:service:ready", true); - Services.obs.addObserver(this, "quit-application", true); // Remove the observer if the window is closed before the observer // was triggered. @@ -65,7 +39,6 @@ var gSyncUI = { gSyncUI._unloaded = true; window.removeEventListener("unload", onUnload, false); Services.obs.removeObserver(gSyncUI, "weave:service:ready"); - Services.obs.removeObserver(gSyncUI, "quit-application"); if (Weave.Status.ready) { gSyncUI._obs.forEach(function(topic) { @@ -85,158 +58,127 @@ var gSyncUI = { Services.obs.addObserver(this, topic, true); }, this); - // initial label for the sync buttons. - let broadcaster = document.getElementById("sync-status"); - broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label")); - - this.maybeMoveSyncedTabsButton(); - + if (gBrowser && Weave.Notifications.notifications.length) { + this.initNotifications(); + } this.updateUI(); }, + initNotifications: function SUI_initNotifications() { + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let notificationbox = document.createElementNS(XULNS, "notificationbox"); + notificationbox.id = "sync-notifications"; + notificationbox.setAttribute("flex", "1"); - // Returns a promise that resolves with true if Sync needs to be configured, - // false otherwise. - _needsSetup() { - // If Sync is configured for FxAccounts then we do that promise-dance. - if (this.weaveService.fxAccountsEnabled) { - return fxAccounts.getSignedInUser().then(user => { - // We want to treat "account needs verification" as "needs setup". - return !(user && user.verified); - }); - } - // We are using legacy sync - check that. - let firstSync = Services.prefs.getCharPref("services.sync.firstSync", ""); + let bottombox = document.getElementById("browser-bottombox"); + bottombox.insertBefore(notificationbox, bottombox.firstChild); - return Promise.resolve(Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || - firstSync == "notReady"); + // Force a style flush to ensure that our binding is attached. + notificationbox.clientTop; + + // notificationbox will listen to observers from now on. + Services.obs.removeObserver(this, "weave:notification:added"); }, - // Returns a promise that resolves with true if the user currently signed in - // to Sync needs to be verified, false otherwise. - _needsVerification() { - // For callers who care about the distinction between "needs setup" and - // "needs verification" - if (this.weaveService.fxAccountsEnabled) { - return fxAccounts.getSignedInUser().then(user => { - // If there is no user, they can't be in a "needs verification" state. - if (!user) { - return false; - } - return !user.verified; - }); - } + _wasDelayed: false, - // Otherwise we are configured for legacy Sync, which has no verification - // concept. - return Promise.resolve(false); + _needsSetup: function SUI__needsSetup() { + let firstSync = Services.prefs.getCharPref("services.sync.firstSync", ""); + return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + firstSync == "notReady"; }, - // Note that we don't show login errors in a notification bar here, but do - // still need to track a login-failed state so the "Tools" menu updates - // with the correct state. - _loginFailed: function () { - // If Sync isn't already ready, we don't want to force it to initialize - // by referencing Weave.Status - and it isn't going to be accurate before - // Sync is ready anyway. - if (!this.weaveService.ready) { - this.log.debug("_loginFailed has sync not ready, so returning false"); - return false; - } - this.log.debug("_loginFailed has sync state=${sync}", - { sync: Weave.Status.login}); - return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; - }, + updateUI: function SUI_updateUI() { + let needsSetup = this._needsSetup(); + document.getElementById("sync-setup-state").hidden = !needsSetup; + document.getElementById("sync-syncnow-state").hidden = needsSetup; - // Kick off an update of the UI - does *not* return a promise. - updateUI() { - this._promiseUpdateUI().catch(err => { - this.log.error("updateUI failed", err); - }) - }, + if (!gBrowser) + return; - // Updates the UI - returns a promise. - _promiseUpdateUI() { - return this._needsSetup().then(needsSetup => { - if (!gBrowser) - return Promise.resolve(); - - let loginFailed = this._loginFailed(); - - // Start off with a clean slate - document.getElementById("sync-reauth-state").hidden = true; - document.getElementById("sync-setup-state").hidden = true; - document.getElementById("sync-syncnow-state").hidden = true; - -#ifdef MOZ_SERVICES_CLOUDSYNC - if (CloudSync && CloudSync.ready && CloudSync().adapters.count) { - document.getElementById("sync-syncnow-state").hidden = false; - } else if (loginFailed) { -#else - if (loginFailed) { -#endif - // unhiding this element makes the menubar show the login failure state. - document.getElementById("sync-reauth-state").hidden = false; - } else if (needsSetup) { - document.getElementById("sync-setup-state").hidden = false; - } else { - document.getElementById("sync-syncnow-state").hidden = false; - } + let button = document.getElementById("sync-button"); + if (!button) + return; - return this._updateSyncButtonsTooltip(); - }); + button.removeAttribute("status"); + this._updateLastSyncTime(); + if (needsSetup) + button.removeAttribute("tooltiptext"); }, + // Functions called by observers - onActivityStart() { + onActivityStart: function SUI_onActivityStart() { if (!gBrowser) return; - this.log.debug("onActivityStart"); + let button = document.getElementById("sync-button"); + if (!button) + return; - clearTimeout(this._syncAnimationTimer); - this._syncStartTime = Date.now(); + button.setAttribute("status", "active"); + }, - let broadcaster = document.getElementById("sync-status"); - broadcaster.setAttribute("syncstatus", "active"); - broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label")); - broadcaster.setAttribute("disabled", "true"); + onSyncDelay: function SUI_onSyncDelay() { + // basically, we want to just inform users that stuff is going to take a while + let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + let description = this._stringBundle.GetStringFromName("error.sync.no_node_found"); + let buttons = [new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )]; + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_INFO, buttons); + Weave.Notifications.replaceTitle(notification); + this._wasDelayed = true; + }, - this.updateUI(); + onLoginFinish: function SUI_onLoginFinish() { + // Clear out any login failure notifications + let title = this._stringBundle.GetStringFromName("error.login.title"); + this.clearError(title); }, - _updateSyncStatus() { - if (!gBrowser) - return; - let broadcaster = document.getElementById("sync-status"); - broadcaster.removeAttribute("syncstatus"); - broadcaster.removeAttribute("disabled"); - broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label")); - this.updateUI(); + onSetupComplete: function SUI_onSetupComplete() { + this.onLoginFinish(); }, - onActivityStop() { - if (!gBrowser) + onLoginError: function SUI_onLoginError() { + // if login fails, any other notifications are essentially moot + Weave.Notifications.removeAll(); + + // if we haven't set up the client, don't show errors + if (this._needsSetup()) { + this.updateUI(); return; - this.log.debug("onActivityStop"); + } - let now = Date.now(); - let syncDuration = now - this._syncStartTime; + let title = this._stringBundle.GetStringFromName("error.login.title"); - if (syncDuration < MIN_STATUS_ANIMATION_DURATION) { - let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration; - clearTimeout(this._syncAnimationTimer); - this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime); + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); } else { - this._updateSyncStatus(); + let reason = Weave.Utils.getErrorString(Weave.Status.login); + description = + this._stringBundle.formatStringFromName("error.sync.description", [reason], 1); } - }, - onLoginError: function SUI_onLoginError() { - this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status); + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.login.prefs.label"), + this._stringBundle.GetStringFromName("error.login.prefs.accesskey"), + function() { gSyncUI.openPrefs(); return true; } + )); - // We don't show any login errors here; browser-fxaccounts shows them in - // the hamburger menu. + let notification = new Weave.Notification(title, description, null, + Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); this.updateUI(); }, @@ -244,6 +186,10 @@ var gSyncUI = { this.updateUI(); }, + onStartOver: function SUI_onStartOver() { + this.clearError(); + }, + onQuotaNotice: function onQuotaNotice(subject, data) { let title = this._stringBundle.GetStringFromName("warning.sync.quota.label"); let description = this._stringBundle.GetStringFromName("warning.sync.quota.description"); @@ -259,40 +205,26 @@ var gSyncUI = { Weave.Notifications.replaceTitle(notification); }, - _getAppName: function () { - let brand = new StringBundle("chrome://branding/locale/brand.properties"); - return brand.get("brandShortName"); + openServerStatus: function () { + let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); + window.openUILinkIn(statusURL, "tab"); }, // Commands - // doSync forces a sync - it *does not* return a promise as it is called - // via the various UI components. - doSync() { - this._needsSetup().then(needsSetup => { - if (!needsSetup) { - setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0); - } - Services.obs.notifyObservers(null, "cloudsync:user-sync", null); - }).catch(err => { - this.log.error("Failed to force a sync", err); - }); + doSync: function SUI_doSync() { + setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0); }, - // Handle clicking the toolbar button - which either opens the Sync setup - // pages or forces a sync now. Does *not* return a promise as it is called - // via the UI. - handleToolbarButton() { - this._needsSetup().then(needsSetup => { - if (needsSetup || this._loginFailed()) { - this.openSetup(); - } else { - this.doSync(); - } - }).catch(err => { - this.log.error("Failed to handle toolbar button command", err); - }); + handleToolbarButton: function SUI_handleStatusbarButton() { + if (this._needsSetup()) + this.openSetup(); + else + this.doSync(); }, + //XXXzpao should be part of syncCommon.js - which we might want to make a module... + // To be fixed in a followup (bug 583366) + /** * Invoke the Sync setup wizard. * @@ -301,11 +233,9 @@ var gSyncUI = { * null -- regular set up wizard * "pair" -- pair a device first * "reset" -- reset sync - * @param entryPoint - * Indicates the entrypoint from where this method was called. */ - openSetup: function SUI_openSetup(wizardType, entryPoint = "syncbutton") { + openSetup: function SUI_openSetup(wizardType) { let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); if (win) win.focus(); @@ -316,7 +246,6 @@ var gSyncUI = { } }, - // Open the legacy-sync device pairing UI. Note used for FxA Sync. openAddDevice: function () { if (!Weave.Utils.ensureMPUnlocked()) return; @@ -329,199 +258,185 @@ var gSyncUI = { "syncAddDevice", "centerscreen,chrome,resizable=no"); }, - openPrefs: function (entryPoint) { - openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } }); + openQuotaDialog: function SUI_openQuotaDialog() { + let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); + if (win) + win.focus(); + else + Services.ww.activeWindow.openDialog( + "chrome://browser/content/sync/quota.xul", "", + "centerscreen,chrome,dialog,modal"); }, - openSignInAgainPage: function (entryPoint = "syncbutton") { - gFxAccounts.openSignInAgainPage(entryPoint); + openPrefs: function SUI_openPrefs() { + openPreferences("paneSync"); }, - /* After Sync is initialized we perform a once-only check for the sync - button being in "customize purgatory" and if so, move it to the panel. - This is done primarily for profiles created before SyncedTabs landed, - where the button defaulted to being in that purgatory. - We use a preference to ensure we only do it once, so people can still - customize it away and have it stick. - */ - maybeMoveSyncedTabsButton() { - const prefName = "browser.migrated-sync-button"; - let migrated = Services.prefs.getBoolPref(prefName, false); - if (migrated) { + // Helpers + _updateLastSyncTime: function SUI__updateLastSyncTime() { + if (!gBrowser) return; - } - if (!CustomizableUI.getPlacementOfWidget("sync-button")) { - CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); - } - Services.prefs.setBoolPref(prefName, true); - }, - /* Update the tooltip for the sync-status broadcaster (which will update the - Sync Toolbar button and the Sync spinner in the FxA hamburger area.) - If Sync is configured, the tooltip is when the last sync occurred, - otherwise the tooltip reflects the fact that Sync needs to be - (re-)configured. - */ - _updateSyncButtonsTooltip: Task.async(function* () { - if (!gBrowser) + let syncButton = document.getElementById("sync-button"); + if (!syncButton) return; - let email; + let lastSync; try { - email = Services.prefs.getCharPref("services.sync.username"); - } catch (ex) {} - - let needsSetup = yield this._needsSetup(); - let needsVerification = yield this._needsVerification(); - let loginFailed = this._loginFailed(); - // This is a little messy as the Sync buttons are 1/2 Sync related and - // 1/2 FxA related - so for some strings we use Sync strings, but for - // others we reach into gFxAccounts for strings. - let tooltiptext; - if (needsVerification) { - // "needs verification" - tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1); - } else if (needsSetup) { - // "needs setup". - tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description"); - } else if (loginFailed) { - // "need to reconnect/re-enter your password" - tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1); - } else { - // Sync appears configured - format the "last synced at" time. - try { - let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync")); - tooltiptext = this.formatLastSyncDate(lastSync); - } - catch (e) { - // pref doesn't exist (which will be the case until we've seen the - // first successful sync) or is invalid (which should be impossible!) - // Just leave tooltiptext as the empty string in these cases, which - // will cause the tooltip to be removed below. - } + lastSync = Services.prefs.getCharPref("services.sync.lastSync"); } - - // We've done all our promise-y work and ready to update the UI - make - // sure it hasn't been torn down since we started. - if (!gBrowser) + catch (e) { }; + if (!lastSync || this._needsSetup()) { + syncButton.removeAttribute("tooltiptext"); return; - - let broadcaster = document.getElementById("sync-status"); - if (broadcaster) { - if (tooltiptext) { - broadcaster.setAttribute("tooltiptext", tooltiptext); - } else { - broadcaster.removeAttribute("tooltiptext"); - } - } - }), - - formatLastSyncDate: function(date) { - let dateFormat; - let sixDaysAgo = (() => { - let date = new Date(); - date.setDate(date.getDate() - 6); - date.setHours(0, 0, 0, 0); - return date; - })(); - // It may be confusing for the user to see "Last Sync: Monday" when the last sync was a indeed a Monday but 3 weeks ago - if (date < sixDaysAgo) { - dateFormat = {month: 'long', day: 'numeric'}; - } else { - dateFormat = {weekday: 'long', hour: 'numeric', minute: 'numeric'}; } - let lastSyncDateString = date.toLocaleDateString(undefined, dateFormat); - return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1); + + // Show the day-of-week and time (HH:MM) of last sync + let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let lastSyncLabel = + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); + + syncButton.setAttribute("tooltiptext", lastSyncLabel); }, - onClientsSynced: function() { - let broadcaster = document.getElementById("sync-syncnow-state"); - if (broadcaster) { - if (Weave.Service.clientsEngine.stats.numClients > 1) { - broadcaster.setAttribute("devices-status", "multi"); - } else { - broadcaster.setAttribute("devices-status", "single"); - } + clearError: function SUI_clearError(errorString) { + Weave.Notifications.removeAll(errorString); + this.updateUI(); + }, + + onSyncFinish: function SUI_onSyncFinish() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + // Clear out sync failures on a successful sync + this.clearError(title); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + this.clearError(title); + this._wasDelayed = false; } }, onSyncError: function SUI_onSyncError() { - this.log.debug("onSyncError: login=${login}, sync=${sync}", Weave.Status); let title = this._stringBundle.GetStringFromName("error.sync.title"); - let error = Weave.Utils.getErrorString(Weave.Status.sync); - let description = + + if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) { + this.onLoginError(); + return; + } + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let error = Weave.Utils.getErrorString(Weave.Status.sync); + description = this._stringBundle.formatStringFromName("error.sync.description", [error], 1); + } let priority = Weave.Notifications.PRIORITY_WARNING; let buttons = []; - if (Weave.Status.sync == Weave.OVER_QUOTA) { - description = this._stringBundle.GetStringFromName("error.sync.quota.description"); + // Check if the client is outdated in some way + let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE; + for (let [engine, reason] in Iterator(Weave.Status.engines)) + outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE; + + if (outdated) { + description = this._stringBundle.GetStringFromName( + "error.sync.needUpdate.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.needUpdate.label"), + this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"), + function() { + window.openUILinkIn(Services.prefs.getCharPref("services.sync.outdated.url"), "tab"); + return true; + } + )); + } + else if (Weave.Status.sync == Weave.OVER_QUOTA) { + description = this._stringBundle.GetStringFromName( + "error.sync.quota.description"); buttons.push(new Weave.NotificationButton( - this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"), - this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"), + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.accesskey"), function() { gSyncUI.openQuotaDialog(); return true; } ) ); - // Only show the notification bar on Quota error. the panel will show the rest. - let notification = - new Weave.Notification(title, description, null, priority, buttons); - Weave.Notifications.replaceTitle(notification); + } + else if (Weave.Status.enforceBackoff) { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )); + } + else { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"), + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"), + function() { gSyncUI.doSync(); return true; } + )); + } + + let notification = + new Weave.Notification(title, description, null, priority, buttons); + Weave.Notifications.replaceTitle(notification); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + Weave.Notifications.removeAll(title); + this._wasDelayed = false; } this.updateUI(); }, - observe: function SUI_observe(subject, topic, data) { - this.log.debug("observed", topic); if (this._unloaded) { Cu.reportError("SyncUI observer called after unload: " + topic); return; } - // Unwrap, just like Svc.Obs, but without pulling in that dependency. - if (subject && typeof subject == "object" && - ("wrappedJSObject" in subject) && - ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) { - subject = subject.wrappedJSObject.object; - } - - // First handle "activity" only. switch (topic) { case "weave:service:sync:start": this.onActivityStart(); break; - case "weave:service:sync:finish": - case "weave:service:sync:error": - this.onActivityStop(); - break; - } - // Now non-activity state (eg, enabled, errors, etc) - // Note that sync uses the ":ui:" notifications for errors because sync. - switch (topic) { case "weave:ui:sync:finish": - // Do nothing. + this.onSyncFinish(); break; case "weave:ui:sync:error": this.onSyncError(); break; - case "weave:service:setup-complete": - case "weave:service:login:finish": - case "weave:service:login:start": - case "weave:service:start-over": - this.updateUI(); + case "weave:service:sync:delayed": + this.onSyncDelay(); break; case "weave:service:quota:remaining": this.onQuotaNotice(); break; + case "weave:service:setup-complete": + this.onSetupComplete(); + break; + case "weave:service:login:start": + this.onActivityStart(); + break; + case "weave:service:login:finish": + this.onLoginFinish(); + break; case "weave:ui:login:error": - case "weave:service:login:error": this.onLoginError(); break; case "weave:service:logout:finish": this.onLogout(); break; - case "weave:service:start-over:finish": - this.updateUI(); + case "weave:service:start-over": + this.onStartOver(); break; case "weave:service:ready": this.initUI(); @@ -529,16 +444,8 @@ var gSyncUI = { case "weave:notification:added": this.initNotifications(); break; - case "weave:engine:sync:finish": - if (data != "clients") { - return; - } - this.onClientsSynced(); - break; - case "quit-application": - // Stop the animation timer on shutdown, since we can't update the UI - // after this. - clearTimeout(this._syncAnimationTimer); + case "weave:ui:clear-error": + this.clearError(); break; } }, @@ -550,19 +457,10 @@ var gSyncUI = { }; XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() { - // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381) + //XXXzpao these strings should probably be moved from /services to /browser... (bug 583381) // but for now just make it work return Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). createBundle("chrome://weave/locale/services/sync.properties"); }); -XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() { - return Log.repository.getLogger("browserwindow.syncui"); -}); - -XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() { - return Components.classes["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; -}); diff --git a/application/basilisk/base/content/browser.css b/application/basilisk/base/content/browser.css index 517c1c5eb..a2c2559d6 100644 --- a/application/basilisk/base/content/browser.css +++ b/application/basilisk/base/content/browser.css @@ -687,6 +687,18 @@ window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#pri min-width: 1px; } +%ifdef MOZ_SERVICES_SYNC +/* Sync notification UI */ +#sync-notifications { + -moz-binding: url("chrome://browser/content/sync/notification.xml#notificationbox"); + overflow-y: visible !important; +} + +#sync-notifications notification { + -moz-binding: url("chrome://browser/content/sync/notification.xml#notification"); +} +%endif + /* History Swipe Animation */ #historySwipeAnimationContainer { diff --git a/application/basilisk/base/content/browser.js b/application/basilisk/base/content/browser.js index 7e8f7d821..926a369dd 100644 --- a/application/basilisk/base/content/browser.js +++ b/application/basilisk/base/content/browser.js @@ -46,7 +46,6 @@ Cu.import("resource://gre/modules/NotificationDB.jsm"); ["Task", "resource://gre/modules/Task.jsm"], ["UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"], ["Weave", "resource://services-sync/main.js"], - ["fxAccounts", "resource://gre/modules/FxAccounts.jsm"], #ifdef MOZ_DEVTOOLS // Note: Do not delete! It is used for: base/content/nsContextMenu.js ["gDevTools", "resource://devtools/client/framework/gDevTools.jsm"], @@ -103,6 +102,12 @@ XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() { return new tmp.PageMenuParent(); }); +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); +#endif + + XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { let tmp = {}; Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp); @@ -208,6 +213,10 @@ var gInitialPages = [ "about:logopage" ]; +#ifdef MOZ_SERVICES_SYNC +#include browser-syncui.js +#endif + function* browserWindows() { let windows = Services.wm.getEnumerator("navigator:browser"); while (windows.hasMoreElements()) @@ -1305,13 +1314,14 @@ var gBrowserInit = { FullScreen.init(); PointerLock.init(); - // initialize the sync UI - gSyncUI.init(); - gFxAccounts.init(); - if (AppConstants.MOZ_DATA_REPORTING) gDataNotificationInfoBar.init(); +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + gBrowserThumbnails.init(); gMenuButtonBadgeManager.init(); @@ -1445,8 +1455,6 @@ var gBrowserInit = { FullScreen.uninit(); - gFxAccounts.uninit(); - Services.obs.removeObserver(gPluginHandler.NPAPIPluginCrashed, "plugin-crashed"); try { @@ -1607,8 +1615,10 @@ if (AppConstants.platform == "macosx") { // initialize the private browsing UI gPrivateBrowsingUI.init(); +#ifdef MOZ_SERVICES_SYNC // initialize the sync UI gSyncUI.init(); +#endif }; gBrowserInit.nonBrowserWindowShutdown = function() { @@ -3175,12 +3185,14 @@ var PrintPreviewListener = { this._chromeState.globalNotificationsOpen = !globalNotificationBox.notificationsHidden; globalNotificationBox.notificationsHidden = true; +#ifdef MOZ_SERVICES_SYNC this._chromeState.syncNotificationsOpen = false; var syncNotifications = document.getElementById("sync-notifications"); if (syncNotifications) { this._chromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden; syncNotifications.notificationsHidden = true; } +#endif }, _showChrome: function () { if (this._chromeState.notificationsOpen) @@ -3192,8 +3204,10 @@ var PrintPreviewListener = { if (this._chromeState.globalNotificationsOpen) document.getElementById("global-notificationbox").notificationsHidden = false; +#ifdef MOZ_SERVICES_SYNC if (this._chromeState.syncNotificationsOpen) document.getElementById("sync-notifications").notificationsHidden = false; +#endif if (this._chromeState.sidebarOpen) SidebarUI.show(this._sidebarCommand); @@ -6232,9 +6246,14 @@ function checkEmptyPageOrigin(browser = gBrowser.selectedBrowser, return ssm.isSystemPrincipal(contentPrincipal); } +#ifdef MOZ_SERVICES_SYNC function BrowserOpenSyncTabs() { - switchToTabHavingURI("about:sync-tabs", true); + if (gSyncUI._needsSetup()) + gSyncUI.openSetup(); + else + switchToTabHavingURI("about:sync-tabs", true); } +#endif /** * Format a URL @@ -7534,8 +7553,6 @@ var TabContextMenu = { this.contextTab.addEventListener("TabAttrModified", this, false); aPopupMenu.addEventListener("popuphiding", this, false); - - gFxAccounts.updateTabContextMenu(aPopupMenu); }, handleEvent(aEvent) { switch (aEvent.type) { diff --git a/application/basilisk/base/content/browser.xul b/application/basilisk/base/content/browser.xul index 0cc4c982a..91299de29 100644 --- a/application/basilisk/base/content/browser.xul +++ b/application/basilisk/base/content/browser.xul @@ -100,11 +100,6 @@ tbattr="tabbrowser-multiple" oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/> <menuseparator id="context_sendTabToDevice_separator" hidden="true"/> - <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;" - accesskey="&sendTabToDevice.accesskey;" hidden="true"> - <menupopup id="context_sendTabToDevicePopupMenu" - onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/> - </menu> <menuseparator/> <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;" tbattr="tabbrowser-multiple-visible" @@ -390,59 +385,6 @@ <tooltip id="dynamic-shortcut-tooltip" onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/> - - <menupopup id="SyncedTabsSidebarContext"> - <menuitem label="&syncedTabs.context.open.label;" - accesskey="&syncedTabs.context.open.accesskey;" - id="syncedTabsOpenSelected" where="current"/> - <menuitem label="&syncedTabs.context.openInNewTab.label;" - accesskey="&syncedTabs.context.openInNewTab.accesskey;" - id="syncedTabsOpenSelectedInTab" where="tab"/> - <menuitem label="&syncedTabs.context.openInNewWindow.label;" - accesskey="&syncedTabs.context.openInNewWindow.accesskey;" - id="syncedTabsOpenSelectedInWindow" where="window"/> - <menuitem label="&syncedTabs.context.openInNewPrivateWindow.label;" - accesskey="&syncedTabs.context.openInNewPrivateWindow.accesskey;" - id="syncedTabsOpenSelectedInPrivateWindow" where="window" private="true"/> - <menuseparator/> - <menuitem label="&syncedTabs.context.bookmarkSingleTab.label;" - accesskey="&syncedTabs.context.bookmarkSingleTab.accesskey;" - id="syncedTabsBookmarkSelected"/> - <menuitem label="&syncedTabs.context.copy.label;" - accesskey="&syncedTabs.context.copy.accesskey;" - id="syncedTabsCopySelected"/> - <menuseparator/> - <menuitem label="&syncSyncNowItem.label;" - accesskey="&syncSyncNowItem.accesskey;" - id="syncedTabsRefresh"/> - </menupopup> - <menupopup id="SyncedTabsSidebarTabsFilterContext" - class="textbox-contextmenu"> - <menuitem label="&undoCmd.label;" - accesskey="&undoCmd.accesskey;" - cmd="cmd_undo"/> - <menuseparator/> - <menuitem label="&cutCmd.label;" - accesskey="&cutCmd.accesskey;" - cmd="cmd_cut"/> - <menuitem label="©Cmd.label;" - accesskey="©Cmd.accesskey;" - cmd="cmd_copy"/> - <menuitem label="&pasteCmd.label;" - accesskey="&pasteCmd.accesskey;" - cmd="cmd_paste"/> - <menuitem label="&deleteCmd.label;" - accesskey="&deleteCmd.accesskey;" - cmd="cmd_delete"/> - <menuseparator/> - <menuitem label="&selectAllCmd.label;" - accesskey="&selectAllCmd.accesskey;" - cmd="cmd_selectAll"/> - <menuseparator/> - <menuitem label="&syncSyncNowItem.label;" - accesskey="&syncSyncNowItem.accesskey;" - id="syncedTabsRefreshFilter"/> - </menupopup> </popupset> #ifdef CAN_DRAW_IN_TITLEBAR @@ -964,6 +906,18 @@ type="checkbox" label="&fullScreenCmd.label;" tooltip="dynamic-shortcut-tooltip"/> + +#ifdef MOZ_SERVICES_SYNC + <toolbarbutton id="sync-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&syncToolbarButton.label;" + oncommand="gSyncUI.handleToolbarButton();"/>> + + <toolbarbutton id="sync-tabs-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&syncTabsToolbarButton.label;" + oncommand="BrowserOpenSyncTabs();"/> +#endif </toolbarpalette> </toolbox> diff --git a/application/basilisk/base/content/global-scripts.inc b/application/basilisk/base/content/global-scripts.inc index eef21e15e..db8496cfc 100644 --- a/application/basilisk/base/content/global-scripts.inc +++ b/application/basilisk/base/content/global-scripts.inc @@ -27,7 +27,6 @@ <script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/> #endif <script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/> -<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/> <script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/> <script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/> <script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/> @@ -36,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/nsContextMenu.js b/application/basilisk/base/content/nsContextMenu.js index 74a2e7a8e..370e5ba60 100644 --- a/application/basilisk/base/content/nsContextMenu.js +++ b/application/basilisk/base/content/nsContextMenu.js @@ -112,7 +112,6 @@ nsContextMenu.prototype = { this.initLeaveDOMFullScreenItems(); this.initClickToPlayItems(); this.initPasswordManagerItems(); - this.initSyncItems(); }, initPageMenuSeparator: function CM_initPageMenuSeparator() { @@ -522,10 +521,6 @@ nsContextMenu.prototype = { popup.insertBefore(fragment, insertBeforeElement); }, - initSyncItems: function() { - gFxAccounts.initPageContextMenu(this); - }, - openPasswordManager: function() { LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host); }, diff --git a/application/basilisk/base/content/sync/customize.css b/application/basilisk/base/content/sync/customize.css deleted file mode 100644 index 2bb62595d..000000000 --- a/application/basilisk/base/content/sync/customize.css +++ /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/. */ - -:root { - font-size: 80%; -} - -#sync-customize-pane { - padding-inline-start: 74px; - background: top left url(chrome://browser/skin/sync-128.png) no-repeat; - background-size: 64px; -} - -#sync-customize-title { - margin-inline-start: 0; - padding-bottom: 0.5em; - font-weight: bold; -} - -#sync-customize-subtitle { - font-size: 90%; -} - -checkbox { - margin: 0; - padding: 0.5em 0 0; -} diff --git a/application/basilisk/base/content/sync/customize.js b/application/basilisk/base/content/sync/customize.js deleted file mode 100644 index f431ac58c..000000000 --- a/application/basilisk/base/content/sync/customize.js +++ /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/. */ - -"use strict"; - -Components.utils.import("resource://gre/modules/Services.jsm"); - -addEventListener("dialogaccept", function () { - let pane = document.getElementById("sync-customize-pane"); - // First determine what the preference for the "global" sync enabled pref - // should be based on the engines selected. - let prefElts = pane.querySelectorAll("preferences > preference"); - let syncEnabled = false; - for (let elt of prefElts) { - if (elt.name.startsWith("services.sync.") && elt.value) { - syncEnabled = true; - break; - } - } - Services.prefs.setBoolPref("services.sync.enabled", syncEnabled); - // and write the individual prefs. - pane.writePreferences(true); - window.arguments[0].accepted = true; -}); diff --git a/application/basilisk/base/content/sync/customize.xul b/application/basilisk/base/content/sync/customize.xul deleted file mode 100644 index 827edf565..000000000 --- a/application/basilisk/base/content/sync/customize.xul +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0"?> - -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://browser/content/sync/customize.css" type="text/css"?> - -<!DOCTYPE dialog [ -<!ENTITY % syncCustomizeDTD SYSTEM "chrome://browser/locale/syncCustomize.dtd"> -%syncCustomizeDTD; -]> -<dialog id="sync-customize" - windowtype="Sync:Customize" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - title="&syncCustomize.dialog.title;" - buttonlabelaccept="&syncCustomize.acceptButton.label;" - buttons="accept"> - - <prefpane id="sync-customize-pane"> - <preferences> - <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/> - <preference id="engine.history" name="services.sync.engine.history" type="bool"/> - <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/> - <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/> - <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/> - <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/> - </preferences> - - <label id="sync-customize-title" value="&syncCustomize.title;"/> - <description id="sync-customize-subtitle" - value="&syncCustomize.description;"/> - - <vbox align="start"> - <checkbox label="&engine.tabs.label;" - accesskey="&engine.tabs.accesskey;" - preference="engine.tabs"/> - <checkbox label="&engine.bookmarks.label;" - accesskey="&engine.bookmarks.accesskey;" - preference="engine.bookmarks"/> - <checkbox label="&engine.passwords.label;" - accesskey="&engine.passwords.accesskey;" - preference="engine.passwords"/> - <checkbox label="&engine.history.label;" - accesskey="&engine.history.accesskey;" - preference="engine.history"/> - <checkbox label="&engine.addons.label;" - accesskey="&engine.addons.accesskey;" - preference="engine.addons"/> - <checkbox label="&engine.prefs.label;" - accesskey="&engine.prefs.accesskey;" - preference="engine.prefs"/> - </vbox> - - </prefpane> - - <script type="application/javascript" - src="chrome://browser/content/sync/customize.js" /> - -</dialog> 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/base/jar.mn b/application/basilisk/base/jar.mn index 3b51e9555..72061a622 100644 --- a/application/basilisk/base/jar.mn +++ b/application/basilisk/base/jar.mn @@ -24,7 +24,9 @@ browser.jar: content/browser/abouthome/bookmarks.png (content/abouthome/bookmarks.png) content/browser/abouthome/history.png (content/abouthome/history.png) content/browser/abouthome/addons.png (content/abouthome/addons.png) +#ifdef MOZ_SERVICES_SYNC content/browser/abouthome/sync.png (content/abouthome/sync.png) +#endif content/browser/abouthome/settings.png (content/abouthome/settings.png) content/browser/abouthome/restore.png (content/abouthome/restore.png) content/browser/abouthome/restore-large.png (content/abouthome/restore-large.png) @@ -35,7 +37,9 @@ browser.jar: content/browser/abouthome/bookmarks@2x.png (content/abouthome/bookmarks@2x.png) content/browser/abouthome/history@2x.png (content/abouthome/history@2x.png) content/browser/abouthome/addons@2x.png (content/abouthome/addons@2x.png) +#ifdef MOZ_SERVICES_SYNC content/browser/abouthome/sync@2x.png (content/abouthome/sync@2x.png) +#endif content/browser/abouthome/settings@2x.png (content/abouthome/settings@2x.png) content/browser/abouthome/restore@2x.png (content/abouthome/restore@2x.png) content/browser/abouthome/restore-large@2x.png (content/abouthome/restore-large@2x.png) @@ -48,16 +52,6 @@ browser.jar: content/browser/abouthealthreport/abouthealth.js (content/abouthealthreport/abouthealth.js) content/browser/abouthealthreport/abouthealth.css (content/abouthealthreport/abouthealth.css) #endif - content/browser/aboutaccounts/aboutaccounts.xhtml (content/aboutaccounts/aboutaccounts.xhtml) - content/browser/aboutaccounts/aboutaccounts.js (content/aboutaccounts/aboutaccounts.js) - content/browser/aboutaccounts/aboutaccounts.css (content/aboutaccounts/aboutaccounts.css) - content/browser/aboutaccounts/main.css (content/aboutaccounts/main.css) - content/browser/aboutaccounts/normalize.css (content/aboutaccounts/normalize.css) - content/browser/aboutaccounts/images/fox.png (content/aboutaccounts/images/fox.png) - content/browser/aboutaccounts/images/graphic_sync_intro.png (content/aboutaccounts/images/graphic_sync_intro.png) - content/browser/aboutaccounts/images/graphic_sync_intro@2x.png (content/aboutaccounts/images/graphic_sync_intro@2x.png) - - content/browser/aboutTabCrashed.css (content/aboutTabCrashed.css) content/browser/aboutTabCrashed.js (content/aboutTabCrashed.js) content/browser/aboutTabCrashed.xhtml (content/aboutTabCrashed.xhtml) @@ -73,17 +67,15 @@ browser.jar: content/browser/browser-feeds.js (content/browser-feeds.js) content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js) content/browser/browser-fullZoom.js (content/browser-fullZoom.js) - content/browser/browser-fxaccounts.js (content/browser-fxaccounts.js) content/browser/browser-gestureSupport.js (content/browser-gestureSupport.js) * content/browser/browser-media.js (content/browser-media.js) - content/browser/browser-places.js (content/browser-places.js) +* content/browser/browser-places.js (content/browser-places.js) content/browser/browser-plugins.js (content/browser-plugins.js) content/browser/browser-refreshblocker.js (content/browser-refreshblocker.js) #ifdef MOZ_SAFE_BROWSING content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js) #endif content/browser/browser-sidebar.js (content/browser-sidebar.js) -* content/browser/browser-syncui.js (content/browser-syncui.js) * content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml) #ifdef CAN_DRAW_IN_TITLEBAR content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar.js) @@ -132,23 +124,6 @@ browser.jar: content/browser/pageinfo/feeds.xml (content/pageinfo/feeds.xml) content/browser/pageinfo/permissions.js (content/pageinfo/permissions.js) content/browser/pageinfo/security.js (content/pageinfo/security.js) - content/browser/sync/aboutSyncTabs.xul (content/sync/aboutSyncTabs.xul) -* content/browser/sync/aboutSyncTabs.js (content/sync/aboutSyncTabs.js) - content/browser/sync/aboutSyncTabs.css (content/sync/aboutSyncTabs.css) - content/browser/sync/aboutSyncTabs-bindings.xml (content/sync/aboutSyncTabs-bindings.xml) - content/browser/sync/setup.xul (content/sync/setup.xul) - content/browser/sync/addDevice.js (content/sync/addDevice.js) - content/browser/sync/addDevice.xul (content/sync/addDevice.xul) - content/browser/sync/setup.js (content/sync/setup.js) - content/browser/sync/genericChange.xul (content/sync/genericChange.xul) - content/browser/sync/genericChange.js (content/sync/genericChange.js) - content/browser/sync/key.xhtml (content/sync/key.xhtml) - content/browser/sync/utils.js (content/sync/utils.js) - content/browser/sync/customize.xul (content/sync/customize.xul) - content/browser/sync/customize.js (content/sync/customize.js) - content/browser/sync/customize.css (content/sync/customize.css) - content/browser/sync/quota.xul (content/sync/quota.xul) - content/browser/sync/quota.js (content/sync/quota.js) content/browser/safeMode.css (content/safeMode.css) content/browser/safeMode.js (content/safeMode.js) content/browser/safeMode.xul (content/safeMode.xul) diff --git a/application/basilisk/components/about/AboutRedirector.cpp b/application/basilisk/components/about/AboutRedirector.cpp index 2b8608484..d52b063e2 100644 --- a/application/basilisk/components/about/AboutRedirector.cpp +++ b/application/basilisk/components/about/AboutRedirector.cpp @@ -92,10 +92,16 @@ static RedirEntry kRedirMap[] = { "welcomeback", "chrome://browser/content/aboutWelcomeBack.xhtml", nsIAboutModule::ALLOW_SCRIPT }, +#ifdef MOZ_SERVICES_SYNC + { + "sync-progress", "chrome://browser/content/sync/progress.xhtml", + nsIAboutModule::ALLOW_SCRIPT + }, { "sync-tabs", "chrome://browser/content/sync/aboutSyncTabs.xul", nsIAboutModule::ALLOW_SCRIPT }, +#endif { "home", "chrome://browser/content/abouthome/aboutHome.xhtml", nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | nsIAboutModule::URI_MUST_LOAD_IN_CHILD | @@ -123,10 +129,6 @@ static RedirEntry kRedirMap[] = { }, #endif { - "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml", - nsIAboutModule::ALLOW_SCRIPT - }, - { "reader", "chrome://global/content/reader/aboutReader.html", nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | nsIAboutModule::ALLOW_SCRIPT | diff --git a/application/basilisk/components/build/nsModule.cpp b/application/basilisk/components/build/nsModule.cpp index 4e082ab6c..3fdde8823 100644 --- a/application/basilisk/components/build/nsModule.cpp +++ b/application/basilisk/components/build/nsModule.cpp @@ -97,12 +97,14 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = { { NS_ABOUT_MODULE_CONTRACTID_PREFIX "searchreset", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcomeback", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, +#ifdef MOZ_SERVICES_SYNC { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, + { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-progress", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, +#endif { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, - { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, #ifdef MOZ_SERVICES_HEALTHREPORT { NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID }, #endif diff --git a/application/basilisk/components/customizableui/CustomizableUI.jsm b/application/basilisk/components/customizableui/CustomizableUI.jsm index be274da71..d56d63d99 100644 --- a/application/basilisk/components/customizableui/CustomizableUI.jsm +++ b/application/basilisk/components/customizableui/CustomizableUI.jsm @@ -197,7 +197,6 @@ var CustomizableUIInternal = { "find-button", "preferences-button", "add-ons-button", - "sync-button", ]; if (!AppConstants.MOZ_DEV_EDITION) { diff --git a/application/basilisk/components/customizableui/CustomizableWidgets.jsm b/application/basilisk/components/customizableui/CustomizableWidgets.jsm index 53812762d..d4a191a97 100644 --- a/application/basilisk/components/customizableui/CustomizableWidgets.jsm +++ b/application/basilisk/components/customizableui/CustomizableWidgets.jsm @@ -283,144 +283,6 @@ const CustomizableWidgets = [ log.debug("History view is being hidden!"); } }, { - id: "sync-button", - label: "remotetabs-panelmenu.label", - tooltiptext: "remotetabs-panelmenu.tooltiptext2", - type: "view", - viewId: "PanelUI-remotetabs", - defaultArea: CustomizableUI.AREA_PANEL, - deckIndices: { - DECKINDEX_TABS: 0, - DECKINDEX_TABSDISABLED: 1, - DECKINDEX_FETCHING: 2, - DECKINDEX_NOCLIENTS: 3, - }, - onCreated(aNode) { - // Add an observer to the button so we get the animation during sync. - // (Note the observer sets many attributes, including label and - // tooltiptext, but we only want the 'syncstatus' attribute for the - // animation) - let doc = aNode.ownerDocument; - let obnode = doc.createElementNS(kNSXUL, "observes"); - obnode.setAttribute("element", "sync-status"); - obnode.setAttribute("attribute", "syncstatus"); - aNode.appendChild(obnode); - }, - setDeckIndex(index) { - let deck = this._tabsList.ownerDocument.getElementById("PanelUI-remotetabs-deck"); - // We call setAttribute instead of relying on the XBL property setter due - // to things going wrong when we try and set the index before the XBL - // binding has been created - see bug 1241851 for the gory details. - deck.setAttribute("selectedIndex", index); - }, - - _showTabsPromise: Promise.resolve(), - // Update the tab list after any existing in-flight updates are complete. - _showTabs() { - this._showTabsPromise = this._showTabsPromise.then(() => { - return this.__showTabs(); - }); - }, - // Return a new promise to update the tab list. - __showTabs() { - let doc = this._tabsList.ownerDocument; - return SyncedTabs.getTabClients().then(clients => { - // The view may have been hidden while the promise was resolving. - if (!this._tabsList) { - return; - } - if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) { - // the "fetching tabs" deck is being shown - let's leave it there. - // When that first sync completes we'll be notified and update. - return; - } - - if (clients.length === 0) { - this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS); - return; - } - - this.setDeckIndex(this.deckIndices.DECKINDEX_TABS); - this._clearTabList(); - SyncedTabs.sortTabClientsByLastUsed(clients, 50 /* maxTabs */); - let fragment = doc.createDocumentFragment(); - - for (let client of clients) { - // add a menu separator for all clients other than the first. - if (fragment.lastChild) { - let separator = doc.createElementNS(kNSXUL, "menuseparator"); - fragment.appendChild(separator); - } - this._appendClient(client, fragment); - } - this._tabsList.appendChild(fragment); - }).catch(err => { - Cu.reportError(err); - }).then(() => { - // an observer for tests. - Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated", null); - }); - }, - _clearTabList () { - let list = this._tabsList; - while (list.lastChild) { - list.lastChild.remove(); - } - }, - _showNoClientMessage() { - this._appendMessageLabel("notabslabel"); - }, - _appendMessageLabel(messageAttr, appendTo = null) { - if (!appendTo) { - appendTo = this._tabsList; - } - let message = this._tabsList.getAttribute(messageAttr); - let doc = this._tabsList.ownerDocument; - let messageLabel = doc.createElementNS(kNSXUL, "label"); - messageLabel.textContent = message; - appendTo.appendChild(messageLabel); - return messageLabel; - }, - _appendClient: function (client, attachFragment) { - let doc = attachFragment.ownerDocument; - // Create the element for the remote client. - let clientItem = doc.createElementNS(kNSXUL, "label"); - clientItem.setAttribute("itemtype", "client"); - let window = doc.defaultView; - clientItem.setAttribute("tooltiptext", - window.gSyncUI.formatLastSyncDate(new Date(client.lastModified))); - clientItem.textContent = client.name; - - attachFragment.appendChild(clientItem); - - if (client.tabs.length == 0) { - let label = this._appendMessageLabel("notabsforclientlabel", attachFragment); - label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label"); - } else { - for (let tab of client.tabs) { - let tabEnt = this._createTabElement(doc, tab); - attachFragment.appendChild(tabEnt); - } - } - }, - _createTabElement(doc, tabInfo) { - let item = doc.createElementNS(kNSXUL, "toolbarbutton"); - let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url; - item.setAttribute("itemtype", "tab"); - item.setAttribute("class", "subviewbutton"); - item.setAttribute("targetURI", tabInfo.url); - item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url); - item.setAttribute("image", tabInfo.icon); - item.setAttribute("tooltiptext", tooltipText); - // We need to use "click" instead of "command" here so openUILink - // respects different buttons (eg, to open in a new tab). - item.addEventListener("click", e => { - doc.defaultView.openUILink(tabInfo.url, e); - CustomizableUI.hidePanelForNode(item); - }); - return item; - }, - }, { id: "privatebrowsing-button", shortcutId: "key_privatebrowsing", defaultArea: CustomizableUI.AREA_PANEL, diff --git a/application/basilisk/components/customizableui/content/panelUI.inc.xul b/application/basilisk/components/customizableui/content/panelUI.inc.xul index 8ebd93327..da8077554 100644 --- a/application/basilisk/components/customizableui/content/panelUI.inc.xul +++ b/application/basilisk/components/customizableui/content/panelUI.inc.xul @@ -20,27 +20,6 @@ oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);" wrap="true" hidden="true"/> - <hbox id="PanelUI-footer-fxa"> - <hbox id="PanelUI-fxa-status" - defaultlabel="&fxaSignIn.label;" - signedinTooltiptext="&syncSettings.label;" - tooltiptext="&syncSettings.label;" - errorlabel="&fxaSignInError.label;" - unverifiedlabel="&fxaUnverified.label;" - settingslabel="&syncSettings.label;" - onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();"> - <image id="PanelUI-fxa-avatar"/> - <toolbarbutton id="PanelUI-fxa-label" - fxabrandname="&syncBrand.fxAccount.label;"/> - </hbox> - <toolbarseparator/> - <toolbarbutton id="PanelUI-fxa-icon" - oncommand="gSyncUI.doSync();" - closemenu="none"> - <observes element="sync-status" attribute="syncstatus"/> - <observes element="sync-status" attribute="tooltiptext"/> - </toolbarbutton> - </hbox> <hbox id="PanelUI-footer-inner"> <toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;" @@ -103,95 +82,6 @@ oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/> </panelview> - <panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView"> - <label value="&appMenuRemoteTabs.label;" class="panel-subview-header"/> - <vbox class="panel-subview-body"> - <!-- this widget has 3 boxes in the body, but only 1 is ever visible --> - <!-- When Sync is ready to sync --> - <vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state"> - <vbox id="PanelUI-remotetabs-buttons"> - <toolbarbutton id="PanelUI-remotetabs-view-sidebar" - class="subviewbutton" - oncommand="BrowserOpenSyncTabs();" - label="&appMenuRemoteTabs.sidebar.label;"/> - <toolbarbutton id="PanelUI-remotetabs-syncnow" - observes="sync-status" - class="subviewbutton" - oncommand="gSyncUI.doSync();" - closemenu="none"/> - <menuseparator id="PanelUI-remotetabs-separator"/> - </vbox> - <deck id="PanelUI-remotetabs-deck"> - <!-- Sync is ready to Sync and the "tabs" engine is enabled --> - <vbox id="PanelUI-remotetabs-tabspane"> - <vbox id="PanelUI-remotetabs-tabslist" - notabsforclientlabel="&appMenuRemoteTabs.notabs.label;" - /> - </vbox> - <!-- Sync is ready to Sync but the "tabs" engine isn't enabled--> - <hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1"> - <vbox class="PanelUI-remotetabs-instruction-box"> - <hbox pack="center"> - <image class="fxaSyncIllustration" alt=""/> - </hbox> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label> - <hbox pack="center"> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.openprefs.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </hbox> - </vbox> - </hbox> - <!-- Sync is ready to Sync but we are still fetching the tabs to show --> - <vbox id="PanelUI-remotetabs-fetching"> - <!-- Show intentionally blank panel, see bug 1239845 --> - </vbox> - <!-- Sync has only 1 (ie, this) device connected --> - <hbox id="PanelUI-remotetabs-nodevicespane" pack="center" flex="1"> - <vbox class="PanelUI-remotetabs-instruction-box"> - <hbox pack="center"> - <image class="fxaSyncIllustration" alt=""/> - </hbox> - <label class="PanelUI-remotetabs-instruction-title">&appMenuRemoteTabs.noclients.title;</label> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.subtitle;</label> - <!-- The inner HTML for PanelUI-remotetabs-mobile-promo is built at runtime --> - <label id="PanelUI-remotetabs-mobile-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"/> - </vbox> - </hbox> - </deck> - </vbox> - <!-- a box to ensure contained boxes are centered horizonally --> - <hbox pack="center" flex="1"> - <!-- When Sync is not configured --> - <vbox id="PanelUI-remotetabs-setupsync" - flex="1" - align="center" - class="PanelUI-remotetabs-instruction-box" - observes="sync-setup-state"> - <image class="fxaSyncIllustration" alt=""/> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.signin.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </vbox> - <!-- When Sync needs re-authentication. This uses the exact same messaging - as "Sync is not configured" but remains a separate box so we get - the goodness of observing broadcasters to manage the hidden states --> - <vbox id="PanelUI-remotetabs-reauthsync" - flex="1" - align="center" - class="PanelUI-remotetabs-instruction-box" - observes="sync-reauth-state"> - <image class="fxaSyncIllustration" alt=""/> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.signin.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </vbox> - </hbox> - </vbox> - </panelview> - <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView"> <label value="&bookmarksMenu.label;" class="panel-subview-header"/> <vbox class="panel-subview-body"> diff --git a/application/basilisk/components/customizableui/moz.build b/application/basilisk/components/customizableui/moz.build index 034630dc9..5797a03b0 100644 --- a/application/basilisk/components/customizableui/moz.build +++ b/application/basilisk/components/customizableui/moz.build @@ -9,7 +9,6 @@ DIRS += [ ] EXTRA_JS_MODULES += [ - 'CustomizableUI.jsm', 'CustomizableWidgets.jsm', 'CustomizeMode.jsm', 'DragPositionManager.jsm', @@ -17,5 +16,9 @@ EXTRA_JS_MODULES += [ 'ScrollbarSampler.jsm', ] +EXTRA_PP_JS_MODULES += [ + 'CustomizableUI.jsm', +] + if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'): DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1 diff --git a/application/basilisk/components/moz.build b/application/basilisk/components/moz.build index 65e8beb76..a9c29936b 100644 --- a/application/basilisk/components/moz.build +++ b/application/basilisk/components/moz.build @@ -19,10 +19,12 @@ DIRS += [ 'sessionstore', 'shell', 'selfsupport', - 'syncedtabs', 'translation', ] +if CONFIG['MOZ_SERVICES_SYNC']: + DIRS += ['sync'] + DIRS += ['build'] XPIDL_SOURCES += [ @@ -35,6 +37,9 @@ XPIDL_MODULE = 'browsercompsbase' EXTRA_COMPONENTS += [ 'BrowserComponents.manifest', 'nsBrowserContentHandler.js', +] + +EXTRA_PP_COMPONENTS += [ 'nsBrowserGlue.js', ] diff --git a/application/basilisk/components/nsBrowserGlue.js b/application/basilisk/components/nsBrowserGlue.js index f2eb8fdf3..b4256523f 100644 --- a/application/basilisk/components/nsBrowserGlue.js +++ b/application/basilisk/components/nsBrowserGlue.js @@ -143,6 +143,7 @@ BrowserGlue.prototype = { Services.prefs.savePrefFile(null); }, +#ifdef MOZ_SERVICES_SYNC _setSyncAutoconnectDelay: function BG__setSyncAutoconnectDelay() { // Assume that a non-zero value for services.sync.autoconnectDelay should override if (Services.prefs.prefHasUserValue("services.sync.autoconnectDelay")) { @@ -164,6 +165,7 @@ BrowserGlue.prototype = { Cu.import("resource://services-sync/main.js"); Weave.Service.scheduler.delayedAutoConnect(delay); }, +#endif // nsIObserver implementation observe: function BG_observe(subject, topic, data) { @@ -210,18 +212,14 @@ BrowserGlue.prototype = { this._setPrefToSaveSession(); } break; +#ifdef MOZ_SERVICES_SYNC case "weave:service:ready": this._setSyncAutoconnectDelay(); break; - case "fxaccounts:onverified": - this._showSyncStartedDoorhanger(); - break; - case "fxaccounts:device_disconnected": - this._onDeviceDisconnected(); - break; - case "weave:engine:clients:display-uris": - this._onDisplaySyncURIs(subject); - break; + case "weave:engine:clients:display-uri": + this._onDisplaySyncURI(subject); + break; +#endif case "session-save": this._setPrefToSaveSession(true); subject.QueryInterface(Ci.nsISupportsPRBool); @@ -428,10 +426,10 @@ BrowserGlue.prototype = { os.addObserver(this, "browser-lastwindow-close-requested", false); os.addObserver(this, "browser-lastwindow-close-granted", false); } +#ifdef MOZ_SERVICES_SYNC os.addObserver(this, "weave:service:ready", false); - os.addObserver(this, "fxaccounts:onverified", false); - os.addObserver(this, "fxaccounts:device_disconnected", false); - os.addObserver(this, "weave:engine:clients:display-uris", false); + os.addObserver(this, "weave:engine:clients:display-uri", false); +#endif os.addObserver(this, "session-save", false); os.addObserver(this, "places-init-complete", false); this._isPlacesInitObserver = true; @@ -479,10 +477,10 @@ BrowserGlue.prototype = { os.removeObserver(this, "browser-lastwindow-close-requested"); os.removeObserver(this, "browser-lastwindow-close-granted"); } +#ifdef MOZ_SERVICES_SYNC os.removeObserver(this, "weave:service:ready"); - os.removeObserver(this, "fxaccounts:onverified"); - os.removeObserver(this, "fxaccounts:device_disconnected"); - os.removeObserver(this, "weave:engine:clients:display-uris"); + os.removeObserver(this, "weave:engine:clients:display-uri"); +#endif os.removeObserver(this, "session-save"); if (this._bookmarksBackupIdleTime) { this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime); @@ -2255,90 +2253,29 @@ BrowserGlue.prototype = { chromeWindow.openPreferences(...args); }, +#ifdef MOZ_SERVICES_SYNC /** - * Called as an observer when Sync's "display URIs" notification is fired. + * Called as an observer when Sync's "display URI" notification is fired. + * + * We open the received URI in a background tab. * - * We open the received URIs in background tabs. + * Eventually, this will likely be replaced by a more robust tab syncing + * feature. This functionality is considered somewhat evil by UX because it + * opens a new tab automatically without any prompting. However, it is a + * lesser evil than sending a tab to a specific device (from e.g. Fennec) + * and having nothing happen on the receiving end. */ - _onDisplaySyncURIs: function _onDisplaySyncURIs(data) { + _onDisplaySyncURI: function _onDisplaySyncURI(data) { try { - // The payload is wrapped weirdly because of how Sync does notifications. - const URIs = data.wrappedJSObject.object; - - const findWindow = () => RecentWindow.getMostRecentBrowserWindow({private: false}); - - // win can be null, but it's ok, we'll assign it later in openTab() - let win = findWindow(); + let tabbrowser = RecentWindow.getMostRecentBrowserWindow({private: false}).gBrowser; - const openTab = URI => { - let tab; - if (!win) { - Services.appShell.hiddenDOMWindow.open(URI.uri); - win = findWindow(); - tab = win.gBrowser.tabs[0]; - } else { - tab = win.gBrowser.addTab(URI.uri); - } - tab.setAttribute("attention", true); - return tab; - }; - - const firstTab = openTab(URIs[0]); - URIs.slice(1).forEach(URI => openTab(URI)); - - let title, body; - const deviceName = Weave.Service.clientsEngine.getClientName(URIs[0].clientId); - const bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties"); - if (URIs.length == 1) { - // Due to bug 1305895, tabs from iOS may not have device information, so - // we have separate strings to handle those cases. (See Also - // unnamedTabsArrivingNotificationNoDevice.body below) - if (deviceName) { - title = bundle.formatStringFromName("tabArrivingNotificationWithDevice.title", [deviceName], 1); - } else { - title = bundle.GetStringFromName("tabArrivingNotification.title"); - } - // Use the page URL as the body. We strip the fragment and query to - // reduce size, and also format it the same way that the url bar would. - body = URIs[0].uri.replace(/[?#].*$/, ""); - if (win.gURLBar) { - body = win.gURLBar.trimValue(body); - } - } else { - title = bundle.GetStringFromName("tabsArrivingNotification.title"); - const allSameDevice = URIs.every(URI => URI.clientId == URIs[0].clientId); - const unknownDevice = allSameDevice && !deviceName; - let tabArrivingBody; - if (unknownDevice) { - tabArrivingBody = "unnamedTabsArrivingNotificationNoDevice.body"; - } else if (allSameDevice) { - tabArrivingBody = "unnamedTabsArrivingNotification2.body"; - } else { - tabArrivingBody = "unnamedTabsArrivingNotificationMultiple2.body" - } - - body = bundle.GetStringFromName(tabArrivingBody); - body = PluralForm.get(URIs.length, body); - body = body.replace("#1", URIs.length); - body = body.replace("#2", deviceName); - } - - const clickCallback = (subject, topic, data) => { - if (topic == "alertclickcallback") { - win.gBrowser.selectedTab = firstTab; - } - } - - // Specify an icon because on Windows no icon is shown at the moment - let imageURL; - if (AppConstants.platform == "win") { - imageURL = "chrome://branding/content/icon64.png"; - } - AlertsService.showAlertNotification(imageURL, title, body, true, null, clickCallback); + // The payload is wrapped weirdly because of how Sync does notifications. + tabbrowser.addTab(data.wrappedJSObject.object.uri); } catch (ex) { - Cu.reportError("Error displaying tab(s) received by Sync: " + ex); + Cu.reportError("Error displaying tab received by Sync: " + ex); } }, +#endif _onDeviceDisconnected() { let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties"); diff --git a/application/basilisk/components/places/PlacesUIUtils.jsm b/application/basilisk/components/places/PlacesUIUtils.jsm index 17fa276aa..035fc12c2 100644 --- a/application/basilisk/components/places/PlacesUIUtils.jsm +++ b/application/basilisk/components/places/PlacesUIUtils.jsm @@ -1418,9 +1418,9 @@ this.PlacesUIUtils = { }, shouldShowTabsFromOtherComputersMenuitem: function() { - let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED && - Weave.Svc.Prefs.get("firstSync", "") != "notReady"; - return weaveOK; +#ifdef MOZ_SERVICES_SYNC + // Weave code to enable menu item +#endif }, /** diff --git a/application/basilisk/components/places/moz.build b/application/basilisk/components/places/moz.build index 9e5a2c074..ea6d43538 100644 --- a/application/basilisk/components/places/moz.build +++ b/application/basilisk/components/places/moz.build @@ -6,6 +6,6 @@ JAR_MANIFESTS += ['jar.mn'] -EXTRA_JS_MODULES += [ +EXTRA_PP_JS_MODULES += [ 'PlacesUIUtils.jsm', ] diff --git a/application/basilisk/components/preferences/in-content/jar.mn b/application/basilisk/components/preferences/in-content/jar.mn index 70544f332..21f5ccf38 100644 --- a/application/basilisk/components/preferences/in-content/jar.mn +++ b/application/basilisk/components/preferences/in-content/jar.mn @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: - content/browser/preferences/in-content/preferences.js +* content/browser/preferences/in-content/preferences.js * content/browser/preferences/in-content/preferences.xul content/browser/preferences/in-content/subdialogs.js @@ -12,6 +12,8 @@ browser.jar: content/browser/preferences/in-content/advanced.js content/browser/preferences/in-content/applications.js * content/browser/preferences/in-content/content.js +#ifdef MOZ_SERVICES_SYNC content/browser/preferences/in-content/sync.js +#endif * content/browser/preferences/in-content/security.js content/browser/preferences/in-content/search.js diff --git a/application/basilisk/components/preferences/in-content/preferences.js b/application/basilisk/components/preferences/in-content/preferences.js index 35e10c58d..69cb180d5 100644 --- a/application/basilisk/components/preferences/in-content/preferences.js +++ b/application/basilisk/components/preferences/in-content/preferences.js @@ -64,7 +64,9 @@ function init_all() { register_module("paneAdvanced", gAdvancedPane); register_module("paneApplications", gApplicationsPane); register_module("paneContent", gContentPane); +#ifdef MOZ_SERVICES_SYNC register_module("paneSync", gSyncPane); +#endif register_module("paneSecurity", gSecurityPane); let categories = document.getElementById("categories"); diff --git a/application/basilisk/components/preferences/in-content/preferences.xul b/application/basilisk/components/preferences/in-content/preferences.xul index 093516140..61639aa19 100644 --- a/application/basilisk/components/preferences/in-content/preferences.xul +++ b/application/basilisk/components/preferences/in-content/preferences.xul @@ -22,8 +22,10 @@ <!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd"> <!ENTITY % tabsDTD SYSTEM "chrome://browser/locale/preferences/tabs.dtd"> <!ENTITY % searchDTD SYSTEM "chrome://browser/locale/preferences/search.dtd"> +#ifdef MOZ_SERVICES_SYNC <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> <!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd"> +#endif <!ENTITY % securityDTD SYSTEM "chrome://browser/locale/preferences/security.dtd"> <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> @@ -40,8 +42,10 @@ %privacyDTD; %tabsDTD; %searchDTD; +#ifdef MOZ_SERVICES_SYNC %syncBrandDTD; %syncDTD; +#endif %securityDTD; %sanitizeDTD; %mainDTD; @@ -140,6 +144,7 @@ <label class="category-name" flex="1">&paneSecurity.title;</label> </richlistitem> +#ifdef MOZ_SERVICES_SYNC <richlistitem id="category-sync" class="category" value="paneSync" @@ -149,6 +154,7 @@ <image class="category-icon"/> <label class="category-name" flex="1">&paneSync.title;</label> </richlistitem> +#endif <richlistitem id="category-advanced" class="category" @@ -177,7 +183,9 @@ #include applications.xul #include content.xul #include security.xul +#ifdef MOZ_SERVICES_SYNC #include sync.xul +#endif </prefpane> </vbox> diff --git a/application/basilisk/components/preferences/in-content/sync.js b/application/basilisk/components/preferences/in-content/sync.js index 9496d34b6..917b5f123 100644 --- a/application/basilisk/components/preferences/in-content/sync.js +++ b/application/basilisk/components/preferences/in-content/sync.js @@ -1,32 +1,16 @@ /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ Components.utils.import("resource://services-sync/main.js"); Components.utils.import("resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () { - return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {}); -}); - -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - const PAGE_NO_ACCOUNT = 0; const PAGE_HAS_ACCOUNT = 1; const PAGE_NEEDS_UPDATE = 2; -const FXA_PAGE_LOGGED_OUT = 3; -const FXA_PAGE_LOGGED_IN = 4; - -// Indexes into the "login status" deck. -// We are in a successful verified state - everything should work! -const FXA_LOGIN_VERIFIED = 0; -// We have logged in to an unverified account. -const FXA_LOGIN_UNVERIFIED = 1; -// We are logged in locally, but the server rejected our credentials. -const FXA_LOGIN_FAILED = 2; var gSyncPane = { + _stringBundle: null, prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs", "engine.tabs", "engine.history"], @@ -45,13 +29,11 @@ var gSyncPane = { needsUpdate: function () { this.page = PAGE_NEEDS_UPDATE; let label = document.getElementById("loginError"); - label.textContent = Weave.Utils.getErrorString(Weave.Status.login); + label.value = Weave.Utils.getErrorString(Weave.Status.login); label.className = "error"; }, init: function () { - this._setupEventListeners(); - // If the Service hasn't finished initializing, wait for it. let xps = Components.classes["@mozilla.org/weave/service;1"] .getService(Components.interfaces.nsISupports) @@ -62,10 +44,6 @@ var gSyncPane = { return; } - // it may take some time before we can determine what provider to use - // and the state of that provider, so show the "please wait" page. - this._showLoadPage(xps); - let onUnload = function () { window.removeEventListener("unload", onUnload, false); try { @@ -85,238 +63,50 @@ var gSyncPane = { xps.ensureLoaded(); }, - _showLoadPage: function (xps) { - let username = Services.prefs.getCharPref("services.sync.username", ""); - if (!username) { - this.page = FXA_PAGE_LOGGED_OUT; - } else if (xps.fxAccountsEnabled) { - // Use cached values while we wait for the up-to-date values - let cachedComputerName = Services.prefs.getCharPref("services.sync.client.name", ""); - document.getElementById("fxaEmailAddress1").textContent = username; - this._populateComputerName(cachedComputerName); - this.page = FXA_PAGE_LOGGED_IN; - } else { // Old Sync - this.page = PAGE_HAS_ACCOUNT; - } - }, - _init: function () { let topics = ["weave:service:login:error", "weave:service:login:finish", - "weave:service:start-over:finish", + "weave:service:start-over", "weave:service:setup-complete", - "weave:service:logout:finish", - FxAccountsCommon.ONVERIFIED_NOTIFICATION, - FxAccountsCommon.ONLOGIN_NOTIFICATION, - FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, - ]; + "weave:service:logout:finish"]; + // Add the observers now and remove them on unload - // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling // of `this`. Fix in a followup. (bug 583347) topics.forEach(function (topic) { Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this); }, this); - window.addEventListener("unload", function() { topics.forEach(function (topic) { Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this); }, gSyncPane); }, false); - XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => { - return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties"); - }); - - XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => { - return Services.strings.createBundle("chrome://browser/locale/accounts.properties"); - }); - - let url = Services.prefs.getCharPref("identity.mobilepromo.android") + "sync-preferences"; - document.getElementById("fxaMobilePromo-android").setAttribute("href", url); - document.getElementById("fxaMobilePromo-android-hasFxaAccount").setAttribute("href", url); - url = Services.prefs.getCharPref("identity.mobilepromo.ios") + "sync-preferences"; - document.getElementById("fxaMobilePromo-ios").setAttribute("href", url); - document.getElementById("fxaMobilePromo-ios-hasFxaAccount").setAttribute("href", url); - - document.getElementById("tosPP-small-ToS").setAttribute("href", gSyncUtils.tosURL); - document.getElementById("tosPP-normal-ToS").setAttribute("href", gSyncUtils.tosURL); - document.getElementById("tosPP-small-PP").setAttribute("href", gSyncUtils.privacyPolicyURL); - document.getElementById("tosPP-normal-PP").setAttribute("href", gSyncUtils.privacyPolicyURL); - - fxAccounts.promiseAccountsManageURI(this._getEntryPoint()).then(url => { - document.getElementById("verifiedManage").setAttribute("href", url); - }); - + this._stringBundle = + Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties"); this.updateWeavePrefs(); - - this._initProfileImageUI(); - }, - - _toggleComputerNameControls: function(editMode) { - let textbox = document.getElementById("fxaSyncComputerName"); - textbox.disabled = !editMode; - document.getElementById("fxaChangeDeviceName").hidden = editMode; - document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode; - document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode; - }, - - _focusComputerNameTextbox: function() { - let textbox = document.getElementById("fxaSyncComputerName"); - let valLength = textbox.value.length; - textbox.focus(); - textbox.setSelectionRange(valLength, valLength); - }, - - _blurComputerNameTextbox: function() { - document.getElementById("fxaSyncComputerName").blur(); - }, - - _focusAfterComputerNameTextbox: function() { - // Focus the most appropriate element that's *not* the "computer name" box. - Services.focus.moveFocus(window, - document.getElementById("fxaSyncComputerName"), - Services.focus.MOVEFOCUS_FORWARD, 0); - }, - - _updateComputerNameValue: function(save) { - if (save) { - let textbox = document.getElementById("fxaSyncComputerName"); - Weave.Service.clientsEngine.localName = textbox.value; - } - this._populateComputerName(Weave.Service.clientsEngine.localName); - }, - - _setupEventListeners: function() { - function setEventListener(aId, aEventType, aCallback) - { - document.getElementById(aId) - .addEventListener(aEventType, aCallback.bind(gSyncPane)); - } - - setEventListener("noAccountSetup", "click", function (aEvent) { - aEvent.stopPropagation(); - gSyncPane.openSetup(null); - }); - setEventListener("noAccountPair", "click", function (aEvent) { - aEvent.stopPropagation(); - gSyncPane.openSetup('pair'); - }); - setEventListener("syncChangePassword", "command", - () => gSyncUtils.changePassword()); - setEventListener("syncResetPassphrase", "command", - () => gSyncUtils.resetPassphrase()); - setEventListener("syncReset", "command", gSyncPane.resetSync); - setEventListener("syncAddDeviceLabel", "click", function () { - gSyncPane.openAddDevice(); - return false; - }); - setEventListener("syncEnginesList", "select", function () { - if (this.selectedCount) - this.clearSelection(); - }); - setEventListener("syncComputerName", "change", function (e) { - gSyncUtils.changeName(e.target); - }); - setEventListener("fxaChangeDeviceName", "command", function () { - this._toggleComputerNameControls(true); - this._focusComputerNameTextbox(); - }); - setEventListener("fxaCancelChangeDeviceName", "command", function () { - // We explicitly blur the textbox because of bug 75324, then after - // changing the state of the buttons, force focus to whatever the focus - // manager thinks should be next (which on the mac, depends on an OSX - // keyboard access preference) - this._blurComputerNameTextbox(); - this._toggleComputerNameControls(false); - this._updateComputerNameValue(false); - this._focusAfterComputerNameTextbox(); - }); - setEventListener("fxaSaveChangeDeviceName", "command", function () { - // Work around bug 75324 - see above. - this._blurComputerNameTextbox(); - this._toggleComputerNameControls(false); - this._updateComputerNameValue(true); - this._focusAfterComputerNameTextbox(); - }); - setEventListener("unlinkDevice", "click", function () { - gSyncPane.startOver(true); - return false; - }); - setEventListener("loginErrorUpdatePass", "click", function () { - gSyncPane.updatePass(); - return false; - }); - setEventListener("loginErrorResetPass", "click", function () { - gSyncPane.resetPass(); - return false; - }); - setEventListener("loginErrorStartOver", "click", function () { - gSyncPane.startOver(true); - return false; - }); - setEventListener("noFxaSignUp", "command", function () { - gSyncPane.signUp(); - return false; - }); - setEventListener("noFxaSignIn", "command", function () { - gSyncPane.signIn(); - return false; - }); - setEventListener("fxaUnlinkButton", "command", function () { - gSyncPane.unlinkFirefoxAccount(true); - }); - setEventListener("verifyFxaAccount", "command", - gSyncPane.verifyFirefoxAccount); - setEventListener("unverifiedUnlinkFxaAccount", "command", function () { - /* no warning as account can't have previously synced */ - gSyncPane.unlinkFirefoxAccount(false); - }); - setEventListener("rejectReSignIn", "command", - gSyncPane.reSignIn); - setEventListener("rejectUnlinkFxaAccount", "command", function () { - gSyncPane.unlinkFirefoxAccount(true); - }); - setEventListener("fxaSyncComputerName", "keypress", function (e) { - if (e.keyCode == KeyEvent.DOM_VK_RETURN) { - document.getElementById("fxaSaveChangeDeviceName").click(); - } else if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) { - document.getElementById("fxaCancelChangeDeviceName").click(); - } - }); - }, - - _initProfileImageUI: function () { - try { - if (Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled")) { - document.getElementById("fxaProfileImage").hidden = false; - } - } catch (e) { } + document.getElementById("weavePrefsDeck").setAttribute("hidden", ""); }, updateWeavePrefs: function () { - let service = Components.classes["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED || Weave.Svc.Prefs.get("firstSync", "") == "notReady") { this.page = PAGE_NO_ACCOUNT; - // else: sync was previously configured for the legacy provider, so we - // make the "old" panels available. } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE || Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { this.needsUpdate(); } else { this.page = PAGE_HAS_ACCOUNT; - document.getElementById("accountName").textContent = Weave.Service.identity.account; + document.getElementById("accountName").value = Weave.Service.identity.account; document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; - document.getElementById("tosPP-normal").hidden = this._usingCustomServer; + document.getElementById("tosPP").hidden = this._usingCustomServer; } }, startOver: function (showDialog) { if (showDialog) { let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + - Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + Services.prompt.BUTTON_POS_1_DEFAULT; let buttonChoice = Services.prompt.confirmEx(window, @@ -327,8 +117,9 @@ var gSyncPane = { null, null, null, {}); // If the user selects cancel, just bail - if (buttonChoice == 1) + if (buttonChoice == 1) { return; + } } Weave.Service.startOver(); @@ -336,33 +127,19 @@ var gSyncPane = { }, updatePass: function () { - if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { gSyncUtils.changePassword(); - else + } else { gSyncUtils.updatePassphrase(); + } }, resetPass: function () { - if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) + if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) { gSyncUtils.resetPassword(); - else + } else { gSyncUtils.resetPassphrase(); - }, - - _getEntryPoint: function () { - let params = new URLSearchParams(document.URL.split("#")[0].split("?")[1] || ""); - return params.get("entrypoint") || "preferences"; - }, - - _openAboutAccounts: function(action) { - let entryPoint = this._getEntryPoint(); - let params = new URLSearchParams(); - if (action) { - params.set("action", action); } - params.set("entrypoint", entryPoint); - - this.replaceTabWithUrl("about:accounts?" + params); }, /** @@ -375,163 +152,16 @@ var gSyncPane = { * "reset" -- reset sync */ openSetup: function (wizardType) { - let service = Components.classes["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - if (service.fxAccountsEnabled) { - this._openAboutAccounts(); + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) { + win.focus(); } else { - let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); - if (win) - win.focus(); - else { - window.openDialog("chrome://browser/content/sync/setup.xul", - "weaveSetup", "centerscreen,chrome,resizable=no", - wizardType); - } + window.openDialog("chrome://browser/content/sync/setup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); } }, - openContentInBrowser: function(url, options) { - let win = Services.wm.getMostRecentWindow("navigator:browser"); - if (!win) { - // no window to use, so use _openLink to create a new one. We don't - // always use that as it prefers to open a new window rather than use - // an existing one. - gSyncUtils._openLink(url); - return; - } - win.switchToTabHavingURI(url, true, options); - }, - - // Replace the current tab with the specified URL. - replaceTabWithUrl(url) { - // Get the <browser> element hosting us. - let browser = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .chromeEventHandler; - // And tell it to load our URL. - browser.loadURI(url); - }, - - signUp: function() { - this._openAboutAccounts("signup"); - }, - - signIn: function() { - this._openAboutAccounts("signin"); - }, - - reSignIn: function() { - this._openAboutAccounts("reauth"); - }, - - - clickOrSpaceOrEnterPressed: function(event) { - // Note: charCode is deprecated, but 'char' not yet implemented. - // Replace charCode with char when implemented, see Bug 680830 - return ((event.type == "click" && event.button == 0) || - (event.type == "keypress" && - (event.charCode == KeyEvent.DOM_VK_SPACE || event.keyCode == KeyEvent.DOM_VK_RETURN))); - }, - - openChangeProfileImage: function(event) { - if (this.clickOrSpaceOrEnterPressed(event)) { - fxAccounts.promiseAccountsChangeProfileURI(this._getEntryPoint(), "avatar") - .then(url => { - this.openContentInBrowser(url, { - replaceQueryString: true - }); - }); - // Prevent page from scrolling on the space key. - event.preventDefault(); - } - }, - - openManageFirefoxAccount: function(event) { - if (this.clickOrSpaceOrEnterPressed(event)) { - this.manageFirefoxAccount(); - // Prevent page from scrolling on the space key. - event.preventDefault(); - } - }, - - manageFirefoxAccount: function() { - fxAccounts.promiseAccountsManageURI(this._getEntryPoint()) - .then(url => { - this.openContentInBrowser(url, { - replaceQueryString: true - }); - }); - }, - - verifyFirefoxAccount: function() { - let showVerifyNotification = (data) => { - let isError = !data; - let maybeNot = isError ? "Not" : ""; - let sb = this._accountsStringBundle; - let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle"); - let email = !isError && data ? data.email : ""; - let body = sb.formatStringFromName("verification" + maybeNot + "SentBody", [email], 1); - new Notification(title, { body }) - } - - let onError = () => { - showVerifyNotification(); - }; - - let onSuccess = data => { - if (data) { - showVerifyNotification(data); - } else { - onError(); - } - }; - - fxAccounts.resendVerificationEmail() - .then(fxAccounts.getSignedInUser, onError) - .then(onSuccess, onError); - }, - - openOldSyncSupportPage: function() { - let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; - this.openContentInBrowser(url); - }, - - unlinkFirefoxAccount: function(confirm) { - if (confirm) { - // We use a string bundle shared with aboutAccounts. - let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); - let disconnectLabel = sb.GetStringFromName("disconnect.label"); - let title = sb.GetStringFromName("disconnect.verify.title"); - let body = sb.GetStringFromName("disconnect.verify.bodyHeading") + - "\n\n" + - sb.GetStringFromName("disconnect.verify.bodyText"); - 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; - - let factory = Cc["@mozilla.org/prompter;1"] - .getService(Ci.nsIPromptFactory); - let prompt = factory.getPrompt(window, Ci.nsIPrompt); - let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2); - bag.setPropertyAsBool("allowTabModal", true); - - let pressed = prompt.confirmEx(title, body, buttonFlags, - disconnectLabel, null, null, null, {}); - - if (pressed != 0) { // 0 is the "continue" button - return; - } - } - fxAccounts.signOut().then(() => { - this.updateWeavePrefs(); - }); - }, - openQuotaDialog: function () { let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); if (win) { @@ -543,27 +173,21 @@ var gSyncPane = { }, openAddDevice: function () { - if (!Weave.Utils.ensureMPUnlocked()) + if (!Weave.Utils.ensureMPUnlocked()) { return; + } let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); - if (win) + if (win) { win.focus(); - else + } else { window.openDialog("chrome://browser/content/sync/addDevice.xul", "syncAddDevice", "centerscreen,chrome,resizable=no"); + } }, resetSync: function () { this.openSetup("reset"); }, - - _populateComputerName(value) { - let textbox = document.getElementById("fxaSyncComputerName"); - if (!textbox.hasAttribute("placeholder")) { - textbox.setAttribute("placeholder", - Weave.Utils.getDefaultDeviceName()); - } - textbox.value = value; - }, }; + diff --git a/application/basilisk/components/preferences/in-content/sync.xul b/application/basilisk/components/preferences/in-content/sync.xul index cf0e81ca1..7ca075483 100644 --- a/application/basilisk/components/preferences/in-content/sync.xul +++ b/application/basilisk/components/preferences/in-content/sync.xul @@ -1,35 +1,22 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.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 panel --> - -<preferences id="syncEnginePrefs" hidden="true" data-category="paneSync"> - <preference id="engine.addons" - name="services.sync.engine.addons" - type="bool"/> - <preference id="engine.bookmarks" - name="services.sync.engine.bookmarks" - type="bool"/> - <preference id="engine.history" - name="services.sync.engine.history" - type="bool"/> - <preference id="engine.tabs" - name="services.sync.engine.tabs" - type="bool"/> - <preference id="engine.prefs" - name="services.sync.engine.prefs" - type="bool"/> - <preference id="engine.passwords" - name="services.sync.engine.passwords" - type="bool"/> -</preferences> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <script type="application/javascript" src="chrome://browser/content/preferences/in-content/sync.js"/> <script type="application/javascript" src="chrome://browser/content/sync/utils.js"/> +<preferences> +<!-- <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/> --> + <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/> + <preference id="engine.history" name="services.sync.engine.history" type="bool"/> + <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/> + <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/> + <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/> +</preferences> + + <hbox id="header-sync" class="header" hidden="true" @@ -39,22 +26,19 @@ </hbox> <deck id="weavePrefsDeck" data-category="paneSync" hidden="true"> - <!-- These panels are for the "legacy" sync provider --> <vbox id="noAccount" align="center"> <spacer flex="1"/> <description id="syncDesc"> &weaveDesc.label; </description> <separator/> - <label id="noAccountSetup" class="text-link"> - &setupButton.label; - </label> - <vbox id="pairDevice"> - <separator/> - <label id="noAccountPair" class="text-link"> - &pairDevice.label; - </label> - </vbox> + <label class="text-link" + onclick="event.stopPropagation(); gSyncPane.openSetup(null);" + value="&setupButton.label;"/> + <separator/> + <label class="text-link" + onclick="event.stopPropagation(); gSyncPane.openSetup('pair');" + value="&pairDevice.label;"/> <spacer flex="3"/> </vbox> @@ -63,7 +47,7 @@ <!-- label is set to account name --> <caption id="accountCaption" align="center"> <image id="accountCaptionImage"/> - <label id="accountName"/> + <label id="accountName" value=""/> </caption> <hbox> @@ -71,28 +55,39 @@ label="&manageAccount.label;" accesskey="&manageAccount.accesskey;"> <menupopup> - <menuitem id="syncViewQuota" label="&viewQuota.label;" + <menuitem label="&viewQuota.label;" oncommand="gSyncPane.openQuotaDialog();"/> - <menuseparator/> - <menuitem id="syncChangePassword" label="&changePassword2.label;"/> - <menuitem id="syncResetPassphrase" label="&myRecoveryKey.label;"/> <menuseparator/> - <menuitem id="syncReset" label="&resetSync2.label;"/> + <menuitem label="&changePassword2.label;" + oncommand="gSyncUtils.changePassword();"/> + <menuitem label="&myRecoveryKey.label;" + oncommand="gSyncUtils.resetPassphrase();"/> + <menuseparator/> + <menuitem label="&resetSync2.label;" + oncommand="gSyncPane.resetSync();"/> </menupopup> </button> </hbox> <hbox> <label id="syncAddDeviceLabel" - class="text-link"> - &pairDevice.label; - </label> + class="text-link" + onclick="gSyncPane.openAddDevice(); return false;" + value="&pairDevice.label;"/> </hbox> <vbox> - <label>&syncMy.label;</label> + <label value="&syncMy.label;" /> <richlistbox id="syncEnginesList" - orient="vertical"> + orient="vertical" + onselect="if (this.selectedCount) this.clearSelection();"> + <!-- + <richlistitem> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + </richlistitem> + --> <richlistitem> <checkbox label="&engine.bookmarks.label;" accesskey="&engine.bookmarks.accesskey;" @@ -130,228 +125,42 @@ </columns> <rows> <row align="center"> - <label control="syncComputerName"> - &syncDeviceName.label; - </label> - <textbox id="syncComputerName"/> + <label value="&syncDeviceName.label;" + accesskey="&syncDeviceName.accesskey;" + control="syncComputerName"/> + <textbox id="syncComputerName" + onchange="gSyncUtils.changeName(this)"/> </row> </rows> </grid> <hbox> - <label id="unlinkDevice" class="text-link"> - &unlinkDevice.label; - </label> + <label class="text-link" + onclick="gSyncPane.startOver(true); return false;" + value="&unlinkDevice.label;"/> </hbox> </groupbox> - <vbox id="tosPP-normal"> - <label id="tosPP-normal-ToS" class="text-link"> - &prefs.tosLink.label; - </label> - <label id="tosPP-normal-PP" class="text-link"> - &prefs.ppLink.label; - </label> - </vbox> - </vbox> - - <vbox id="needsUpdate" align="center" pack="center"> - <hbox> - <label id="loginError"/> - <label id="loginErrorUpdatePass" class="text-link"> - &updatePass.label; - </label> - <label id="loginErrorResetPass" class="text-link"> - &resetPass.label; - </label> + <hbox id="tosPP" pack="center"> + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openToS();" + value="&prefs.tosLink.label;"/> + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();" + value="&prefs.ppLink.label;"/> </hbox> - <label id="loginErrorStartOver" class="text-link"> - &unlinkDevice.label; - </label> </vbox> - <!-- These panels are for the Firefox Accounts identity provider --> - <vbox id="noFxaAccount"> - <hbox> - <vbox id="fxaContentWrapper"> - <groupbox id="noFxaGroup"> - <vbox> - <label id="noFxaCaption">&signedOut.caption;</label> - <description id="noFxaDescription" flex="1">&signedOut.description;</description> - <hbox class="fxaAccountBox"> - <vbox> - <image class="fxaFirefoxLogo"/> - </vbox> - <vbox flex="1"> - <label id="signedOutAccountBoxTitle">&signedOut.accountBox.title;</label> - <hbox class="fxaAccountBoxButtons"> - <button id="noFxaSignUp" label="&signedOut.accountBox.create;" accesskey="&signedOut.accountBox.create.accesskey;"></button> - <button id="noFxaSignIn" label="&signedOut.accountBox.signin;" accesskey="&signedOut.accountBox.signin.accesskey;"></button> - </hbox> - </vbox> - </hbox> - </vbox> - </groupbox> - </vbox> - <vbox> - <image class="fxaSyncIllustration"/> - </vbox> - </hbox> - <label class="fxaMobilePromo"> - &mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces - --><label id="fxaMobilePromo-android" - class="androidLink text-link"><!-- - -->&mobilePromo3.androidLink;</label><!-- - -->&mobilePromo3.iOSBefore;<!-- - --><label id="fxaMobilePromo-ios" - class="iOSLink text-link"><!-- - -->&mobilePromo3.iOSLink;</label><!-- - -->&mobilePromo3.end; - </label> - </vbox> - - <vbox id="hasFxaAccount"> + <vbox id="needsUpdate" align="center" pack="center"> <hbox> - <vbox id="fxaContentWrapper"> - <groupbox id="fxaGroup"> - <caption><label>&syncBrand.fxAccount.label;</label></caption> - <deck id="fxaLoginStatus"> - - <!-- logged in and verified and all is good --> - <hbox id="fxaLoginVerified" class="fxaAccountBox"> - <vbox align="center" pack="center"> - <image id="fxaProfileImage" class="actionable" - role="button" - onclick="gSyncPane.openChangeProfileImage(event);" hidden="true" - onkeypress="gSyncPane.openChangeProfileImage(event);" - tooltiptext="&profilePicture.tooltip;"/> - </vbox> - <vbox flex="1" pack="center"> - <label id="fxaDisplayName" hidden="true"/> - <label id="fxaEmailAddress1"/> - <hbox class="fxaAccountBoxButtons"> - <button id="fxaUnlinkButton" label="&disconnect.label;" accesskey="&disconnect.accesskey;"/> - <html:a id="verifiedManage" target="_blank" - accesskey="&verifiedManage.accesskey;" - onkeypress="gSyncPane.openManageFirefoxAccount(event);"><!-- - -->&verifiedManage.label;</html:a> - </hbox> - </vbox> - </hbox> - - <!-- logged in to an unverified account --> - <hbox id="fxaLoginUnverified" class="fxaAccountBox"> - <vbox> - <image id="fxaProfileImage"/> - </vbox> - <vbox flex="1"> - <hbox> - <vbox><image id="fxaLoginRejectedWarning"/></vbox> - <description flex="1"> - &signedInUnverified.beforename.label; - <label id="fxaEmailAddress2"/> - &signedInUnverified.aftername.label; - </description> - </hbox> - <hbox class="fxaAccountBoxButtons"> - <button id="verifyFxaAccount" accesskey="&verify.accesskey;">&verify.label;</button> - <button id="unverifiedUnlinkFxaAccount" accesskey="&forget.accesskey;">&forget.label;</button> - </hbox> - </vbox> - </hbox> - - <!-- logged in locally but server rejected credentials --> - <hbox id="fxaLoginRejected" class="fxaAccountBox"> - <vbox> - <image id="fxaProfileImage"/> - </vbox> - <vbox flex="1"> - <hbox> - <vbox><image id="fxaLoginRejectedWarning"/></vbox> - <description flex="1"> - &signedInLoginFailure.beforename.label; - <label id="fxaEmailAddress3"/> - &signedInLoginFailure.aftername.label; - </description> - </hbox> - <hbox class="fxaAccountBoxButtons"> - <button id="rejectReSignIn" accessky="&signIn.accesskey;">&signIn.label;</button> - <button id="rejectUnlinkFxaAccount" accesskey="&forget.accesskey;">&forget.label;</button> - </hbox> - </vbox> - </hbox> - </deck> - </groupbox> - <groupbox id="syncOptions"> - <caption><label>&signedIn.engines.label;</label></caption> - <hbox id="fxaSyncEngines"> - <vbox align="start" flex="1"> - <checkbox label="&engine.tabs.label;" - accesskey="&engine.tabs.accesskey;" - preference="engine.tabs"/> - <checkbox label="&engine.bookmarks.label;" - accesskey="&engine.bookmarks.accesskey;" - preference="engine.bookmarks"/> - <checkbox label="&engine.passwords.label;" - accesskey="&engine.passwords.accesskey;" - preference="engine.passwords"/> - </vbox> - <vbox align="start" flex="1"> - <checkbox label="&engine.history.label;" - accesskey="&engine.history.accesskey;" - preference="engine.history"/> - <checkbox label="&engine.addons.label;" - accesskey="&engine.addons.accesskey;" - preference="engine.addons"/> - <checkbox label="&engine.prefs.label;" - accesskey="&engine.prefs.accesskey;" - preference="engine.prefs"/> - </vbox> - <spacer/> - </hbox> - </groupbox> - </vbox> - <vbox> - <image class="fxaSyncIllustration"/> - </vbox> + <label id="loginError" value=""/> + <label class="text-link" + onclick="gSyncPane.updatePass(); return false;" + value="&updatePass.label;"/> + <label class="text-link" + onclick="gSyncPane.resetPass(); return false;" + value="&resetPass.label;"/> </hbox> - <groupbox> - <caption> - <label control="fxaSyncComputerName"> - &fxaSyncDeviceName.label; - </label> - </caption> - <hbox id="fxaDeviceName"> - <textbox id="fxaSyncComputerName" disabled="true"/> - <hbox> - <button id="fxaChangeDeviceName" - label="&changeSyncDeviceName.label;" - accesskey="&changeSyncDeviceName.accesskey;"/> - <button id="fxaCancelChangeDeviceName" - label="&cancelChangeSyncDeviceName.label;" - accesskey="&cancelChangeSyncDeviceName.accesskey;" - hidden="true"/> - <button id="fxaSaveChangeDeviceName" - label="&saveChangeSyncDeviceName.label;" - accesskey="&saveChangeSyncDeviceName.accesskey;" - hidden="true"/> - </hbox> - </hbox> - </groupbox> - <label class="fxaMobilePromo"> - &mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces - --><label class="androidLink text-link" id="fxaMobilePromo-android-hasFxaAccount"><!-- - -->&mobilePromo3.androidLink;</label><!-- - -->&mobilePromo3.iOSBefore;<!-- - --><label class="iOSLink text-link" id="fxaMobilePromo-ios-hasFxaAccount"><!-- - -->&mobilePromo3.iOSLink;</label><!-- - -->&mobilePromo3.end; - </label> - <vbox id="tosPP-small" align="start"> - <label id="tosPP-small-ToS" class="text-link"> - &prefs.tosLink.label; - </label> - <label id="tosPP-small-PP" class="text-link"> - &fxaPrivacyNotice.link.label; - </label> - </vbox> + <label class="text-link" + onclick="gSyncPane.startOver(true); return false;" + value="&unlinkDevice.label;"/> </vbox> </deck> diff --git a/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml b/application/basilisk/components/sync/aboutSyncTabs-bindings.xml index e6108209a..e6108209a 100644 --- a/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml +++ b/application/basilisk/components/sync/aboutSyncTabs-bindings.xml diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.css b/application/basilisk/components/sync/aboutSyncTabs.css index 5a353175b..5a353175b 100644 --- a/application/basilisk/base/content/sync/aboutSyncTabs.css +++ b/application/basilisk/components/sync/aboutSyncTabs.css diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.js b/application/basilisk/components/sync/aboutSyncTabs.js index 69ec71e55..4808c052f 100644 --- a/application/basilisk/base/content/sync/aboutSyncTabs.js +++ b/application/basilisk/components/sync/aboutSyncTabs.js @@ -11,14 +11,6 @@ Cu.import("resource://gre/modules/PlacesUtils.jsm", this); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); - -#ifdef MOZ_SERVICES_CLOUDSYNC -XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", - "resource://gre/modules/CloudSync.jsm"); -#endif - var RemoteTabViewer = { _tabsList: null, @@ -26,8 +18,6 @@ var RemoteTabViewer = { Services.obs.addObserver(this, "weave:service:login:finish", false); Services.obs.addObserver(this, "weave:engine:sync:finish", false); - Services.obs.addObserver(this, "cloudsync:tabs:update", false); - this._tabsList = document.getElementById("tabsList"); this.buildList(true); @@ -36,14 +26,12 @@ var RemoteTabViewer = { uninit: function () { Services.obs.removeObserver(this, "weave:service:login:finish"); Services.obs.removeObserver(this, "weave:engine:sync:finish"); - - Services.obs.removeObserver(this, "cloudsync:tabs:update"); }, - createItem: function (attrs) { + createItem: function(attrs) { let item = document.createElement("richlistitem"); - // Copy the attributes from the argument into the item. + // Copy the attributes from the argument into the item for (let attr in attrs) { item.setAttribute(attr, attrs[attr]); } @@ -55,7 +43,7 @@ var RemoteTabViewer = { return item; }, - filterTabs: function (event) { + filterTabs: function(event) { let val = event.target.value.toLowerCase(); let numTabs = this._tabsList.getRowCount(); let clientTabs = 0; @@ -65,7 +53,7 @@ var RemoteTabViewer = { let item = this._tabsList.getItemAtIndex(i); let hide = false; if (item.getAttribute("type") == "tab") { - if (!item.getAttribute("url").toLowerCase().includes(val) && + if (!item.getAttribute("url").toLowerCase().includes(val) && !item.getAttribute("title").toLowerCase().includes(val)) { hide = true; } else { @@ -88,10 +76,10 @@ var RemoteTabViewer = { } }, - openSelected: function () { + openSelected: function() { let items = this._tabsList.selectedItems; let urls = []; - for (let i = 0; i < items.length; i++) { + for (let i = 0;i < items.length;i++) { if (items[i].getAttribute("type") == "tab") { urls.push(items[i].getAttribute("url")); let index = this._tabsList.getIndexOfItem(items[i]); @@ -104,7 +92,7 @@ var RemoteTabViewer = { } }, - bookmarkSingleTab: function () { + bookmarkSingleTab: function() { let item = this._tabsList.selectedItems[0]; let uri = Weave.Utils.makeURI(item.getAttribute("url")); let title = item.getAttribute("title"); @@ -119,10 +107,10 @@ var RemoteTabViewer = { }, window.top); }, - bookmarkSelectedTabs: function () { + bookmarkSelectedTabs: function() { let items = this._tabsList.selectedItems; let URIs = []; - for (let i = 0; i < items.length; i++) { + for (let i = 0;i < items.length;i++) { if (items[i].getAttribute("type") == "tab") { let uri = Weave.Utils.makeURI(items[i].getAttribute("url")); if (!uri) { @@ -157,7 +145,7 @@ var RemoteTabViewer = { _buildListRequested: false, - buildList: function (forceSync) { + buildList: function (force) { if (this._waitingForBuildList) { this._buildListRequested = true; return; @@ -168,37 +156,27 @@ var RemoteTabViewer = { this._clearTabList(); - if (Weave.Service.isLoggedIn) { - this._refetchTabs(forceSync); + if (Weave.Service.isLoggedIn && this._refetchTabs(force)) { this._generateWeaveTabList(); } else { - // XXXzpao We should say something about not being logged in & not having data + //XXXzpao We should say something about not being logged in & not having data // or tell the appropriate condition. (bug 583344) } - let complete = () => { + function complete() { this._waitingForBuildList = false; if (this._buildListRequested) { CommonUtils.nextTick(this.buildList, this); } } -#ifdef MOZ_SERVICES_CLOUDSYNC - if (CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs()) { - this._generateCloudSyncTabList() - .then(complete, complete); - } else { - complete(); - } -#else complete(); -#endif }, _clearTabList: function () { let list = this._tabsList; - // Clear out existing richlistitems. + // Clear out existing richlistitems let count = list.getRowCount(); if (count > 0) { for (let i = count - 1; i >= 0; i--) { @@ -214,7 +192,7 @@ var RemoteTabViewer = { let seenURLs = new Set(); let localURLs = engine.getOpenURLs(); - for (let [, client] of Object.entries(engine.getAllClients())) { + for (let [guid, client] in Iterator(engine.getAllClients())) { // Create the client node, but don't add it in-case we don't show any tabs let appendClient = true; @@ -248,37 +226,7 @@ var RemoteTabViewer = { } }, - _generateCloudSyncTabList: function () { - let updateTabList = function (remoteTabs) { - let list = this._tabsList; - - for (let client of remoteTabs) { - let clientAttrs = { - type: "client", - clientName: client.name, - }; - - let clientEnt = this.createItem(clientAttrs); - list.appendChild(clientEnt); - - for (let tab of client.tabs) { - let tabAttrs = { - type: "tab", - title: tab.title, - url: tab.url, - icon: this.getIcon(tab.icon), - }; - let tabEnt = this.createItem(tabAttrs); - list.appendChild(tabEnt); - } - } - }.bind(this); - - return CloudSync().tabs.getRemoteTabs() - .then(updateTabList, Promise.reject.bind(Promise)); - }, - - adjustContextMenu: function (event) { + adjustContextMenu: function(event) { let mode = "all"; switch (this._tabsList.selectedItems.length) { case 0: @@ -303,7 +251,7 @@ var RemoteTabViewer = { } }, - _refetchTabs: function (force) { + _refetchTabs: function(force) { if (!force) { // Don't bother refetching tabs if we already did so recently lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch", 0); @@ -314,40 +262,40 @@ var RemoteTabViewer = { } } - // Ask Sync to just do the tabs engine if it can. - Weave.Service.sync(["tabs"]); + // if Clients hasn't synced yet this session, we need to sync it as well. + if (Weave.Service.clientsEngine.lastSync == 0) { + Weave.Service.clientsEngine.sync(); + } + + // Force a sync only for the tabs engine + let engine = Weave.Service.engineManager.get("tabs"); + engine.lastModified = null; + engine.sync(); Services.prefs.setIntPref("services.sync.lastTabFetch", Math.floor(Date.now() / 1000)); return true; }, - observe: function (subject, topic, data) { + observe: function(subject, topic, data) { switch (topic) { case "weave:service:login:finish": - // A login has finished, which means that a Sync is about to start and - // we will eventually get to the "tabs" engine - but try and force the - // tab engine to sync first by passing |true| for the forceSync param. this.buildList(true); break; case "weave:engine:sync:finish": - if (data == "tabs") { - // The tabs engine just finished, so re-build the list without - // forcing a new sync of the tabs engine. + if (subject == "tabs") { this.buildList(false); } break; - case "cloudsync:tabs:update": - this.buildList(false); - break; } }, - handleClick: function (event) { + handleClick: function(event) { if (event.target.getAttribute("type") != "tab") { return; } + if (event.button == 1) { let url = event.target.getAttribute("url"); openUILink(url, event); @@ -356,3 +304,4 @@ var RemoteTabViewer = { } } } + diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.xul b/application/basilisk/components/sync/aboutSyncTabs.xul index a4aa0032f..a4aa0032f 100644 --- a/application/basilisk/base/content/sync/aboutSyncTabs.xul +++ b/application/basilisk/components/sync/aboutSyncTabs.xul diff --git a/application/basilisk/base/content/sync/addDevice.js b/application/basilisk/components/sync/addDevice.js index 0390d4397..0390d4397 100644 --- a/application/basilisk/base/content/sync/addDevice.js +++ b/application/basilisk/components/sync/addDevice.js diff --git a/application/basilisk/base/content/sync/addDevice.xul b/application/basilisk/components/sync/addDevice.xul index 83c3b7b3c..f2371aad0 100644 --- a/application/basilisk/base/content/sync/addDevice.xul +++ b/application/basilisk/components/sync/addDevice.xul @@ -43,7 +43,7 @@ &pairDevice.dialog.description.label; <label class="text-link" value="&addDevice.showMeHow.label;" - href="https://services.mozilla.com/sync/help/add-device"/> + href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> </description> <separator class="groove-thin"/> <description> diff --git a/application/basilisk/base/content/sync/genericChange.js b/application/basilisk/components/sync/genericChange.js index 51a74f1b1..df6639178 100644 --- a/application/basilisk/base/content/sync/genericChange.js +++ b/application/basilisk/components/sync/genericChange.js @@ -32,6 +32,7 @@ var Change = { onLoad: function Change_onLoad() { /* Load labels */ let introText = document.getElementById("introText"); + let introText2 = document.getElementById("introText2"); let warningText = document.getElementById("warningText"); // load some other elements & info from the window @@ -69,7 +70,6 @@ var Change = { else { document.getElementById("generatePassphraseButton").hidden = false; document.getElementById("passphraseBackupButtons").hidden = false; - this._passphraseBox.setAttribute("readonly", "true"); let pp = Weave.Service.identity.syncKey; if (Weave.Utils.isPassphrase(pp)) pp = Weave.Utils.hyphenatePassphrase(pp); @@ -120,7 +120,7 @@ var Change = { _updateStatus: function Change__updateStatus(str, state) { this._updateStatusWithString(this._str(str), state); }, - + _updateStatusWithString: function Change__updateStatusWithString(string, state) { this._statusRow.hidden = false; this._status.value = string; @@ -141,10 +141,11 @@ var Change = { case "UpdatePassphrase": case "ResetPassphrase": return this.doChangePassphrase(); + break; case "ChangePassword": return this.doChangePassword(); + break; } - return undefined; }, doGeneratePassphrase: function () { @@ -212,10 +213,10 @@ var Change = { [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox); } else { - if (!this._updatingPassphrase) - return; - - valid = this._passphraseBox.value != ""; + //Pale Moon: Enforce minimum length of 8 for allowed custom passphrase + //and don't restrict it to "out of sync" situations only. People who + //go to this page generally know what they are doing ;) + valid = this._passphraseBox.value.length >= 8; } if (errorString == "") diff --git a/application/basilisk/base/content/sync/genericChange.xul b/application/basilisk/components/sync/genericChange.xul index db74a1b31..3c0b2cd6c 100644 --- a/application/basilisk/base/content/sync/genericChange.xul +++ b/application/basilisk/components/sync/genericChange.xul @@ -67,7 +67,7 @@ <label id="generatePassphraseButton" hidden="true" value="&syncGenerateNewKey.label;" - class="text-link" + class="text-link inline-link" onclick="event.stopPropagation(); Change.doGeneratePassphrase();"/> </hbox> @@ -105,7 +105,7 @@ <description> &existingRecoveryKey.description; <label class="text-link" - href="https://services.mozilla.com/sync/help/manual-setup"> + href="http://www.palemoon.org/sync/help/recoverykey.shtml"> &addDevice.showMeHow.label; </label> </description> diff --git a/application/basilisk/components/sync/jar.mn b/application/basilisk/components/sync/jar.mn new file mode 100644 index 000000000..3782038cd --- /dev/null +++ b/application/basilisk/components/sync/jar.mn @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: + content/browser/sync/aboutSyncTabs.xul + content/browser/sync/aboutSyncTabs.js + content/browser/sync/aboutSyncTabs.css + content/browser/sync/aboutSyncTabs-bindings.xml + content/browser/sync/setup.xul + content/browser/sync/addDevice.js + content/browser/sync/addDevice.xul + content/browser/sync/setup.js + content/browser/sync/genericChange.xul + content/browser/sync/genericChange.js + content/browser/sync/key.xhtml + content/browser/sync/notification.xml + content/browser/sync/quota.xul + content/browser/sync/quota.js + content/browser/sync/utils.js + content/browser/sync/progress.js + content/browser/sync/progress.xhtml
\ No newline at end of file diff --git a/application/basilisk/base/content/sync/key.xhtml b/application/basilisk/components/sync/key.xhtml index 1363132e7..92abf0ee6 100644 --- a/application/basilisk/base/content/sync/key.xhtml +++ b/application/basilisk/components/sync/key.xhtml @@ -44,7 +44,7 @@ <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p> </div> -<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p> +<p>&syncKey.findOutMore1.label;<a href="http://www.palemoon.org/sync/">http://www.palemoon.org/sync/</a>&syncKey.findOutMore2.label;</p> <footer> &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label; diff --git a/services/fxaccounts/interfaces/moz.build b/application/basilisk/components/sync/moz.build index ac80b3e93..2d64d506c 100644 --- a/services/fxaccounts/interfaces/moz.build +++ b/application/basilisk/components/sync/moz.build @@ -1,11 +1,8 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# -*- Mode: python; c-basic-offset: 4; 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' -] +JAR_MANIFESTS += ['jar.mn'] -XPIDL_MODULE = 'services_fxaccounts' diff --git a/application/basilisk/components/sync/notification.xml b/application/basilisk/components/sync/notification.xml new file mode 100644 index 000000000..8ac881e08 --- /dev/null +++ b/application/basilisk/components/sync/notification.xml @@ -0,0 +1,129 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.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 bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +]> + +<bindings id="notificationBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="notificationbox" extends="chrome://global/content/bindings/notification.xml#notificationbox"> + <content> + <xul:vbox xbl:inherits="hidden=notificationshidden"> + <xul:spacer/> + <children includes="notification"/> + </xul:vbox> + <children/> + </content> + + <implementation> + <constructor><![CDATA[ + let temp = {}; + Cu.import("resource://services-common/observers.js", temp); + temp.Observers.add("weave:notification:added", this.onNotificationAdded, this); + temp.Observers.add("weave:notification:removed", this.onNotificationRemoved, this); + + for each (var notification in Weave.Notifications.notifications) + this._appendNotification(notification); + ]]></constructor> + + <destructor><![CDATA[ + let temp = {}; + Cu.import("resource://services-common/observers.js", temp); + temp.Observers.remove("weave:notification:added", this.onNotificationAdded, this); + temp.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this); + ]]></destructor> + + <method name="onNotificationAdded"> + <parameter name="subject"/> + <parameter name="data"/> + <body><![CDATA[ + this._appendNotification(subject); + ]]></body> + </method> + + <method name="onNotificationRemoved"> + <parameter name="subject"/> + <parameter name="data"/> + <body><![CDATA[ + // If the view of the notification hasn't been removed yet, remove it. + var notifications = this.allNotifications; + for each (var notification in notifications) { + if (notification.notification == subject) { + notification.close(); + break; + } + } + ]]></body> + </method> + + <method name="_appendNotification"> + <parameter name="notification"/> + <body><![CDATA[ + var node = this.appendNotification(notification.description, + notification.title, + notification.iconURL, + notification.priority, + notification.buttons); + node.notification = notification; + ]]></body> + </method> + + </implementation> + </binding> + + <binding id="notification" extends="chrome://global/content/bindings/notification.xml#notification"> + <content> + <xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type"> + <xul:toolbarbutton ondblclick="event.stopPropagation();" + class="messageCloseButton close-icon tabbable" + xbl:inherits="hidden=hideclose" + tooltiptext="&closeNotification.tooltip;" + oncommand="document.getBindingParent(this).close()"/> + <xul:hbox anonid="details" align="center" flex="1"> + <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type"/> + <xul:description anonid="messageText" class="messageText" xbl:inherits="xbl:text=label"/> + + <!-- The children are the buttons defined by the notification. --> + <xul:hbox oncommand="document.getBindingParent(this)._doButtonCommand(event);"> + <children/> + </xul:hbox> + </xul:hbox> + </xul:hbox> + </content> + <implementation> + <!-- Note: this used to be a field, but for some reason it kept getting + - reset to its default value for TabNotification elements. + - As a property, that doesn't happen, even though the property stores + - its value in a JS property |_notification| that is not defined + - in XBL as a field or property. Maybe this is wrong, but it works. + --> + <property name="notification" + onget="return this._notification" + onset="this._notification = val; return val;"/> + <method name="close"> + <body><![CDATA[ + Weave.Notifications.remove(this.notification); + + // We should be able to call the base class's close method here + // to remove the notification element from the notification box, + // but we can't because of bug 373652, so instead we copied its code + // and execute it below. + var control = this.control; + if (control) + control.removeNotification(this); + else + this.hidden = true; + ]]></body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/application/basilisk/components/sync/progress.js b/application/basilisk/components/sync/progress.js new file mode 100644 index 000000000..101160fa8 --- /dev/null +++ b/application/basilisk/components/sync/progress.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-sync/main.js"); + +var gProgressBar; +var gCounter = 0; + +function onLoad(event) { + Services.obs.addObserver(onEngineSync, "weave:engine:sync:finish", false); + Services.obs.addObserver(onEngineSync, "weave:engine:sync:error", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:finish", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:error", false); + + gProgressBar = document.getElementById('uploadProgressBar'); + + if (Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { + gProgressBar.hidden = false; + } + else { + gProgressBar.hidden = true; + } +} + +function onUnload(event) { + cleanUpObservers(); +} + +function cleanUpObservers() { + try { + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:finish"); + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:error"); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:finish"); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:error"); + } + catch (e) { + // may be double called by unload & exit. Ignore. + } +} + +function onEngineSync(subject, topic, data) { + // The Clients engine syncs first. At this point we don't necessarily know + // yet how many engines will be enabled, so we'll ignore the Clients engine + // and evaluate how many engines are enabled when the first "real" engine + // syncs. + if (data == "clients") { + return; + } + + if (!gCounter && + Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { + gProgressBar.max = Weave.Service.engineManager.getEnabled().length; + } + + gCounter += 1; + gProgressBar.setAttribute("value", gCounter); +} + +function onServiceSync(subject, topic, data) { + // To address the case where 0 engines are synced, we will fill the + // progress bar so the user knows that the sync has finished. + gProgressBar.setAttribute("value", gProgressBar.max); + cleanUpObservers(); +} + +function closeTab() { + window.close(); +} diff --git a/application/basilisk/components/sync/progress.xhtml b/application/basilisk/components/sync/progress.xhtml new file mode 100644 index 000000000..d403cb20d --- /dev/null +++ b/application/basilisk/components/sync/progress.xhtml @@ -0,0 +1,55 @@ +<?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 [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % syncProgressDTD + SYSTEM "chrome://browser/locale/syncProgress.dtd"> + %syncProgressDTD; + <!ENTITY % syncSetupDTD + SYSTEM "chrome://browser/locale/syncSetup.dtd"> + %syncSetupDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&syncProgress.pageTitle;</title> + + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/skin/syncProgress.css"/> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://browser/skin/sync-16.png"/> + + <script type="text/javascript;version=1.8" + src="chrome://browser/content/sync/progress.js"/> + </head> + <body onload="onLoad(event)" onunload="onUnload(event)" dir="&locale.dir;"> + <title>&setup.successPage.title;</title> + <div id="floatingBox" class="main-content"> + <div id="title"> + <h1>&setup.successPage.title;</h1> + </div> + <div id="successLogo"> + <img id="brandSyncLogo" src="chrome://browser/skin/sync-128.png" alt="&syncProgress.logoAltText;" /> + </div> + <div id="loadingText"> + <p id="blurb">&syncProgress.textBlurb; </p> + </div> + <div id="progressBar"> + <progress id="uploadProgressBar" value="0"/> + </div> + <div id="bottomRow"> + <button id="closeButton" onclick="closeTab()">&syncProgress.closeButton; </button> + </div> + </div> + </body> +</html> diff --git a/application/basilisk/base/content/sync/quota.js b/application/basilisk/components/sync/quota.js index b416a44cc..b416a44cc 100644 --- a/application/basilisk/base/content/sync/quota.js +++ b/application/basilisk/components/sync/quota.js diff --git a/application/basilisk/base/content/sync/quota.xul b/application/basilisk/components/sync/quota.xul index 99e6ed78b..99e6ed78b 100644 --- a/application/basilisk/base/content/sync/quota.xul +++ b/application/basilisk/components/sync/quota.xul diff --git a/application/basilisk/base/content/sync/setup.js b/application/basilisk/components/sync/setup.js index f9dae1bd4..e8d67a5f6 100644 --- a/application/basilisk/base/content/sync/setup.js +++ b/application/basilisk/components/sync/setup.js @@ -1,4 +1,3 @@ -// -*- 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/. */ @@ -51,9 +50,7 @@ var gSyncSetup = { server: false }, - get _remoteSites() { - return [Weave.Service.serverURL, RECAPTCHA_DOMAIN]; - }, + get _remoteSites() [Weave.Service.serverURL, RECAPTCHA_DOMAIN], get _usingMainServers() { if (this._settingUpNew) @@ -72,7 +69,7 @@ var gSyncSetup = { let self = this; let addRem = function(add) { obs.forEach(function([topic, func]) { - // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling // of `this`. Fix in a followup. (bug 583347) if (add) Weave.Svc.Obs.add(topic, self[func], self); @@ -81,7 +78,7 @@ var gSyncSetup = { }); }; addRem(true); - window.addEventListener("unload", () => addRem(false), false); + window.addEventListener("unload", function() addRem(false), false); window.setTimeout(function () { // Force Service to be loaded so that engines are registered. @@ -123,14 +120,14 @@ var gSyncSetup = { startNewAccountSetup: function () { if (!Weave.Utils.ensureMPUnlocked()) - return; + return false; this._settingUpNew = true; this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE; }, useExistingAccount: function () { if (!Weave.Utils.ensureMPUnlocked()) - return; + return false; this._settingUpNew = false; if (this.wizardType == "pair") { // We're already pairing, so there's no point in pairing again. @@ -512,6 +509,7 @@ var gSyncSetup = { onWizardBack: function () { switch (this.wizard.pageIndex) { case NEW_ACCOUNT_START_PAGE: + case EXISTING_ACCOUNT_LOGIN_PAGE: this.wizard.pageIndex = INTRO_PAGE; return false; case EXISTING_ACCOUNT_CONNECT_PAGE: @@ -552,12 +550,19 @@ var gSyncSetup = { for (let i = 0;i < prefs.length;i++) { Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i])); } + + // XXX: Addons syncing is currently not operational; + // Make doubly-sure to always disable addons syncing pref + Weave.Svc.Prefs.set("engine.addons", false); + this._handleNoScript(false); if (Weave.Svc.Prefs.get("firstSync", "") == "notReady") Weave.Svc.Prefs.reset("firstSync"); Weave.Service.persistLogin(); Weave.Svc.Obs.notify("weave:service:setup-complete"); + + gSyncUtils.openFirstSyncProgressPage(); } Weave.Utils.nextTick(Weave.Service.sync, Weave.Service); window.close(); @@ -797,6 +802,7 @@ var gSyncSetup = { let el = document.getElementById("server"); let valid = false; let feedback = document.getElementById("serverFeedbackRow"); + let str = ""; if (el.value) { valid = this._validateServer(el); let str = valid ? "" : "serverInvalid.label"; @@ -932,7 +938,12 @@ var gSyncSetup = { let addonsEngine = Weave.Service.engineManager.get("addons"); if (addonsEngine.enabled) { let ids = addonsEngine._store.getAllIDs(); - let blessedcount = Object.keys(ids).filter(id => ids[id]).length; + let blessedcount = 0; + for each (let i in ids) { + if (i) { + blessedcount++; + } + } // bug 600141 does not apply, as this does not have to support existing strings document.getElementById("addonCount").value = PluralForm.get(blessedcount, @@ -956,7 +967,7 @@ var gSyncSetup = { box.appendChild(node); } - for (let name of Weave.Service.clientsEngine.stats.names) { + for each (let name in Weave.Service.clientsEngine.stats.names) { // Don't list the current client if (name == Weave.Service.clientsEngine.localName) continue; @@ -998,7 +1009,7 @@ var gSyncSetup = { if (string) { try { str = this._stringBundle.GetStringFromName(string); - } catch (e) {} + } catch(e) {} if (!str) str = Weave.Utils.getErrorString(string); @@ -1027,7 +1038,7 @@ var gSyncSetup = { // If we didn't find a captcha, assume it's not needed and don't show it. let responseStatus = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; setVisibility(this.captchaBrowser, responseStatus != 404); - // XXX TODO we should really log any responseStatus other than 200 + //XXX TODO we should really log any responseStatus other than 200 }, onProgressChange: function() {}, onStatusChange: function() {}, diff --git a/application/basilisk/base/content/sync/setup.xul b/application/basilisk/components/sync/setup.xul index 11c085931..cf2cc77e4 100644 --- a/application/basilisk/base/content/sync/setup.xul +++ b/application/basilisk/components/sync/setup.xul @@ -43,7 +43,7 @@ &pairDevice.dialog.description.label; <label class="text-link" value="&addDevice.showMeHow.label;" - href="https://services.mozilla.com/sync/help/add-device"/> + href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> </description> <separator class="groove-thin"/> <description> @@ -183,12 +183,12 @@ onclick="document.getElementById('tos').focus(); document.getElementById('tos').click()"> &setup.tosAgree1.label; - <label class="text-link" + <label class="text-link inline-link" onclick="event.stopPropagation();gSyncUtils.openToS();"> &setup.tosLink.label; </label> &setup.tosAgree2.label; - <label class="text-link" + <label class="text-link inline-link" onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"> &setup.ppLink.label; </label> @@ -221,7 +221,7 @@ &pairDevice.setup.description.label; <label class="text-link" value="&addDevice.showMeHow.label;" - href="https://services.mozilla.com/sync/help/easy-setup"/> + href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> </description> <label value="&addDevice.setup.enterCode.label;" control="easySetupPIN1"/> @@ -340,7 +340,7 @@ <description> &existingRecoveryKey.description; <label class="text-link" - href="https://services.mozilla.com/sync/help/manual-setup"> + href="http://www.palemoon.org/sync/help/recoverykey.shtml"> &addDevice.showMeHow.label; </label> <spacer id="passphraseHelpSpacer"/> @@ -359,7 +359,7 @@ <grid> <columns> <column/> - <column flex="1" style="margin-inline-end: 2px"/> + <column flex="1" style="-moz-margin-end: 2px"/> </columns> <rows> <row align="center"> @@ -375,7 +375,8 @@ <checkbox label="&engine.addons.label;" accesskey="&engine.addons.accesskey;" id="engine.addons" - checked="true"/> + checked="false" + hidden="true"/> <checkbox label="&engine.bookmarks.label;" accesskey="&engine.bookmarks.accesskey;" id="engine.bookmarks" diff --git a/application/basilisk/base/content/sync/utils.js b/application/basilisk/components/sync/utils.js index 92981f7b4..d41ecf18a 100644 --- a/application/basilisk/base/content/sync/utils.js +++ b/application/basilisk/components/sync/utils.js @@ -14,13 +14,6 @@ var gSyncUtils = { return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); }, - get fxAccountsEnabled() { - let service = Components.classes["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - return service.fxAccountsEnabled; - }, - // opens in a new window if we're in a modal prefwindow world, in a new tab otherwise _openLink: function (url) { let thisDocEl = document.documentElement, @@ -77,22 +70,16 @@ var gSyncUtils = { this._openLink(Weave.Service.pwResetURL); }, - get tosURL() { - let root = this.fxAccountsEnabled ? "fxa." : ""; - return Weave.Svc.Prefs.get(root + "termsURL"); - }, - openToS: function () { - this._openLink(this.tosURL); + this._openLink(Weave.Svc.Prefs.get("termsURL")); }, - get privacyPolicyURL() { - let root = this.fxAccountsEnabled ? "fxa." : ""; - return Weave.Svc.Prefs.get(root + "privacyURL"); + openPrivacyPolicy: function () { + this._openLink(Weave.Svc.Prefs.get("privacyURL")); }, - openPrivacyPolicy: function () { - this._openLink(this.privacyPolicyURL); + openFirstSyncProgressPage: function () { + this._openLink("about:sync-progress"); }, /** @@ -134,7 +121,7 @@ var gSyncUtils = { /** * Print passphrase backup document. - * + * * @param elid : ID of the form element containing the passphrase. */ passphrasePrint: function(elid) { @@ -162,7 +149,7 @@ var gSyncUtils = { /** * Save passphrase backup document to disk as HTML file. - * + * * @param elid : ID of the form element containing the passphrase. */ passphraseSave: function(elid) { @@ -200,7 +187,7 @@ var gSyncUtils = { * * @param el1 : the first textbox element in the form * @param el2 : the second textbox element, if omitted it's an update form - * + * * returns [valid, errorString] */ validatePassword: function (el1, el2) { diff --git a/application/basilisk/components/syncedtabs/EventEmitter.jsm b/application/basilisk/components/syncedtabs/EventEmitter.jsm deleted file mode 100644 index ec3225f0f..000000000 --- a/application/basilisk/components/syncedtabs/EventEmitter.jsm +++ /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/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "EventEmitter" -]; - -// Simple event emitter abstraction for storage objects to use. -function EventEmitter () { - this._events = new Map(); -} - -EventEmitter.prototype = { - on(event, listener) { - if (this._events.has(event)) { - this._events.get(event).add(listener); - } else { - this._events.set(event, new Set([listener])); - } - }, - off(event, listener) { - if (!this._events.has(event)) { - return; - } - this._events.get(event).delete(listener); - }, - emit(event, ...args) { - if (!this._events.has(event)) { - return; - } - for (let listener of this._events.get(event).values()) { - try { - listener.apply(this, args); - } catch (e) { - Cu.reportError(e); - } - } - }, -}; - diff --git a/application/basilisk/components/syncedtabs/SyncedTabsDeckComponent.js b/application/basilisk/components/syncedtabs/SyncedTabsDeckComponent.js deleted file mode 100644 index c35277795..000000000 --- a/application/basilisk/components/syncedtabs/SyncedTabsDeckComponent.js +++ /dev/null @@ -1,169 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -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:///modules/syncedtabs/SyncedTabsDeckStore.js"); -Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckView.js"); -Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js"); -Cu.import("resource:///modules/syncedtabs/TabListComponent.js"); -Cu.import("resource:///modules/syncedtabs/TabListView.js"); -let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {}); - -XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () { - return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {}); -}); - -let log = Cu.import("resource://gre/modules/Log.jsm", {}) - .Log.repository.getLogger("Sync.RemoteTabs"); - -this.EXPORTED_SYMBOLS = [ - "SyncedTabsDeckComponent" -]; - -/* SyncedTabsDeckComponent - * This component instantiates views and storage objects as well as defines - * behaviors that will be passed down to the views. This helps keep the views - * isolated and easier to test. - */ - -function SyncedTabsDeckComponent({ - window, SyncedTabs, fxAccounts, deckStore, listStore, listComponent, DeckView, getChromeWindowMock, -}) { - this._window = window; - this._SyncedTabs = SyncedTabs; - this._fxAccounts = fxAccounts; - this._DeckView = DeckView || SyncedTabsDeckView; - // used to stub during tests - this._getChromeWindow = getChromeWindowMock || getChromeWindow; - - this._deckStore = deckStore || new SyncedTabsDeckStore(); - this._syncedTabsListStore = listStore || new SyncedTabsListStore(SyncedTabs); - this.tabListComponent = listComponent || new TabListComponent({ - window: this._window, - store: this._syncedTabsListStore, - View: TabListView, - SyncedTabs: SyncedTabs, - clipboardHelper: Cc["@mozilla.org/widget/clipboardhelper;1"] - .getService(Ci.nsIClipboardHelper), - getChromeWindow: this._getChromeWindow, - }); -} - -SyncedTabsDeckComponent.prototype = { - PANELS: { - TABS_CONTAINER: "tabs-container", - TABS_FETCHING: "tabs-fetching", - NOT_AUTHED_INFO: "notAuthedInfo", - SINGLE_DEVICE_INFO: "singleDeviceInfo", - TABS_DISABLED: "tabs-disabled", - }, - - get container() { - return this._deckView ? this._deckView.container : null; - }, - - init() { - Services.obs.addObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED, false); - Services.obs.addObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION, false); - - // Go ahead and trigger sync - this._SyncedTabs.syncTabs() - .catch(Cu.reportError); - - this._deckView = new this._DeckView(this._window, this.tabListComponent, { - onAndroidClick: event => this.openAndroidLink(event), - oniOSClick: event => this.openiOSLink(event), - onSyncPrefClick: event => this.openSyncPrefs(event) - }); - - this._deckStore.on("change", state => this._deckView.render(state)); - // Trigger the initial rendering of the deck view - // Object.values only in nightly - this._deckStore.setPanels(Object.keys(this.PANELS).map(k => this.PANELS[k])); - // Set the initial panel to display - this.updatePanel(); - }, - - uninit() { - Services.obs.removeObserver(this, this._SyncedTabs.TOPIC_TABS_CHANGED); - Services.obs.removeObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION); - this._deckView.destroy(); - }, - - observe(subject, topic, data) { - switch (topic) { - case this._SyncedTabs.TOPIC_TABS_CHANGED: - this._syncedTabsListStore.getData(); - this.updatePanel(); - break; - case FxAccountsCommon.ONLOGIN_NOTIFICATION: - this.updatePanel(); - break; - default: - break; - } - }, - - // There's no good way to mock fxAccounts in browser tests where it's already - // been instantiated, so we have this method for stubbing. - _accountStatus() { - return this._fxAccounts.accountStatus(); - }, - - getPanelStatus() { - return this._accountStatus().then(exists => { - if (!exists) { - return this.PANELS.NOT_AUTHED_INFO; - } - if (!this._SyncedTabs.isConfiguredToSyncTabs) { - return this.PANELS.TABS_DISABLED; - } - if (!this._SyncedTabs.hasSyncedThisSession) { - return this.PANELS.TABS_FETCHING; - } - return this._SyncedTabs.getTabClients().then(clients => { - if (clients.length) { - return this.PANELS.TABS_CONTAINER; - } - return this.PANELS.SINGLE_DEVICE_INFO; - }); - }) - .catch(err => { - Cu.reportError(err); - return this.PANELS.NOT_AUTHED_INFO; - }); - }, - - updatePanel() { - // return promise for tests - return this.getPanelStatus() - .then(panelId => this._deckStore.selectPanel(panelId)) - .catch(Cu.reportError); - }, - - openAndroidLink(event) { - let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar"; - this._openUrl(href, event); - }, - - openiOSLink(event) { - let href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar"; - this._openUrl(href, event); - }, - - _openUrl(url, event) { - this._window.openUILink(url, event); - }, - - openSyncPrefs() { - this._getChromeWindow(this._window).gSyncUI.openSetup(null, "tabs-sidebar"); - } -}; - diff --git a/application/basilisk/components/syncedtabs/SyncedTabsDeckStore.js b/application/basilisk/components/syncedtabs/SyncedTabsDeckStore.js deleted file mode 100644 index ede6914c8..000000000 --- a/application/basilisk/components/syncedtabs/SyncedTabsDeckStore.js +++ /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/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {}); - -this.EXPORTED_SYMBOLS = [ - "SyncedTabsDeckStore" -]; - -/** - * SyncedTabsDeckStore - * - * This store keeps track of the deck view state, including the panels and which - * one is selected. The view listens for change events on the store, which are - * triggered whenever the state changes. If it's a small change, the state - * will have `isUpdatable` set to true so the view can skip rerendering the whole - * DOM. - */ -function SyncedTabsDeckStore() { - EventEmitter.call(this); - this._panels = []; -} - -Object.assign(SyncedTabsDeckStore.prototype, EventEmitter.prototype, { - _change(isUpdatable = false) { - let panels = this._panels.map(panel => { - return {id: panel, selected: panel === this._selectedPanel}; - }); - this.emit("change", {panels, isUpdatable: isUpdatable}); - }, - - /** - * Sets the selected panelId and triggers a change event. - * @param {String} panelId - ID of the panel to select. - */ - selectPanel(panelId) { - if (this._panels.indexOf(panelId) === -1 || this._selectedPanel === panelId) { - return; - } - this._selectedPanel = panelId; - this._change(true); - }, - - /** - * Update the set of panels in the deck and trigger a change event. - * @param {Array} panels - an array of IDs for each panel in the deck. - */ - setPanels(panels) { - if (panels === this._panels) { - return; - } - this._panels = panels || []; - this._change(); - } -}); diff --git a/application/basilisk/components/syncedtabs/SyncedTabsDeckView.js b/application/basilisk/components/syncedtabs/SyncedTabsDeckView.js deleted file mode 100644 index e9efff323..000000000 --- a/application/basilisk/components/syncedtabs/SyncedTabsDeckView.js +++ /dev/null @@ -1,116 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {}); - -let log = Cu.import("resource://gre/modules/Log.jsm", {}) - .Log.repository.getLogger("Sync.RemoteTabs"); - -this.EXPORTED_SYMBOLS = [ - "SyncedTabsDeckView" -]; - -/** - * SyncedTabsDeckView - * - * Instances of SyncedTabsDeckView render DOM nodes from a given state. - * No state is kept internaly and the DOM will completely - * rerender unless the state flags `isUpdatable`, which helps - * make small changes without the overhead of a full rerender. - */ -const SyncedTabsDeckView = function (window, tabListComponent, props) { - this.props = props; - - this._window = window; - this._doc = window.document; - - this._tabListComponent = tabListComponent; - this._deckTemplate = this._doc.getElementById("deck-template"); - this.container = this._doc.createElement("div"); -}; - -SyncedTabsDeckView.prototype = { - render(state) { - if (state.isUpdatable) { - this.update(state); - } else { - this.create(state); - } - }, - - create(state) { - let deck = this._doc.importNode(this._deckTemplate.content, true).firstElementChild; - this._clearChilden(); - - let tabListWrapper = this._doc.createElement("div"); - tabListWrapper.className = "tabs-container sync-state"; - this._tabListComponent.init(); - tabListWrapper.appendChild(this._tabListComponent.container); - deck.appendChild(tabListWrapper); - this.container.appendChild(deck); - - this._generateDevicePromo(); - - this._attachListeners(); - this.update(state); - }, - - _getBrowserBundle() { - return getChromeWindow(this._window).document.getElementById("bundle_browser"); - }, - - _generateDevicePromo() { - let bundle = this._getBrowserBundle(); - let formatArgs = ["android", "ios"].map(os => { - let link = this._doc.createElement("a"); - link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`); - link.className = `${os}-link text-link`; - link.setAttribute("href", "#"); - return link.outerHTML; - }); - // Put it all together... - let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs); - this.container.querySelector(".device-promo").innerHTML = contents; - }, - - destroy() { - this._tabListComponent.uninit(); - this.container.remove(); - }, - - update(state) { - // Note that we may also want to update elements that are outside of the - // deck, so use the document to find the class names rather than our - // container. - for (let panel of state.panels) { - if (panel.selected) { - Array.prototype.map.call(this._doc.getElementsByClassName(panel.id), - item => item.classList.add("selected")); - } else { - Array.prototype.map.call(this._doc.getElementsByClassName(panel.id), - item => item.classList.remove("selected")); - } - } - }, - - _clearChilden() { - while (this.container.firstChild) { - this.container.removeChild(this.container.firstChild); - } - }, - - _attachListeners() { - this.container.querySelector(".android-link").addEventListener("click", this.props.onAndroidClick); - this.container.querySelector(".ios-link").addEventListener("click", this.props.oniOSClick); - let syncPrefLinks = this.container.querySelectorAll(".sync-prefs"); - for (let link of syncPrefLinks) { - link.addEventListener("click", this.props.onSyncPrefClick); - } - }, -}; - diff --git a/application/basilisk/components/syncedtabs/SyncedTabsListStore.js b/application/basilisk/components/syncedtabs/SyncedTabsListStore.js deleted file mode 100644 index 8f03d9a89..000000000 --- a/application/basilisk/components/syncedtabs/SyncedTabsListStore.js +++ /dev/null @@ -1,235 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -let { EventEmitter } = Cu.import("resource:///modules/syncedtabs/EventEmitter.jsm", {}); - -this.EXPORTED_SYMBOLS = [ - "SyncedTabsListStore" -]; - -/** - * SyncedTabsListStore - * - * Instances of this store encapsulate all of the state associated with a synced tabs list view. - * The state includes the clients, their tabs, the row that is currently selected, - * and the filtered query. - */ -function SyncedTabsListStore(SyncedTabs) { - EventEmitter.call(this); - this._SyncedTabs = SyncedTabs; - this.data = []; - this._closedClients = {}; - this._selectedRow = [-1, -1]; - this.filter = ""; - this.inputFocused = false; -} - -Object.assign(SyncedTabsListStore.prototype, EventEmitter.prototype, { - // This internal method triggers the "change" event that views - // listen for. It denormalizes the state so that it's easier for - // the view to deal with. updateType hints to the view what - // actually needs to be rerendered or just updated, and can be - // empty (to (re)render everything), "searchbox" (to rerender just the tab list), - // or "all" (to skip rendering and just update all attributes of existing nodes). - _change(updateType) { - let selectedParent = this._selectedRow[0]; - let selectedChild = this._selectedRow[1]; - let rowSelected = false; - // clone the data so that consumers can't mutate internal storage - let data = Cu.cloneInto(this.data, {}); - let tabCount = 0; - - data.forEach((client, index) => { - client.closed = !!this._closedClients[client.id]; - - if (rowSelected || selectedParent < 0) { - return; - } - if (this.filter) { - if (selectedParent < tabCount + client.tabs.length) { - client.tabs[selectedParent - tabCount].selected = true; - client.tabs[selectedParent - tabCount].focused = !this.inputFocused; - rowSelected = true; - } else { - tabCount += client.tabs.length; - } - return; - } - if (selectedParent === index && selectedChild === -1) { - client.selected = true; - client.focused = !this.inputFocused; - rowSelected = true; - } else if (selectedParent === index) { - client.tabs[selectedChild].selected = true; - client.tabs[selectedChild].focused = !this.inputFocused; - rowSelected = true; - } - }); - - // If this were React the view would be smart enough - // to not re-render the whole list unless necessary. But it's - // not, so updateType is a hint to the view of what actually - // needs to be rerendered. - this.emit("change", { - clients: data, - canUpdateAll: updateType === "all", - canUpdateInput: updateType === "searchbox", - filter: this.filter, - inputFocused: this.inputFocused - }); - }, - - /** - * Moves the row selection from a child to its parent, - * which occurs when the parent of a selected row closes. - */ - _selectParentRow() { - this._selectedRow[1] = -1; - }, - - _toggleBranch(id, closed) { - this._closedClients[id] = closed; - if (this._closedClients[id]) { - this._selectParentRow(); - } - this._change("all"); - }, - - _isOpen(client) { - return !this._closedClients[client.id]; - }, - - moveSelectionDown() { - let branchRow = this._selectedRow[0]; - let childRow = this._selectedRow[1]; - let branch = this.data[branchRow]; - - if (this.filter) { - this.selectRow(branchRow + 1); - return; - } - - if (branchRow < 0) { - this.selectRow(0, -1); - } else if ((!branch.tabs.length || childRow >= branch.tabs.length - 1 || !this._isOpen(branch)) && branchRow < this.data.length) { - this.selectRow(branchRow + 1, -1); - } else if (childRow < branch.tabs.length) { - this.selectRow(branchRow, childRow + 1); - } - }, - - moveSelectionUp() { - let branchRow = this._selectedRow[0]; - let childRow = this._selectedRow[1]; - - if (this.filter) { - this.selectRow(branchRow - 1); - return; - } - - if (branchRow < 0) { - this.selectRow(0, -1); - } else if (childRow < 0 && branchRow > 0) { - let prevBranch = this.data[branchRow - 1]; - let newChildRow = this._isOpen(prevBranch) ? prevBranch.tabs.length - 1 : -1; - this.selectRow(branchRow - 1, newChildRow); - } else if (childRow >= 0) { - this.selectRow(branchRow, childRow - 1); - } - }, - - // Selects a row and makes sure the selection is within bounds - selectRow(parent, child) { - let maxParentRow = this.filter ? this._tabCount() : this.data.length; - let parentRow = parent; - if (parent <= -1) { - parentRow = 0; - } else if (parent >= maxParentRow) { - return; - } - - let childRow = child; - if (parentRow === -1 || this.filter || typeof child === "undefined" || child < -1) { - childRow = -1; - } else if (child >= this.data[parentRow].tabs.length) { - childRow = this.data[parentRow].tabs.length - 1; - } - - if (this._selectedRow[0] === parentRow && this._selectedRow[1] === childRow) { - return; - } - - this._selectedRow = [parentRow, childRow]; - this.inputFocused = false; - this._change("all"); - }, - - _tabCount() { - return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0); - }, - - toggleBranch(id) { - this._toggleBranch(id, !this._closedClients[id]); - }, - - closeBranch(id) { - this._toggleBranch(id, true); - }, - - openBranch(id) { - this._toggleBranch(id, false); - }, - - focusInput() { - this.inputFocused = true; - // A change type of "all" updates rather than rebuilds, which is what we - // want here - only the selection/focus has changed. - this._change("all"); - }, - - blurInput() { - this.inputFocused = false; - // A change type of "all" updates rather than rebuilds, which is what we - // want here - only the selection/focus has changed. - this._change("all"); - }, - - clearFilter() { - this.filter = ""; - this._selectedRow = [-1, -1]; - return this.getData(); - }, - - // Fetches data from the SyncedTabs module and triggers - // and update - getData(filter) { - let updateType; - let hasFilter = typeof filter !== "undefined"; - if (hasFilter) { - this.filter = filter; - this._selectedRow = [-1, -1]; - - // When a filter is specified we tell the view that only the list - // needs to be rerendered so that it doesn't disrupt the input - // field's focus. - updateType = "searchbox"; - } - - // return promise for tests - return this._SyncedTabs.getTabClients(this.filter) - .then(result => { - if (!hasFilter) { - // Only sort clients and tabs if we're rendering the whole list. - this._SyncedTabs.sortTabClientsByLastUsed(result); - } - this.data = result; - this._change(updateType); - }) - .catch(Cu.reportError); - } -}); diff --git a/application/basilisk/components/syncedtabs/TabListComponent.js b/application/basilisk/components/syncedtabs/TabListComponent.js deleted file mode 100644 index aa60e4769..000000000 --- a/application/basilisk/components/syncedtabs/TabListComponent.js +++ /dev/null @@ -1,138 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -let log = Cu.import("resource://gre/modules/Log.jsm", {}) - .Log.repository.getLogger("Sync.RemoteTabs"); - -XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils", - "resource:///modules/PlacesUIUtils.jsm"); - -this.EXPORTED_SYMBOLS = [ - "TabListComponent" -]; - -/** - * TabListComponent - * - * The purpose of this component is to compose the view, state, and actions. - * It defines high level actions that act on the state and passes them to the - * view for it to trigger during user interaction. It also subscribes the view - * to state changes so it can rerender. - */ - -function TabListComponent({window, store, View, SyncedTabs, clipboardHelper, - getChromeWindow}) { - this._window = window; - this._store = store; - this._View = View; - this._clipboardHelper = clipboardHelper; - this._getChromeWindow = getChromeWindow; - // used to trigger Sync from context menu - this._SyncedTabs = SyncedTabs; -} - -TabListComponent.prototype = { - get container() { - return this._view.container; - }, - - init() { - log.debug("Initializing TabListComponent"); - - this._view = new this._View(this._window, { - onSelectRow: (...args) => this.onSelectRow(...args), - onOpenTab: (...args) => this.onOpenTab(...args), - onOpenTabs: (...args) => this.onOpenTabs(...args), - onMoveSelectionDown: (...args) => this.onMoveSelectionDown(...args), - onMoveSelectionUp: (...args) => this.onMoveSelectionUp(...args), - onToggleBranch: (...args) => this.onToggleBranch(...args), - onBookmarkTab: (...args) => this.onBookmarkTab(...args), - onCopyTabLocation: (...args) => this.onCopyTabLocation(...args), - onSyncRefresh: (...args) => this.onSyncRefresh(...args), - onFilter: (...args) => this.onFilter(...args), - onClearFilter: (...args) => this.onClearFilter(...args), - onFilterFocus: (...args) => this.onFilterFocus(...args), - onFilterBlur: (...args) => this.onFilterBlur(...args) - }); - - this._store.on("change", state => this._view.render(state)); - this._view.render({clients: []}); - // get what's already available... - this._store.getData(); - this._store.focusInput(); - }, - - uninit() { - this._view.destroy(); - }, - - onFilter(query) { - this._store.getData(query); - }, - - onClearFilter() { - this._store.clearFilter(); - }, - - onFilterFocus() { - this._store.focusInput(); - }, - - onFilterBlur() { - this._store.blurInput(); - }, - - onSelectRow(position) { - this._store.selectRow(position[0], position[1]); - }, - - onMoveSelectionDown() { - this._store.moveSelectionDown(); - }, - - onMoveSelectionUp() { - this._store.moveSelectionUp(); - }, - - onToggleBranch(id) { - this._store.toggleBranch(id); - }, - - onBookmarkTab(uri, title) { - this._window.top.PlacesCommandHook - .bookmarkLink(this._window.top.PlacesUtils.bookmarksMenuFolderId, uri, title) - .catch(Cu.reportError); - }, - - onOpenTab(url, where, params) { - this._window.openUILinkIn(url, where, params); - }, - - onOpenTabs(urls, where) { - if (!PlacesUIUtils.confirmOpenInTabs(urls.length, this._window)) { - return; - } - if (where == "window") { - this._window.openDialog(this._window.getBrowserURL(), "_blank", - "chrome,dialog=no,all", urls.join("|")); - } else { - let loadInBackground = where == "tabshifted" ? true : false; - this._getChromeWindow(this._window).gBrowser.loadTabs(urls, loadInBackground, false); - } - }, - - onCopyTabLocation(url) { - this._clipboardHelper.copyString(url); - }, - - onSyncRefresh() { - this._SyncedTabs.syncTabs(true); - } -}; diff --git a/application/basilisk/components/syncedtabs/TabListView.js b/application/basilisk/components/syncedtabs/TabListView.js deleted file mode 100644 index dab15101b..000000000 --- a/application/basilisk/components/syncedtabs/TabListView.js +++ /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/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); - -let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {}); - -let log = Cu.import("resource://gre/modules/Log.jsm", {}) - .Log.repository.getLogger("Sync.RemoteTabs"); - -this.EXPORTED_SYMBOLS = [ - "TabListView" -]; - -function getContextMenu(window) { - return getChromeWindow(window).document.getElementById("SyncedTabsSidebarContext"); -} - -function getTabsFilterContextMenu(window) { - return getChromeWindow(window).document.getElementById("SyncedTabsSidebarTabsFilterContext"); -} - -/* - * TabListView - * - * Given a state, this object will render the corresponding DOM. - * It maintains no state of it's own. It listens for DOM events - * and triggers actions that may cause the state to change and - * ultimately the view to rerender. - */ -function TabListView(window, props) { - this.props = props; - - this._window = window; - this._doc = this._window.document; - - this._tabsContainerTemplate = this._doc.getElementById("tabs-container-template"); - this._clientTemplate = this._doc.getElementById("client-template"); - this._emptyClientTemplate = this._doc.getElementById("empty-client-template"); - this._tabTemplate = this._doc.getElementById("tab-template"); - this.tabsFilter = this._doc.querySelector(".tabsFilter"); - this.clearFilter = this._doc.querySelector(".textbox-search-clear"); - this.searchBox = this._doc.querySelector(".search-box"); - this.searchIcon = this._doc.querySelector(".textbox-search-icon"); - - this.container = this._doc.createElement("div"); - - this._attachFixedListeners(); - - this._setupContextMenu(); -} - -TabListView.prototype = { - render(state) { - // Don't rerender anything; just update attributes, e.g. selection - if (state.canUpdateAll) { - this._update(state); - return; - } - // Rerender the tab list - if (state.canUpdateInput) { - this._updateSearchBox(state); - this._createList(state); - return; - } - // Create the world anew - this._create(state); - }, - - // Create the initial DOM from templates - _create(state) { - let wrapper = this._doc.importNode(this._tabsContainerTemplate.content, true).firstElementChild; - this._clearChilden(); - this.container.appendChild(wrapper); - - this.list = this.container.querySelector(".list"); - - this._createList(state); - this._updateSearchBox(state); - - this._attachListListeners(); - }, - - _createList(state) { - this._clearChilden(this.list); - for (let client of state.clients) { - if (state.filter) { - this._renderFilteredClient(client); - } else { - this._renderClient(client); - } - } - if (this.list.firstChild) { - const firstTab = this.list.firstChild.querySelector(".item.tab:first-child .item-title"); - if (firstTab) { - firstTab.setAttribute("tabindex", 2); - } - } - }, - - destroy() { - this._teardownContextMenu(); - this.container.remove(); - }, - - _update(state) { - this._updateSearchBox(state); - for (let client of state.clients) { - let clientNode = this._doc.getElementById("item-" + client.id); - if (clientNode) { - this._updateClient(client, clientNode); - } - - client.tabs.forEach((tab, index) => { - let tabNode = this._doc.getElementById('tab-' + client.id + '-' + index); - this._updateTab(tab, tabNode, index); - }); - } - }, - - // Client rows are hidden when the list is filtered - _renderFilteredClient(client, filter) { - client.tabs.forEach((tab, index) => { - let node = this._renderTab(client, tab, index); - this.list.appendChild(node); - }); - }, - - _renderClient(client) { - let itemNode = client.tabs.length ? - this._createClient(client) : - this._createEmptyClient(client); - - this._updateClient(client, itemNode); - - let tabsList = itemNode.querySelector(".item-tabs-list"); - client.tabs.forEach((tab, index) => { - let node = this._renderTab(client, tab, index); - tabsList.appendChild(node); - }); - - this.list.appendChild(itemNode); - return itemNode; - }, - - _renderTab(client, tab, index) { - let itemNode = this._createTab(tab); - this._updateTab(tab, itemNode, index); - return itemNode; - }, - - _createClient(item) { - return this._doc.importNode(this._clientTemplate.content, true).firstElementChild; - }, - - _createEmptyClient(item) { - return this._doc.importNode(this._emptyClientTemplate.content, true).firstElementChild; - }, - - _createTab(item) { - return this._doc.importNode(this._tabTemplate.content, true).firstElementChild; - }, - - _clearChilden(node) { - let parent = node || this.container; - while (parent.firstChild) { - parent.removeChild(parent.firstChild); - } - }, - - // These listeners are attached only once, when we initialize the view - _attachFixedListeners() { - this.tabsFilter.addEventListener("input", this.onFilter.bind(this)); - this.tabsFilter.addEventListener("focus", this.onFilterFocus.bind(this)); - this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this)); - this.clearFilter.addEventListener("click", this.onClearFilter.bind(this)); - this.searchIcon.addEventListener("click", this.onFilterFocus.bind(this)); - }, - - // These listeners have to be re-created every time since we re-create the list - _attachListListeners() { - this.list.addEventListener("click", this.onClick.bind(this)); - this.list.addEventListener("mouseup", this.onMouseUp.bind(this)); - this.list.addEventListener("keydown", this.onKeyDown.bind(this)); - }, - - _updateSearchBox(state) { - if (state.filter) { - this.searchBox.classList.add("filtered"); - } else { - this.searchBox.classList.remove("filtered"); - } - this.tabsFilter.value = state.filter; - if (state.inputFocused) { - this.searchBox.setAttribute("focused", true); - this.tabsFilter.focus(); - } else { - this.searchBox.removeAttribute("focused"); - } - }, - - /** - * Update the element representing an item, ensuring it's in sync with the - * underlying data. - * @param {client} item - Item to use as a source. - * @param {Element} itemNode - Element to update. - */ - _updateClient(item, itemNode) { - itemNode.setAttribute("id", "item-" + item.id); - let lastSync = new Date(item.lastModified); - let lastSyncTitle = getChromeWindow(this._window).gSyncUI.formatLastSyncDate(lastSync); - itemNode.setAttribute("title", lastSyncTitle); - if (item.closed) { - itemNode.classList.add("closed"); - } else { - itemNode.classList.remove("closed"); - } - if (item.selected) { - itemNode.classList.add("selected"); - } else { - itemNode.classList.remove("selected"); - } - if (item.isMobile) { - itemNode.classList.add("device-image-mobile"); - } else { - itemNode.classList.add("device-image-desktop"); - } - if (item.focused) { - itemNode.focus(); - } - itemNode.dataset.id = item.id; - itemNode.querySelector(".item-title").textContent = item.name; - }, - - /** - * Update the element representing a tab, ensuring it's in sync with the - * underlying data. - * @param {tab} item - Item to use as a source. - * @param {Element} itemNode - Element to update. - */ - _updateTab(item, itemNode, index) { - itemNode.setAttribute("title", `${item.title}\n${item.url}`); - itemNode.setAttribute("id", "tab-" + item.client + '-' + index); - if (item.selected) { - itemNode.classList.add("selected"); - } else { - itemNode.classList.remove("selected"); - } - if (item.focused) { - itemNode.focus(); - } - itemNode.dataset.url = item.url; - - itemNode.querySelector(".item-title").textContent = item.title; - - if (item.icon) { - let icon = itemNode.querySelector(".item-icon-container"); - icon.style.backgroundImage = "url(" + item.icon + ")"; - } - }, - - onMouseUp(event) { - if (event.which == 2) { // Middle click - this.onClick(event); - } - }, - - onClick(event) { - let itemNode = this._findParentItemNode(event.target); - if (!itemNode) { - return; - } - - if (itemNode.classList.contains("tab")) { - let url = itemNode.dataset.url; - if (url) { - this.onOpenSelected(url, event); - } - } - - // Middle click on a client - if (itemNode.classList.contains("client")) { - let where = getChromeWindow(this._window).whereToOpenLink(event); - if (where != "current") { - const tabs = itemNode.querySelector(".item-tabs-list").childNodes; - const urls = [...tabs].map(tab => tab.dataset.url); - this.props.onOpenTabs(urls, where); - } - } - - if (event.target.classList.contains("item-twisty-container") - && event.which != 2) { - this.props.onToggleBranch(itemNode.dataset.id); - return; - } - - let position = this._getSelectionPosition(itemNode); - this.props.onSelectRow(position); - }, - - /** - * Handle a keydown event on the list box. - * @param {Event} event - Triggering event. - */ - onKeyDown(event) { - if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) { - event.preventDefault(); - this.props.onMoveSelectionDown(); - } else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) { - event.preventDefault(); - this.props.onMoveSelectionUp(); - } else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) { - let selectedNode = this.container.querySelector('.item.selected'); - if (selectedNode.dataset.url) { - this.onOpenSelected(selectedNode.dataset.url, event); - } else if (selectedNode) { - this.props.onToggleBranch(selectedNode.dataset.id); - } - } - }, - - onBookmarkTab() { - let item = this._getSelectedTabNode(); - if (item) { - let title = item.querySelector(".item-title").textContent; - this.props.onBookmarkTab(item.dataset.url, title); - } - }, - - onCopyTabLocation() { - let item = this._getSelectedTabNode(); - if (item) { - this.props.onCopyTabLocation(item.dataset.url); - } - }, - - onOpenSelected(url, event) { - let where = getChromeWindow(this._window).whereToOpenLink(event); - this.props.onOpenTab(url, where, {}); - }, - - onOpenSelectedFromContextMenu(event) { - let item = this._getSelectedTabNode(); - if (item) { - let where = event.target.getAttribute("where"); - let params = { - private: event.target.hasAttribute("private"), - }; - this.props.onOpenTab(item.dataset.url, where, params); - } - }, - - onFilter(event) { - let query = event.target.value; - if (query) { - this.props.onFilter(query); - } else { - this.props.onClearFilter(); - } - }, - - onClearFilter() { - this.props.onClearFilter(); - }, - - onFilterFocus() { - this.props.onFilterFocus(); - }, - onFilterBlur() { - this.props.onFilterBlur(); - }, - - _getSelectedTabNode() { - let item = this.container.querySelector('.item.selected'); - if (this._isTab(item) && item.dataset.url) { - return item; - } - return null; - }, - - // Set up the custom context menu - _setupContextMenu() { - Services.els.addSystemEventListener(this._window, "contextmenu", this, false); - for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) { - let menu = getMenu(this._window); - menu.addEventListener("popupshowing", this, true); - menu.addEventListener("command", this, true); - } - }, - - _teardownContextMenu() { - // Tear down context menu - Services.els.removeSystemEventListener(this._window, "contextmenu", this, false); - for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) { - let menu = getMenu(this._window); - menu.removeEventListener("popupshowing", this, true); - menu.removeEventListener("command", this, true); - } - }, - - handleEvent(event) { - switch (event.type) { - case "contextmenu": - this.handleContextMenu(event); - break; - - case "popupshowing": { - if (event.target.getAttribute("id") == "SyncedTabsSidebarTabsFilterContext") { - this.handleTabsFilterContextMenuShown(event); - } - break; - } - - case "command": { - let menu = event.target.closest("menupopup"); - switch (menu.getAttribute("id")) { - case "SyncedTabsSidebarContext": - this.handleContentContextMenuCommand(event); - break; - - case "SyncedTabsSidebarTabsFilterContext": - this.handleTabsFilterContextMenuCommand(event); - break; - } - break; - } - } - }, - - handleTabsFilterContextMenuShown(event) { - let document = event.target.ownerDocument; - let focusedElement = document.commandDispatcher.focusedElement; - if (focusedElement != this.tabsFilter) { - this.tabsFilter.focus(); - } - for (let item of event.target.children) { - if (!item.hasAttribute("cmd")) { - continue; - } - let command = item.getAttribute("cmd"); - let controller = document.commandDispatcher.getControllerForCommand(command); - if (controller.isCommandEnabled(command)) { - item.removeAttribute("disabled"); - } else { - item.setAttribute("disabled", "true"); - } - } - }, - - handleContentContextMenuCommand(event) { - let id = event.target.getAttribute("id"); - switch (id) { - case "syncedTabsOpenSelected": - case "syncedTabsOpenSelectedInTab": - case "syncedTabsOpenSelectedInWindow": - case "syncedTabsOpenSelectedInPrivateWindow": - this.onOpenSelectedFromContextMenu(event); - break; - case "syncedTabsBookmarkSelected": - this.onBookmarkTab(); - break; - case "syncedTabsCopySelected": - this.onCopyTabLocation(); - break; - case "syncedTabsRefresh": - case "syncedTabsRefreshFilter": - this.props.onSyncRefresh(); - break; - } - }, - - handleTabsFilterContextMenuCommand(event) { - let command = event.target.getAttribute("cmd"); - let dispatcher = getChromeWindow(this._window).document.commandDispatcher; - let controller = dispatcher.focusedElement.controllers.getControllerForCommand(command); - controller.doCommand(command); - }, - - handleContextMenu(event) { - let menu; - - if (event.target == this.tabsFilter) { - menu = getTabsFilterContextMenu(this._window); - } else { - let itemNode = this._findParentItemNode(event.target); - if (itemNode) { - let position = this._getSelectionPosition(itemNode); - this.props.onSelectRow(position); - } - menu = getContextMenu(this._window); - this.adjustContextMenu(menu); - } - - menu.openPopupAtScreen(event.screenX, event.screenY, true, event); - }, - - adjustContextMenu(menu) { - let item = this.container.querySelector('.item.selected'); - let showTabOptions = this._isTab(item); - - let el = menu.firstChild; - - while (el) { - if (showTabOptions || el.getAttribute("id") === "syncedTabsRefresh") { - el.hidden = false; - } else { - el.hidden = true; - } - - el = el.nextSibling; - } - }, - - /** - * Find the parent item element, from a given child element. - * @param {Element} node - Child element. - * @return {Element} Element for the item, or null if not found. - */ - _findParentItemNode(node) { - while (node && node !== this.list && node !== this._doc.documentElement && - !node.classList.contains("item")) { - node = node.parentNode; - } - - if (node !== this.list && node !== this._doc.documentElement) { - return node; - } - - return null; - }, - - _findParentBranchNode(node) { - while (node && !node.classList.contains("list") && node !== this._doc.documentElement && - !node.parentNode.classList.contains("list")) { - node = node.parentNode; - } - - if (node !== this.list && node !== this._doc.documentElement) { - return node; - } - - return null; - }, - - _getSelectionPosition(itemNode) { - let parent = this._findParentBranchNode(itemNode); - let parentPosition = this._indexOfNode(parent.parentNode, parent); - let childPosition = -1; - // if the node is not a client, find its position within the parent - if (parent !== itemNode) { - childPosition = this._indexOfNode(itemNode.parentNode, itemNode); - } - return [parentPosition, childPosition]; - }, - - _indexOfNode(parent, child) { - return Array.prototype.indexOf.call(parent.childNodes, child); - }, - - _isTab(item) { - return item && item.classList.contains("tab"); - } -}; diff --git a/application/basilisk/components/syncedtabs/jar.mn b/application/basilisk/components/syncedtabs/jar.mn deleted file mode 100644 index ba2b105a1..000000000 --- a/application/basilisk/components/syncedtabs/jar.mn +++ /dev/null @@ -1,7 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -browser.jar: - content/browser/syncedtabs/sidebar.xhtml - content/browser/syncedtabs/sidebar.js diff --git a/application/basilisk/components/syncedtabs/moz.build b/application/basilisk/components/syncedtabs/moz.build deleted file mode 100644 index a6515d6a1..000000000 --- a/application/basilisk/components/syncedtabs/moz.build +++ /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/. - -JAR_MANIFESTS += ['jar.mn'] - -EXTRA_JS_MODULES.syncedtabs += [ - 'EventEmitter.jsm', - 'SyncedTabsDeckComponent.js', - 'SyncedTabsDeckStore.js', - 'SyncedTabsDeckView.js', - 'SyncedTabsListStore.js', - 'TabListComponent.js', - 'TabListView.js', - 'util.js', -] - diff --git a/application/basilisk/components/syncedtabs/sidebar.js b/application/basilisk/components/syncedtabs/sidebar.js deleted file mode 100644 index 84df95e9d..000000000 --- a/application/basilisk/components/syncedtabs/sidebar.js +++ /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/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://services-sync/SyncedTabs.jsm"); -Cu.import("resource:///modules/syncedtabs/SyncedTabsDeckComponent.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", - "resource://gre/modules/FxAccounts.jsm"); - -this.syncedTabsDeckComponent = new SyncedTabsDeckComponent({window, SyncedTabs, fxAccounts}); - -let onLoaded = () => { - syncedTabsDeckComponent.init(); - document.getElementById("template-container").appendChild(syncedTabsDeckComponent.container); -}; - -let onUnloaded = () => { - removeEventListener("DOMContentLoaded", onLoaded); - removeEventListener("unload", onUnloaded); - syncedTabsDeckComponent.uninit(); -}; - -addEventListener("DOMContentLoaded", onLoaded); -addEventListener("unload", onUnloaded); diff --git a/application/basilisk/components/syncedtabs/sidebar.xhtml b/application/basilisk/components/syncedtabs/sidebar.xhtml deleted file mode 100644 index 3efcbea0e..000000000 --- a/application/basilisk/components/syncedtabs/sidebar.xhtml +++ /dev/null @@ -1,114 +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.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" [ - <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> - %browserDTD; - <!ENTITY % globalDTD - SYSTEM "chrome://global/locale/global.dtd"> - %globalDTD; - <!ENTITY % syncBrandDTD - SYSTEM "chrome://browser/locale/syncBrand.dtd"> - %syncBrandDTD; -]> -<html xmlns="http://www.w3.org/1999/xhtml" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - <head> - <script src="chrome://browser/content/syncedtabs/sidebar.js" type="application/javascript;version=1.8"></script> - <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> - - <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/syncedtabs/sidebar.css"/> - <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/"/> - <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/textbox.css"/> - <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/browser.css"/> - <title>&syncedTabs.sidebar.label;</title> - </head> - - <body dir="&locale.dir;" role="application"> - <template id="client-template"> - <div class="item client" role="option" tabindex="-1"> - <div class="item-title-container"> - <div class="item-twisty-container"></div> - <div class="item-icon-container"></div> - <p class="item-title"></p> - </div> - <div class="item-tabs-list"></div> - </div> - </template> - <template id="empty-client-template"> - <div class="item empty client" role="option" tabindex="-1"> - <div class="item-title-container"> - <div class="item-twisty-container"></div> - <div class="item-icon-container"></div> - <p class="item-title"></p> - </div> - <div class="item-tabs-list"> - <div class="item empty" role="option" tabindex="-1"> - <div class="item-title-container"> - <div class="item-icon-container"></div> - <p class="item-title">&syncedTabs.sidebar.notabs.label;</p> - </div> - </div> - </div> - </div> - </template> - <template id="tab-template"> - <div class="item tab" role="option" tabindex="-1"> - <div class="item-title-container"> - <div class="item-icon-container"></div> - <p class="item-title"></p> - </div> - </div> - </template> - - <template id="tabs-container-template"> - <div class="tabs-container"> - <div class="list" role="listbox"></div> - </div> - </template> - - <template id="deck-template"> - <div class="deck"> - <div class="tabs-fetching sync-state"> - <!-- Show intentionally blank panel, see bug 1239845 --> - </div> - <div class="notAuthedInfo sync-state"> - <p>&syncedTabs.sidebar.notsignedin.label;</p> - <p><a href="#" class="sync-prefs text-link">&fxaSignIn.label;</a></p> - </div> - <div class="singleDeviceInfo sync-state"> - <p>&syncedTabs.sidebar.noclients.title;</p> - <p>&syncedTabs.sidebar.noclients.subtitle;</p> - <p class="device-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"></p> - </div> - <div class="tabs-disabled sync-state"> - <p>&syncedTabs.sidebar.tabsnotsyncing.label;</p> - <p><a href="#" class="sync-prefs text-link">&syncedTabs.sidebar.openprefs.label;</a></p> - </div> - </div> - </template> - - <div class="content-container"> - <!-- the non-scrollable header --> - <div class="content-header"> - <div class="sidebar-search-container tabs-container sync-state"> - <div class="search-box compact"> - <div class="textbox-input-box"> - <input type="text" class="tabsFilter textbox-input" tabindex="1"/> - <div class="textbox-search-icons"> - <a class="textbox-search-clear"></a> - <a class="textbox-search-icon"></a> - </div> - </div> - </div> - </div> - </div> - <!-- the scrollable content area where our templates are inserted --> - <div id="template-container" class="content-scrollable" tabindex="-1"> - </div> - </div> - </body> -</html> diff --git a/application/basilisk/components/syncedtabs/util.js b/application/basilisk/components/syncedtabs/util.js deleted file mode 100644 index e09a1a528..000000000 --- a/application/basilisk/components/syncedtabs/util.js +++ /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/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "getChromeWindow" -]; - -// Get the chrome (ie, browser) window hosting this content. -function getChromeWindow(window) { - return window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) - .wrappedJSObject; -} diff --git a/application/basilisk/configure.in b/application/basilisk/configure.in index 4e7a06390..13f2ad54d 100644 --- a/application/basilisk/configure.in +++ b/application/basilisk/configure.in @@ -16,3 +16,10 @@ AC_SUBST(MC_BASILISK) dnl Optional parts of the build. +dnl ======================================================== +dnl = Disable Sync +dnl ======================================================== +MOZ_ARG_DISABLE_BOOL(sync, +[ --disable-sync Disable Sync], + MOZ_SERVICES_SYNC=, + MOZ_SERVICES_SYNC=1)
\ No newline at end of file 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/locales/en-US/chrome/browser/aboutAccounts.dtd b/application/basilisk/locales/en-US/chrome/browser/aboutAccounts.dtd deleted file mode 100644 index 358722156..000000000 --- a/application/basilisk/locales/en-US/chrome/browser/aboutAccounts.dtd +++ /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/. --> - -<!ENTITY aboutAccounts.welcome "Welcome to &syncBrand.shortName.label;"> -<!ENTITY aboutAccounts.connected "Account connected"> - -<!ENTITY aboutAccountsConfig.description "Sign in to sync your tabs, bookmarks, passwords & more."> -<!ENTITY aboutAccountsConfig.startButton.label "Get started"> -<!ENTITY aboutAccountsConfig.useOldSync.label "Using an older version of Sync?"> -<!ENTITY aboutAccountsConfig.syncPreferences.label "Sync preferences"> -<!ENTITY aboutAccounts.noConnection.title "No connection"> -<!ENTITY aboutAccounts.noConnection.description "You must be connected to the Internet to sign in."> -<!ENTITY aboutAccounts.noConnection.retry "Try again"> -<!ENTITY aboutAccounts.badConfig.title "Bad configuration"> -<!ENTITY aboutAccounts.badConfig.description "Unable to determine your Firefox Account server configuration. Please try again later."> diff --git a/application/basilisk/locales/en-US/chrome/browser/aboutHome.dtd b/application/basilisk/locales/en-US/chrome/browser/aboutHome.dtd index 17b401c6c..40681c337 100644 --- a/application/basilisk/locales/en-US/chrome/browser/aboutHome.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/aboutHome.dtd @@ -4,8 +4,10 @@ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; +#ifdef MOZ_SERVICES_SYNC <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> %syncBrandDTD; +#endif <!-- These strings are used in the about:home page --> @@ -32,7 +34,9 @@ <!ENTITY abouthome.preferencesButtonUnix.label "Preferences"> <!ENTITY abouthome.addonsButton.label "Add-ons"> <!ENTITY abouthome.downloadsButton.label "Downloads"> +#ifdef MOZ_SERVICES_SYNC <!ENTITY abouthome.syncButton.label "&syncBrand.shortName.label;"> +#endif <!-- LOCALIZATION NOTE (abouthome.aboutMozilla.label): The (invisible) label for the mozilla wordmark in the top-right corner that links to Mozilla's main diff --git a/application/basilisk/locales/en-US/chrome/browser/browser.dtd b/application/basilisk/locales/en-US/chrome/browser/browser.dtd index d02a6eedb..fe856129f 100644 --- a/application/basilisk/locales/en-US/chrome/browser/browser.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/browser.dtd @@ -40,12 +40,6 @@ can reach it easily. --> <!ENTITY pinTab.accesskey "P"> <!ENTITY unpinTab.label "Unpin Tab"> <!ENTITY unpinTab.accesskey "b"> -<!ENTITY sendTabToDevice.label "Send Tab to Device"> -<!ENTITY sendTabToDevice.accesskey "D"> -<!ENTITY sendPageToDevice.label "Send Page to Device"> -<!ENTITY sendPageToDevice.accesskey "D"> -<!ENTITY sendLinkToDevice.label "Send Link to Device"> -<!ENTITY sendLinkToDevice.accesskey "D"> <!ENTITY moveToNewWindow.label "Move to New Window"> <!ENTITY moveToNewWindow.accesskey "W"> <!ENTITY bookmarkAllTabs.label "Bookmark All Tabs…"> @@ -115,11 +109,6 @@ These should match what Safari and other Apple applications use on OS X Lion. -- <!ENTITY showAllTabsCmd.accesskey "A"> <!ENTITY toggleReaderMode.key "R"> -<!ENTITY fxaSignIn.label "Sign in to &syncBrand.shortName.label;"> -<!ENTITY fxaSignInError.label "Reconnect to &syncBrand.shortName.label;"> -<!ENTITY fxaUnverified.label "Verify Your Account"> -<!ENTITY syncSettings.label "Open &syncBrand.shortName.label; settings"> - <!ENTITY fullScreenMinimize.tooltip "Minimize"> <!ENTITY fullScreenRestore.tooltip "Restore"> <!ENTITY fullScreenClose.tooltip "Close"> @@ -333,23 +322,6 @@ These should match what Safari and other Apple applications use on OS X Lion. -- <!ENTITY appMenuHistory.viewSidebar.label "View History Sidebar"> <!ENTITY appMenuHelp.tooltip "Open Help Menu"> -<!ENTITY appMenuRemoteTabs.label "Synced Tabs"> -<!-- LOCALIZATION NOTE (appMenuRemoteTabs.notabs.label): This is shown beneath - the name of a device when that device has no open tabs --> -<!ENTITY appMenuRemoteTabs.notabs.label "No open tabs"> -<!-- LOCALIZATION NOTE (appMenuRemoteTabs.tabsnotsyncing.label): This is shown - when Sync is configured but syncing tabs is disabled. --> -<!ENTITY appMenuRemoteTabs.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices."> -<!-- LOCALIZATION NOTE (appMenuRemoteTabs.noclients.label): This is shown - when Sync is configured but this appears to be the only device attached to - the account. We also show links to download Firefox for android/ios. --> -<!ENTITY appMenuRemoteTabs.noclients.title "No synced tabs… yet!"> -<!ENTITY appMenuRemoteTabs.noclients.subtitle "Want to see your tabs from other devices here?"> -<!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences"> -<!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices."> -<!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync"> -<!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs"> - <!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar"> <!ENTITY customizeMenu.addToToolbar.accesskey "A"> <!ENTITY customizeMenu.addToPanel.label "Add to Menu"> @@ -723,43 +695,19 @@ you can use these alternative items. Otherwise, their values should be empty. - The word "toolbar" is appended automatically and should not be contained below! --> <!ENTITY tabsToolbar.label "Browser tabs"> -<!-- LOCALIZATION NOTE (syncTabsMenu3.label): This appears in the history menu --> -<!ENTITY syncTabsMenu3.label "Synced Tabs"> - -<!ENTITY syncedTabs.sidebar.label "Synced Tabs"> -<!ENTITY syncedTabs.sidebar.noclients.label "Sign in to Firefox from your other devices to view their tabs here."> -<!ENTITY syncedTabs.sidebar.noclients.title "No synced tabs… yet!"> -<!ENTITY syncedTabs.sidebar.noclients.subtitle "Want to see your tabs from other devices here?"> -<!ENTITY syncedTabs.sidebar.notsignedin.label "Sign in to view a list of tabs from your other devices."> -<!ENTITY syncedTabs.sidebar.notabs.label "No open tabs"> -<!ENTITY syncedTabs.sidebar.openprefs.label "Open &syncBrand.shortName.label; Preferences"> -<!-- LOCALIZATION NOTE (syncedTabs.sidebar.tabsnotsyncing.label): This is shown - when Sync is configured but syncing tabs is disabled. --> -<!ENTITY syncedTabs.sidebar.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices."> - -<!ENTITY syncedTabs.context.open.label "Open"> -<!ENTITY syncedTabs.context.open.accesskey "O"> -<!ENTITY syncedTabs.context.openInNewTab.label "Open in a New Tab"> -<!ENTITY syncedTabs.context.openInNewTab.accesskey "w"> -<!ENTITY syncedTabs.context.openInNewWindow.label "Open in a New Window"> -<!ENTITY syncedTabs.context.openInNewWindow.accesskey "N"> -<!ENTITY syncedTabs.context.openInNewPrivateWindow.label "Open in a New Private Window"> -<!ENTITY syncedTabs.context.openInNewPrivateWindow.accesskey "P"> -<!ENTITY syncedTabs.context.bookmarkSingleTab.label "Bookmark This Tab…"> -<!ENTITY syncedTabs.context.bookmarkSingleTab.accesskey "B"> -<!ENTITY syncedTabs.context.copy.label "Copy"> -<!ENTITY syncedTabs.context.copy.accesskey "C"> - +#ifdef MOZ_SERVICES_SYNC +<!-- LOCALIZATION NOTE (syncTabsMenu2.label): This appears in the history menu --> +<!ENTITY syncTabsMenu2.label "Tabs From Other Devices"> <!ENTITY syncBrand.shortName.label "Sync"> -<!ENTITY syncSignIn.label "Sign In To &syncBrand.shortName.label;…"> -<!ENTITY syncSignIn.accesskey "Y"> +<!ENTITY syncSetup.label "Set Up &syncBrand.shortName.label;…"> +<!ENTITY syncSetup.accesskey "Y"> <!ENTITY syncSyncNowItem.label "Sync Now"> <!ENTITY syncSyncNowItem.accesskey "S"> -<!ENTITY syncReAuthItem.label "Reconnect to &syncBrand.shortName.label;…"> -<!ENTITY syncReAuthItem.accesskey "R"> <!ENTITY syncToolbarButton.label "Sync"> +<!ENTITY syncTabsToolbarButton.label "Synced Tabs"> +#endif <!ENTITY customizeMode.menuAndToolbars.header2 "Additional Tools and Features"> <!ENTITY customizeMode.menuAndToolbars.empty "Want more tools?"> diff --git a/application/basilisk/locales/en-US/chrome/browser/preferences/sync.dtd b/application/basilisk/locales/en-US/chrome/browser/preferences/sync.dtd index a5b290052..f6ef3b876 100644 --- a/application/basilisk/locales/en-US/chrome/browser/preferences/sync.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/preferences/sync.dtd @@ -39,80 +39,9 @@ <!-- Device Settings --> <!ENTITY syncDeviceName.label "Device Name:"> -<!ENTITY fxaSyncDeviceName.label "Device Name"> -<!ENTITY changeSyncDeviceName.label "Change Device Name…"> -<!ENTITY changeSyncDeviceName.accesskey "h"> -<!ENTITY cancelChangeSyncDeviceName.label "Cancel"> -<!ENTITY cancelChangeSyncDeviceName.accesskey "n"> -<!ENTITY saveChangeSyncDeviceName.label "Save"> -<!ENTITY saveChangeSyncDeviceName.accesskey "v"> +<!ENTITY syncDeviceName.accesskey "c"> <!ENTITY unlinkDevice.label "Unlink This Device"> <!-- Footer stuff --> <!ENTITY prefs.tosLink.label "Terms of Service"> <!ENTITY prefs.ppLink.label "Privacy Policy"> - -<!-- Firefox Accounts stuff --> -<!ENTITY fxaPrivacyNotice.link.label "Privacy Notice"> -<!ENTITY determiningAcctStatus.label "Determining your account status…"> - -<!-- LOCALIZATION NOTE (signedInUnverified.beforename.label, -signedInUnverified.aftername.label): these two string are used respectively -before and after the account email address. Localizers can use one of them, or -both, to better adapt this sentence to their language. ---> -<!ENTITY signedInUnverified.beforename.label ""> -<!ENTITY signedInUnverified.aftername.label "is not verified."> - -<!-- LOCALIZATION NOTE (signedInLoginFailure.beforename.label, -signedInLoginFailure.aftername.label): these two string are used respectively -before and after the account email address. Localizers can use one of them, or -both, to better adapt this sentence to their language. ---> -<!ENTITY signedInLoginFailure.beforename.label "Please sign in to reconnect"> -<!ENTITY signedInLoginFailure.aftername.label ""> - -<!ENTITY notSignedIn.label "You are not signed in."> -<!ENTITY signIn.label "Sign in"> -<!ENTITY signIn.accesskey "g"> -<!ENTITY profilePicture.tooltip "Change profile picture"> -<!ENTITY verifiedManage.label "Manage Account"> -<!ENTITY verifiedManage.accesskey "o"> -<!ENTITY disconnect.label "Disconnect…"> -<!ENTITY disconnect.accesskey "D"> -<!ENTITY verify.label "Verify Email"> -<!ENTITY verify.accesskey "V"> -<!ENTITY forget.label "Forget this Email"> -<!ENTITY forget.accesskey "F"> - -<!ENTITY welcome.description "Access your tabs, bookmarks, passwords and more wherever you use &brandShortName;."> -<!ENTITY welcome.signIn.label "Sign In"> -<!ENTITY welcome.createAccount.label "Create Account"> - -<!ENTITY welcome.useOldSync.label "Using an older version of Sync?"> - -<!ENTITY signedOut.caption "Take your Web with you"> -<!ENTITY signedOut.description "Synchronize your bookmarks, history, tabs, passwords, add-ons, and preferences across all your devices."> -<!ENTITY signedOut.accountBox.title "Connect with a &syncBrand.fxAccount.label;"> -<!ENTITY signedOut.accountBox.create "Create Account"> -<!ENTITY signedOut.accountBox.create.accesskey "C"> -<!ENTITY signedOut.accountBox.signin "Sign In"> -<!ENTITY signedOut.accountBox.signin.accesskey "I"> - -<!ENTITY signedIn.engines.label "Sync across all devices"> - -<!-- LOCALIZATION NOTE (mobilePromo3.*): the following strings will be used to - create a single sentence with active links. - The resulting sentence in English is: "Download Firefox for - Android or iOS to sync with your mobile device." --> - -<!ENTITY mobilePromo3.start "Download Firefox for "> -<!-- LOCALIZATION NOTE (mobilePromo3.androidLink): This is a link title that links to https://www.mozilla.org/firefox/android/ --> -<!ENTITY mobilePromo3.androidLink "Android"> - -<!-- LOCALIZATION NOTE (mobilePromo3.iOSBefore): This is text displayed between mobilePromo3.androidLink and mobilePromo3.iosLink --> -<!ENTITY mobilePromo3.iOSBefore " or "> -<!-- LOCALIZATION NOTE (mobilePromo3.iOSLink): This is a link title that links to https://www.mozilla.org/firefox/ios/ --> -<!ENTITY mobilePromo3.iOSLink "iOS"> - -<!ENTITY mobilePromo3.end " to sync with your mobile device."> diff --git a/application/basilisk/locales/en-US/chrome/browser/syncBrand.dtd b/application/basilisk/locales/en-US/chrome/browser/syncBrand.dtd index 71a9f68af..bc4d1b3e0 100644 --- a/application/basilisk/locales/en-US/chrome/browser/syncBrand.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/syncBrand.dtd @@ -4,4 +4,3 @@ <!ENTITY syncBrand.shortName.label "Sync"> <!ENTITY syncBrand.fullName.label "Pale Moon Sync"> -<!ENTITY syncBrand.fxAccount.label "Firefox Account"> diff --git a/application/basilisk/locales/en-US/chrome/browser/syncKey.dtd b/application/basilisk/locales/en-US/chrome/browser/syncKey.dtd index 2ff001842..f37f2c92e 100644 --- a/application/basilisk/locales/en-US/chrome/browser/syncKey.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/syncKey.dtd @@ -5,12 +5,12 @@ <!ENTITY syncKey.page.title "Your &syncBrand.fullName.label; Key"> <!ENTITY syncKey.page.description2 "This key is used to decode the data in your &syncBrand.fullName.label; account. You will need to enter the key each time you configure &syncBrand.fullName.label; on a new device."> <!ENTITY syncKey.keepItSecret.heading "Keep it secret"> -<!ENTITY syncKey.keepItSecret.description "Your &syncBrand.fullName.label; account is encrypted to protect your privacy. Without this key, it would take years for anyone to decode your personal information. You are the only person who holds this key. This means you’re the only one who can access your &syncBrand.fullName.label; data."> +<!ENTITY syncKey.keepItSecret.description "Your &syncBrand.fullName.label; account is encrypted to protect your privacy. Without this key, it would take years for anyone to decode your personal information. You are the only person who holds this key. This means you're the only one who can access your &syncBrand.fullName.label; data."> <!ENTITY syncKey.keepItSafe.heading "Keep it safe"> <!ENTITY syncKey.keepItSafe1.description "Do not lose this key."> -<!ENTITY syncKey.keepItSafe2.description " We don’t keep a copy of your key (that wouldn’t be keeping it secret!) so "> -<!ENTITY syncKey.keepItSafe3.description "we can’t help you recover it"> -<!ENTITY syncKey.keepItSafe4a.description " if it’s lost. You’ll need to use this key any time you connect a new device to &syncBrand.fullName.label;."> +<!ENTITY syncKey.keepItSafe2.description " We don't keep a copy of your key (that wouldn't be keeping it secret!) so "> +<!ENTITY syncKey.keepItSafe3.description "we can't help you recover it"> +<!ENTITY syncKey.keepItSafe4a.description " if it's lost. You'll need to use this key any time you connect a new device to &syncBrand.fullName.label;."> <!ENTITY syncKey.findOutMore1.label "Find out more about &syncBrand.fullName.label; and your privacy at "> <!ENTITY syncKey.findOutMore2.label "."> <!ENTITY syncKey.footer1.label "&syncBrand.fullName.label; Terms of Service are available at "> diff --git a/application/basilisk/locales/en-US/chrome/browser/syncProgress.dtd b/application/basilisk/locales/en-US/chrome/browser/syncProgress.dtd new file mode 100644 index 000000000..db45cb935 --- /dev/null +++ b/application/basilisk/locales/en-US/chrome/browser/syncProgress.dtd @@ -0,0 +1,15 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!ENTITY % brandDTD + SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + +<!-- These strings are used in the sync progress upload page --> +<!ENTITY syncProgress.pageTitle "Your First Sync"> +<!ENTITY syncProgress.textBlurb "Your data is now being encrypted and uploaded in the background. You can close this tab and continue using &brandShortName;."> +<!ENTITY syncProgress.closeButton "Close"> +<!ENTITY syncProgress.logoAltText "&brandShortName; logo"> +<!ENTITY syncProgress.diffText "&brandShortName; will now automatically sync in the background. You can close this tab and continue using &brandShortName;."> + diff --git a/application/basilisk/locales/en-US/chrome/browser/syncQuota.properties b/application/basilisk/locales/en-US/chrome/browser/syncQuota.properties index 099090ec9..0e1b857ca 100644 --- a/application/basilisk/locales/en-US/chrome/browser/syncQuota.properties +++ b/application/basilisk/locales/en-US/chrome/browser/syncQuota.properties @@ -30,7 +30,7 @@ quota.remove.label = Remove quota.treeCaption.label = Uncheck items to stop syncing them and free up space on the server. # LOCALIZATION NOTE (quota.removal.label): %S is a list of engines that will be # disabled and whose data will be removed once the user confirms. -quota.removal.label = Firefox Sync will remove the following data: %S. +quota.removal.label = Sync will remove the following data: %S. # LOCALIZATION NOTE (quota.list.separator): This is the separator string used # for the list of engines (incl. spaces where appropriate) quota.list.separator = ,\u0020 diff --git a/application/basilisk/locales/en-US/chrome/browser/syncSetup.dtd b/application/basilisk/locales/en-US/chrome/browser/syncSetup.dtd index 2657156b7..7ee938e5d 100644 --- a/application/basilisk/locales/en-US/chrome/browser/syncSetup.dtd +++ b/application/basilisk/locales/en-US/chrome/browser/syncSetup.dtd @@ -6,13 +6,13 @@ <!-- First page of the wizard --> -<!ENTITY setup.pickSetupType.description2 "Welcome! If you’ve never used &syncBrand.fullName.label; before, you will need to create a new account."> +<!ENTITY setup.pickSetupType.description2 "Welcome! If you've never used &syncBrand.fullName.label; before, you will need to create a new account."> <!ENTITY button.createNewAccount.label "Create a New Account"> <!ENTITY button.haveAccount.label "I Have an Account"> <!ENTITY setup.choicePage.title.label "Have you used &syncBrand.fullName.label; before?"> -<!ENTITY setup.choicePage.new.label "I’ve never used &syncBrand.shortName.label; before"> -<!ENTITY setup.choicePage.existing2.label "I’m already using &syncBrand.shortName.label; on another device"> +<!ENTITY setup.choicePage.new.label "I've never used &syncBrand.shortName.label; before"> +<!ENTITY setup.choicePage.existing2.label "I'm already using &syncBrand.shortName.label; on another device"> <!-- New Account AND Existing Account --> <!ENTITY server.label "Server"> @@ -33,6 +33,8 @@ <!ENTITY setup.choosePassword.accesskey "P"> <!ENTITY setup.confirmPassword.label "Confirm Password"> <!ENTITY setup.confirmPassword.accesskey "m"> +<!ENTITY setup.setupMetro.label "Sync with Windows 8 style &brandShortName;"> +<!ENTITY setup.setupMetro.accesskey "S"> <!-- LOCALIZATION NOTE: tosAgree1, tosLink, tosAgree2, ppLink, tosAgree3 are joined with implicit white space, so spaces in the strings aren't necessary --> @@ -60,7 +62,7 @@ <!-- Existing Account Page 1: Pair a Device (incl. Pair a Device dialog strings) --> <!ENTITY pairDevice.title.label "Pair a Device"> <!ENTITY addDevice.showMeHow.label "Show me how."> -<!ENTITY addDevice.dontHaveDevice.label "I don’t have the device with me"> +<!ENTITY addDevice.dontHaveDevice.label "I don't have the device with me"> <!ENTITY pairDevice.setup.description.label "To activate, select "Pair a Device" on your other device."> <!ENTITY addDevice.setup.enterCode.label "Then, enter this code:"> <!ENTITY pairDevice.dialog.description.label "To activate your new device, select "Set Up Sync" on the device."> @@ -72,7 +74,7 @@ <!-- Existing Account Page 2: Manual Login --> <!ENTITY setup.signInPage.title.label "Sign In"> -<!ENTITY existingRecoveryKey.description "You can get a copy of your Recovery Key by going to &syncBrand.shortName.label; Preferences on your other device, and selecting "My Recovery Key" under "Manage Account"."> +<!ENTITY existingRecoveryKey.description "You can get a copy of your Recovery Key by going to &syncBrand.shortName.label; Options on your other device, and selecting "My Recovery Key" under "Manage Account"."> <!ENTITY verifying.label "Verifying…"> <!ENTITY resetPassword.label "Reset Password"> <!ENTITY resetSyncKey.label "I have lost my other device."> @@ -96,14 +98,14 @@ <!ENTITY engine.addons.label "Add-ons"> <!ENTITY engine.addons.accesskey "A"> -<!ENTITY choice2a.merge.main.label "Merge this device’s data with my &syncBrand.shortName.label; data"> +<!ENTITY choice2a.merge.main.label "Merge this device's data with my &syncBrand.shortName.label; data"> <!ENTITY choice2.merge.recommended.label "Recommended:"> <!ENTITY choice2a.client.main.label "Replace all data on this device with my &syncBrand.shortName.label; data"> -<!ENTITY choice2a.server.main.label "Replace all other devices with this device’s data"> +<!ENTITY choice2a.server.main.label "Replace all other devices with this device's data"> <!-- Confirm Merge Options --> <!ENTITY setup.optionsConfirmPage.title "Confirm"> -<!ENTITY confirm.merge2.label "&syncBrand.fullName.label; will now merge all this device’s browser data into your Sync account."> +<!ENTITY confirm.merge2.label "&syncBrand.fullName.label; will now merge all this device's browser data into your Sync account."> <!ENTITY confirm.client3.label "Warning: The following &brandShortName; data on this device will be deleted:"> <!ENTITY confirm.client2.moreinfo.label "&brandShortName; will then copy your &syncBrand.fullName.label; data to this device."> <!ENTITY confirm.server2.label "Warning: The following devices will be overwritten with your local data:"> diff --git a/application/basilisk/locales/en-US/chrome/browser/syncSetup.properties b/application/basilisk/locales/en-US/chrome/browser/syncSetup.properties index 9d388af86..8a5170adb 100644 --- a/application/basilisk/locales/en-US/chrome/browser/syncSetup.properties +++ b/application/basilisk/locales/en-US/chrome/browser/syncSetup.properties @@ -35,33 +35,17 @@ passwordsCount.label = #1 password;#1 passwords # LOCALIZATION NOTE (addonsCount.label): Semicolon-separated list of plural forms. # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals # #1 is the number of add-ons, see the link above for forms -addonsCount.label = #1 add-on;#1 add-ons +addonsCount.label = #1 addon;#1 addons save.recoverykey.title = Save Recovery Key -save.recoverykey.defaultfilename = Firefox Recovery Key.html +save.recoverykey.defaultfilename = Pale Moon Recovery Key.html -newAccount.action.label = Firefox Sync is now set up to automatically sync all of your browser data. +newAccount.action.label = Sync is now set up to automatically sync all of your browser data. newAccount.change.label = You can choose exactly what to sync by selecting Sync Options below. -resetClient.change2.label = Firefox Sync will now merge all this device’s browser data into your Sync account. -wipeClient.change2.label = Firefox Sync will now replace all of the browser data on this device with the data in your Sync account. -wipeRemote.change2.label = Firefox Sync will now replace all of the browser data in your Sync account with the data on this device. +resetClient.change2.label = Sync will now merge all this device's browser data into your Sync account. +wipeClient.change2.label = Sync will now replace all of the browser data on this device with the data in your Sync account. +wipeRemote.change2.label = Sync will now replace all of the browser data in your Sync account with the data on this device. existingAccount.change.label = You can change this preference by selecting Sync Options below. # Several other strings are used (via Weave.Status.login), but they come from # /services/sync - -# Firefox Accounts based setup. -continue.label = Continue - -# LOCALIZATION NOTE (disconnect.label, disconnect.verify.title, disconnect.verify.bodyHeading, disconnect.verify.bodyText): -# These strings are used in the confirmation dialog shown when the user hits the disconnect button -# LOCALIZATION NOTE (disconnect.label): This is the label for the disconnect button -disconnect.label = Disconnect -disconnect.verify.title = Disconnect -disconnect.verify.bodyHeading = Disconnect from Sync? -disconnect.verify.bodyText = Your browsing data will remain on this computer, but it will no longer sync with your account. - -relinkVerify.title = Merge Warning -relinkVerify.heading = Are you sure you want to sign in to Sync? -# LOCALIZATION NOTE (relinkVerify.description): Email address of a user previously signed into sync. -relinkVerify.description = A different user was previously signed in to Sync on this computer. Signing in will merge this browser’s bookmarks, passwords and other settings with %S diff --git a/application/basilisk/locales/jar.mn b/application/basilisk/locales/jar.mn index 1a9b7f315..345c0525a 100644 --- a/application/basilisk/locales/jar.mn +++ b/application/basilisk/locales/jar.mn @@ -7,11 +7,10 @@ @AB_CD@.jar: % locale browser @AB_CD@ %locale/browser/ * locale/browser/bookmarks.html (generic/profile/bookmarks.html.in) - locale/browser/aboutAccounts.dtd (%chrome/browser/aboutAccounts.dtd) locale/browser/aboutDialog.dtd (%chrome/browser/aboutDialog.dtd) locale/browser/aboutPrivateBrowsing.dtd (%chrome/browser/aboutPrivateBrowsing.dtd) locale/browser/aboutPrivateBrowsing.properties (%chrome/browser/aboutPrivateBrowsing.properties) - locale/browser/aboutHome.dtd (%chrome/browser/aboutHome.dtd) +* locale/browser/aboutHome.dtd (%chrome/browser/aboutHome.dtd) locale/browser/accounts.properties (%chrome/browser/accounts.properties) #ifdef MOZ_SERVICES_HEALTHREPORT locale/browser/aboutHealthReport.dtd (%chrome/browser/aboutHealthReport.dtd) @@ -20,8 +19,7 @@ locale/browser/aboutSessionRestore.dtd (%chrome/browser/aboutSessionRestore.dtd) locale/browser/aboutTabCrashed.dtd (%chrome/browser/aboutTabCrashed.dtd) locale/browser/syncCustomize.dtd (%chrome/browser/syncCustomize.dtd) - locale/browser/aboutSyncTabs.dtd (%chrome/browser/aboutSyncTabs.dtd) - locale/browser/browser.dtd (%chrome/browser/browser.dtd) +* locale/browser/browser.dtd (%chrome/browser/browser.dtd) locale/browser/baseMenuOverlay.dtd (%chrome/browser/baseMenuOverlay.dtd) locale/browser/browser.properties (%chrome/browser/browser.properties) locale/browser/customizableui/customizableWidgets.properties (%chrome/browser/customizableui/customizableWidgets.properties) @@ -80,9 +78,12 @@ locale/browser/preferences/preferences.properties (%chrome/browser/preferences/preferences.properties) * locale/browser/preferences/privacy.dtd (%chrome/browser/preferences/privacy.dtd) locale/browser/preferences/security.dtd (%chrome/browser/preferences/security.dtd) +#ifdef MOZ_SERVICES_SYNC locale/browser/preferences/sync.dtd (%chrome/browser/preferences/sync.dtd) +#endif locale/browser/preferences/tabs.dtd (%chrome/browser/preferences/tabs.dtd) locale/browser/preferences/search.dtd (%chrome/browser/preferences/search.dtd) +#ifdef MOZ_SERVICES_SYNC locale/browser/syncBrand.dtd (%chrome/browser/syncBrand.dtd) locale/browser/syncSetup.dtd (%chrome/browser/syncSetup.dtd) locale/browser/syncSetup.properties (%chrome/browser/syncSetup.properties) @@ -90,6 +91,9 @@ locale/browser/syncKey.dtd (%chrome/browser/syncKey.dtd) locale/browser/syncQuota.dtd (%chrome/browser/syncQuota.dtd) locale/browser/syncQuota.properties (%chrome/browser/syncQuota.properties) + locale/browser/syncProgress.dtd (%chrome/browser/syncProgress.dtd) + locale/browser/aboutSyncTabs.dtd (%chrome/browser/aboutSyncTabs.dtd) +#endif % resource search-plugins chrome://browser/locale/searchplugins/ #if BUILD_FASTER locale/browser/searchplugins/ (searchplugins/*.xml) 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/application/basilisk/themes/linux/jar.mn b/application/basilisk/themes/linux/jar.mn index 189027812..e9f666418 100644 --- a/application/basilisk/themes/linux/jar.mn +++ b/application/basilisk/themes/linux/jar.mn @@ -8,8 +8,9 @@ browser.jar: #include ../shared/jar.inc.mn skin/classic/browser/sanitizeDialog.css skin/classic/browser/aboutSessionRestore-window-icon.png +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css -* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css) +#endif skin/classic/browser/actionicon-tab.png * skin/classic/browser/browser.css * skin/classic/browser/devedition.css @@ -107,6 +108,7 @@ browser.jar: skin/classic/browser/tabbrowser/tab-stroke-start@2x.png (tabbrowser/tab-stroke-start@2x.png) skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png) +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-16.png skin/classic/browser/sync-32.png skin/classic/browser/sync-bg.png @@ -125,6 +127,8 @@ browser.jar: skin/classic/browser/syncQuota.css skin/classic/browser/syncProgress-horizontalbar.png skin/classic/browser/syncProgress-horizontalbar@2x.png + skin/classic/browser/syncProgress.css +#endif [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: % override chrome://browser/skin/feeds/audioFeedIcon.png chrome://browser/skin/feeds/feedIcon.png diff --git a/application/basilisk/themes/linux/syncProgress.css b/application/basilisk/themes/linux/syncProgress.css new file mode 100644 index 000000000..d7aa59976 --- /dev/null +++ b/application/basilisk/themes/linux/syncProgress.css @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@import url(chrome://global/skin/inContentUI.css); + +:root { + height: 100%; + width: 100%; + padding: 0; +} + +body { + margin: 0; + padding: 0 2em; +} + +#floatingBox { + margin: 4em auto; + max-width: 40em; + min-width: 23em; + padding: 1em 1.5em; + position: relative; + text-align: center; +} + +#successLogo { + margin: 1em 2em; +} + +#loadingText { + margin: 2em 6em; +} + +#progressBar { + margin: 2em 10em; +} + +#uploadProgressBar{ + width: 100%; +} + +#bottomRow { + margin-top: 2em; + padding: 0; + text-align: end; +} diff --git a/application/basilisk/themes/linux/syncedtabs/sidebar.css b/application/basilisk/themes/linux/syncedtabs/sidebar.css deleted file mode 100644 index 04e00a7d4..000000000 --- a/application/basilisk/themes/linux/syncedtabs/sidebar.css +++ /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/. */ - -%include ../../shared/syncedtabs/sidebar.inc.css - -/* These styles are intended to mimic XUL trees and the XUL search box. */ - -html { - border: 1px solid ThreeDShadow; - background-color: -moz-Field; - color: -moz-FieldText; - box-sizing: border-box; -} - -.item { - padding-inline-end: 0; -} - -.item-title { - margin: 1px 0 0; - margin-inline-end: 6px; -} - - -.search-box { - -moz-appearance: textfield; - cursor: text; - margin: 2px 4px; - border: 2px solid; - -moz-border-top-colors: ThreeDShadow ThreeDDarkShadow; - -moz-border-right-colors: ThreeDHighlight ThreeDLightShadow; - -moz-border-bottom-colors: ThreeDHighlight ThreeDLightShadow; - -moz-border-left-colors: ThreeDShadow ThreeDDarkShadow; - padding: 2px 2px 3px; - padding-inline-start: 4px; - background-color: -moz-Field; - color: -moz-FieldText; -} - -.textbox-search-clear { - background-image: url(moz-icon://stock/gtk-clear?size=menu); - background-repeat: no-repeat; - width: 16px; - height: 16px; -} - -.textbox-search-icon { - background-image: url(moz-icon://stock/gtk-find?size=menu); - background-repeat: no-repeat; - width: 16px; - height: 16px; - display: block; -} - -.textbox-search-icon[searchbutton]:not([disabled]) , -.textbox-search-clear:not([disabled]) { - cursor: pointer; -} - -.item.client .item-twisty-container { - -moz-appearance: treetwistyopen; - margin-top: 3px; - margin-left: 2px; -} - -.item.client.closed .item-twisty-container { - -moz-appearance: treetwisty; -} diff --git a/application/basilisk/themes/osx/jar.mn b/application/basilisk/themes/osx/jar.mn index 27802843d..92d2ceedf 100644 --- a/application/basilisk/themes/osx/jar.mn +++ b/application/basilisk/themes/osx/jar.mn @@ -7,8 +7,9 @@ browser.jar: #include ../shared/jar.inc.mn skin/classic/browser/sanitizeDialog.css skin/classic/browser/aboutSessionRestore-window-icon.png +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css -* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css) +#endif skin/classic/browser/actionicon-tab.png skin/classic/browser/actionicon-tab@2x.png * skin/classic/browser/browser.css @@ -159,6 +160,7 @@ browser.jar: skin/classic/browser/tabbrowser/tab-stroke-start@2x.png (tabbrowser/tab-stroke-start@2x.png) skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png) skin/classic/browser/tabbrowser/tabDragIndicator@2x.png (tabbrowser/tabDragIndicator@2x.png) +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-16.png skin/classic/browser/sync-32.png skin/classic/browser/sync-bg.png @@ -179,6 +181,8 @@ browser.jar: skin/classic/browser/syncProgress-toolbar@2x.png skin/classic/browser/syncProgress-toolbar-inverted.png skin/classic/browser/syncProgress-toolbar-inverted@2x.png + skin/classic/browser/syncProgress.css +#endif skin/classic/browser/Toolbar-background-noise.png (Toolbar-background-noise.png) skin/classic/browser/lion/toolbarbutton-dropmarker.png (toolbarbutton-dropmarker-lion.png) skin/classic/browser/toolbarbutton-dropmarker@2x.png (toolbarbutton-dropmarker-lion@2x.png) @@ -195,8 +199,10 @@ browser.jar: skin/classic/browser/yosemite/menuPanel-help@2x.png (menuPanel-help-yosemite@2x.png) skin/classic/browser/yosemite/reload-stop-go.png (reload-stop-go-yosemite.png) skin/classic/browser/yosemite/reload-stop-go@2x.png (reload-stop-go-yosemite@2x.png) +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/yosemite/sync-horizontalbar.png (sync-horizontalbar-yosemite.png) skin/classic/browser/yosemite/sync-horizontalbar@2x.png (sync-horizontalbar-yosemite@2x.png) +#endif skin/classic/browser/yosemite/tab-selected-end-inactive.svg (tabbrowser/tab-selected-end-yosemite-inactive.svg) skin/classic/browser/yosemite/tab-selected-start-inactive.svg (tabbrowser/tab-selected-start-yosemite-inactive.svg) skin/classic/browser/yosemite/tab-active-middle-inactive.png (tabbrowser/tab-active-middle-yosemite-inactive.png) @@ -224,5 +230,7 @@ browser.jar: % override chrome://browser/skin/menuPanel-help@2x.png chrome://browser/skin/yosemite/menuPanel-help@2x.png os=Darwin osversion>=10.10 % override chrome://browser/skin/reload-stop-go.png chrome://browser/skin/yosemite/reload-stop-go.png os=Darwin osversion>=10.10 % override chrome://browser/skin/reload-stop-go@2x.png chrome://browser/skin/yosemite/reload-stop-go@2x.png os=Darwin osversion>=10.10 +#ifdef MOZ_SERVICES_SYNC % override chrome://browser/skin/sync-horizontalbar.png chrome://browser/skin/yosemite/sync-horizontalbar.png os=Darwin osversion>=10.10 % override chrome://browser/skin/sync-horizontalbar@2x.png chrome://browser/skin/yosemite/sync-horizontalbar@2x.png os=Darwin osversion>=10.10 +#endif diff --git a/application/basilisk/themes/osx/syncProgress.css b/application/basilisk/themes/osx/syncProgress.css new file mode 100644 index 000000000..d7aa59976 --- /dev/null +++ b/application/basilisk/themes/osx/syncProgress.css @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@import url(chrome://global/skin/inContentUI.css); + +:root { + height: 100%; + width: 100%; + padding: 0; +} + +body { + margin: 0; + padding: 0 2em; +} + +#floatingBox { + margin: 4em auto; + max-width: 40em; + min-width: 23em; + padding: 1em 1.5em; + position: relative; + text-align: center; +} + +#successLogo { + margin: 1em 2em; +} + +#loadingText { + margin: 2em 6em; +} + +#progressBar { + margin: 2em 10em; +} + +#uploadProgressBar{ + width: 100%; +} + +#bottomRow { + margin-top: 2em; + padding: 0; + text-align: end; +} diff --git a/application/basilisk/themes/osx/syncedtabs/sidebar.css b/application/basilisk/themes/osx/syncedtabs/sidebar.css deleted file mode 100644 index 4d1de766c..000000000 --- a/application/basilisk/themes/osx/syncedtabs/sidebar.css +++ /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/. */ - -%include ../../shared/syncedtabs/sidebar.inc.css - -/* These styles are intended to mimic XUL trees and the XUL search box. */ - -.content-container { - -moz-appearance: -moz-mac-source-list; -} - -.item { - color: -moz-DialogText; -} - -.item-title-container { - box-sizing: border-box; - align-items: center; - height: 24px; - font-size: 12px; -} - -.item.selected > .item-title-container { - color: HighlightText; - font-weight: bold; -} - -.item.selected > .item-title-container { - -moz-appearance: -moz-mac-source-list-selection; -} - -.item.selected:focus > .item-title-container { - -moz-appearance: -moz-mac-active-source-list-selection; -} - -.item.client .item-twisty-container { - min-width: 16px; - height: 16px; - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded"); -} - -@media not all and (-moz-mac-yosemite-theme) { - .item.client.selected .item-twisty-container { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded-inverted"); - } - - .item.client.selected.closed .item-twisty-container { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed-inverted"); - } - - .item.client.selected .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded-inverted"); - } - - .item.client.selected.closed .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed-inverted-rtl"); - } -} - -.item.client.closed .item-twisty-container { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed"); -} - -.item.client.selected:focus .item-twisty-container { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded-inverted"); -} - -.item.client.selected.closed:focus .item-twisty-container { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed-inverted"); -} - -.item.client .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded"); -} - -.item.client.closed .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed-rtl"); -} - -.item.client.selected:focus .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-expanded-inverted"); -} - -.item.client.selected.closed:focus .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/arrow-disclosure.svg#arrow-disclosure-collapsed-inverted-rtl"); -} - -@media (-moz-mac-yosemite-theme) { - .item.selected > .item-title-container { - color: -moz-dialogtext; - font-weight: 500; - } - - .item.selected:focus > .item-title-container { - color: #fff; - } -} - -.sidebar-search-container { - border-bottom: 1px solid #bdbdbd; -} - -.search-box { - -moz-appearance: searchfield; - padding: 1px; - font-size: 12px; - cursor: text; - margin: 4px 8px 10px; - border-width: 3px; - border-style: solid; - border-color: currentcolor; - border-image: none; - -moz-border-top-colors: transparent #888 #000; - -moz-border-right-colors: transparent #FFF #000; - -moz-border-bottom-colors: transparent #FFF #000; - -moz-border-left-colors: transparent #888 #000; - border-top-right-radius: 2px; - border-bottom-left-radius: 2px; - background-color: #FFF; - color: #000; - -moz-user-select: text; - text-shadow: none; -} - -.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-clear { - background-image: url(chrome://global/skin/icons/searchfield-cancel.svg); - background-repeat: no-repeat; - background-size: 11px 11px; - width: 11px; - height: 11px; -} - -.search-box.compact > .textbox-input-box > .textbox-search-icons > .textbox-search-icon { - display: none; -} - -.search-box[focused="true"] { - -moz-border-top-colors: -moz-mac-focusring -moz-mac-focusring #000000; - -moz-border-right-colors: -moz-mac-focusring -moz-mac-focusring #000000; - -moz-border-bottom-colors: -moz-mac-focusring -moz-mac-focusring #000000; - -moz-border-left-colors: -moz-mac-focusring -moz-mac-focusring #000000; -} - -.search-box.compact { - padding: 0px; - /* font size is in px because the XUL it was copied from uses px */ - font-size: 11px; -} - -.textbox-search-clear, -.textbox-search-icon { - margin-top: 1px; -} diff --git a/application/basilisk/themes/shared/browser.inc b/application/basilisk/themes/shared/browser.inc index 81caf94d6..6989f064a 100644 --- a/application/basilisk/themes/shared/browser.inc +++ b/application/basilisk/themes/shared/browser.inc @@ -2,7 +2,7 @@ % Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none. %define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button -%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #webide-button, #containers-panelmenu +%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #sync-tabs-button, #feed-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #webide-button, #containers-panelmenu %ifdef XP_MACOSX % Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen diff --git a/application/basilisk/themes/shared/customizableui/panelUI.inc.css b/application/basilisk/themes/shared/customizableui/panelUI.inc.css index ba36da995..5550ef295 100644 --- a/application/basilisk/themes/shared/customizableui/panelUI.inc.css +++ b/application/basilisk/themes/shared/customizableui/panelUI.inc.css @@ -61,8 +61,7 @@ height: 13px; } -#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge, -#PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge { +#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge { box-shadow: none; filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15)); } @@ -86,13 +85,7 @@ background: #D90000; } -#PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge { - height: 13px; - background: transparent url(chrome://browser/skin/warning.svg) no-repeat center; -} - -#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge:-moz-window-inactive, -#PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge:-moz-window-inactive { +#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge:-moz-window-inactive { filter: none; } @@ -381,9 +374,6 @@ toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"] > iframe { #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item, #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]), #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-status, -#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar, -#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-label, -#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-icon, #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > toolbarseparator, #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-customize, #PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-help:not([panel-multiview-anchor="true"]) { @@ -481,26 +471,6 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { margin: 0; } -#main-window[customizing] #PanelUI-footer-fxa { - display: none; -} - -#PanelUI-footer-fxa:not([fxastatus="signedin"]) > toolbarseparator, -#PanelUI-footer-fxa:not([fxastatus="signedin"]) > #PanelUI-fxa-icon, -#PanelUI-footer-fxa:not([fxaprofileimage]) > #PanelUI-fxa-status > #PanelUI-fxa-avatar { - display: none; -} - -#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status::after { - content: url(chrome://browser/skin/warning.svg); - filter: drop-shadow(0 1px 0 hsla(206,50%,10%,.15)); - width: 47px; - padding-top: 1px; - display: block; - text-align: center; - position: relative; - top: 25%; -} #PanelUI-update-status[update-status]::after { content: ""; @@ -523,40 +493,28 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { background-color: #D90000; } -#PanelUI-fxa-status { - display: flex; - flex: 1 1 0%; - width: 1px; -} - -#PanelUI-footer-inner, -#PanelUI-footer-fxa:not([hidden]) { +#PanelUI-footer-inner { display: flex; border-top: 1px solid var(--panel-separator-color); } -#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-inner, -#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-fxa { +#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-inner { position: relative; } -#PanelUI-footer-inner > toolbarseparator, -#PanelUI-footer-fxa > toolbarseparator { +#PanelUI-footer-inner > toolbarseparator { border: 0; border-left: 1px solid var(--panel-separator-color); margin: 7px 0 7px; -moz-appearance: none; } -#PanelUI-footer-inner:hover > toolbarseparator, -#PanelUI-footer-fxa:hover > toolbarseparator { +#PanelUI-footer-inner:hover > toolbarseparator { margin: 0; } #PanelUI-update-status, #PanelUI-help, -#PanelUI-fxa-label, -#PanelUI-fxa-icon, #PanelUI-customize, #PanelUI-quit { margin: 0; @@ -590,7 +548,6 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { } #PanelUI-update-status > .toolbarbutton-text, -#PanelUI-fxa-label > .toolbarbutton-text, #PanelUI-customize > .toolbarbutton-text { margin: 0; padding: 0 6px; @@ -598,37 +555,23 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { } #PanelUI-help > .toolbarbutton-text, -#PanelUI-quit > .toolbarbutton-text, -#PanelUI-fxa-avatar > .toolbarbutton-text { +#PanelUI-quit > .toolbarbutton-text { display: none; } #PanelUI-update-status > .toolbarbutton-icon, -#PanelUI-fxa-label > .toolbarbutton-icon, -#PanelUI-fxa-icon > .toolbarbutton-icon, #PanelUI-customize > .toolbarbutton-icon, #PanelUI-help > .toolbarbutton-icon, #PanelUI-quit > .toolbarbutton-icon { margin-inline-end: 0; } -#PanelUI-fxa-icon { - padding-inline-start: 15px; - padding-inline-end: 15px; -} - -#PanelUI-fxa-label, #PanelUI-customize { flex: 1; padding-inline-start: 15px; border-inline-start-style: none; } -#PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label, -#PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label { - padding-inline-start: 0px; -} - #PanelUI-update-status { width: calc(@menuPanelWidth@ + 30px); padding-inline-start: 15px; @@ -639,130 +582,6 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { list-style-image: url(chrome://branding/content/icon16.png); } -#PanelUI-fxa-label, -#PanelUI-fxa-icon { - list-style-image: url(chrome://browser/skin/sync-horizontalbar.png); -} - -#PanelUI-remotetabs { - --panel-ui-sync-illustration-height: 157.5px; -} - -.PanelUI-remotetabs-instruction-title, -.PanelUI-remotetabs-instruction-label, -#PanelUI-remotetabs-mobile-promo { - /* If you change the margin here, the min-height of the synced tabs panel - (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, etc) may - need adjusting (see bug 1248506) */ - margin: 15px; - text-align: center; - text-shadow: none; - max-width: 15em; - color: GrayText; -} - -.PanelUI-remotetabs-instruction-title { - font-size: 1.3em; -} - -/* The boxes with "instructions" get extra top and bottom padding for space - around the illustration and buttons */ -.PanelUI-remotetabs-instruction-box { - /* If you change the padding here, the min-height of the synced tabs panel - (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, etc) may - need adjusting (see bug 1248506) */ - padding-bottom: 30px; - padding-top: 15px; -} - -.PanelUI-remotetabs-prefs-button { - -moz-appearance: none; - background-color: #0096dd; - /* !important for the color as an OSX specific rule when a lightweight theme - is used for buttons in the toolbox overrides. See bug 1238531 for details */ - color: white !important; - border-radius: 2px; - /* If you change the margin or padding below, the min-height of the synced tabs - panel (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, - etc) may need adjusting (see bug 1248506) */ - margin-top: 10px; - margin-bottom: 10px; - padding: 8px; - text-shadow: none; - min-width: 200px; -} - -.PanelUI-remotetabs-prefs-button:hover, -.PanelUI-remotetabs-prefs-button:hover:active { - background-color: #018acb; -} - -.remotetabs-promo-link { - margin: 0; -} - -.PanelUI-remotetabs-notabsforclient-label { - color: GrayText; - /* This margin is to line this label up with the labels in toolbarbuttons. */ - margin-left: 28px; -} - -.fxaSyncIllustration { - height: var(--panel-ui-sync-illustration-height); - list-style-image: url(chrome://browser/skin/fxa/sync-illustration.svg); -} - -.PanelUI-remotetabs-prefs-button > .toolbarbutton-text { - /* !important to override ".cui-widget-panel toolbarbutton > .toolbarbutton-text" above. */ - text-align: center !important; - text-shadow: none; -} - -#PanelUI-remotetabs[mainview] { /* panel anchored to toolbar button might be too skinny */ - min-width: 19em; -} - -/* Work around bug 1224412 - these boxes will cause scrollbars to appear when - the panel is anchored to a toolbar button. -*/ -#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, -#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-reauthsync, -#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-nodevicespane, -#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-tabsdisabledpane { - min-height: calc(var(--panel-ui-sync-illustration-height) + - 20px + /* margin of .PanelUI-remotetabs-prefs-button */ - 16px + /* padding of .PanelUI-remotetabs-prefs-button */ - 30px + /* margin of .PanelUI-remotetabs-instruction-label */ - 30px + 15px + /* padding of .PanelUI-remotetabs-instruction-box */ - 11em); -} - -#PanelUI-remotetabs-tabslist > label[itemtype="client"] { - color: GrayText; -} - -/* Collapse the non-active vboxes in the remotetabs deck to use only the - height the active box needs */ -#PanelUI-remotetabs-deck:not([selectedIndex="1"]) > #PanelUI-remotetabs-tabsdisabledpane, -#PanelUI-remotetabs-deck:not([selectedIndex="2"]) > #PanelUI-remotetabs-fetching, -#PanelUI-remotetabs-deck:not([selectedIndex="3"]) > #PanelUI-remotetabs-nodevicespane { - visibility: collapse; -} - -#PanelUI-remotetabs-main[devices-status="single"] > #PanelUI-remotetabs-buttons { - display: none; -} - -#PanelUI-fxa-icon[syncstatus="active"]:not([disabled]) { - list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar.png); -} - -#PanelUI-footer-fxa[fxastatus="migrate-signup"] > #PanelUI-fxa-status > #PanelUI-fxa-label, -#PanelUI-footer-fxa[fxastatus="migrate-verify"] > #PanelUI-fxa-status > #PanelUI-fxa-label { - list-style-image: url(chrome://browser/skin/warning.svg); - -moz-image-region: auto; -} - #PanelUI-customize { list-style-image: url(chrome://browser/skin/menuPanel-customize.png); } @@ -780,46 +599,12 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { list-style-image: url(chrome://browser/skin/menuPanel-exit.png); } -#PanelUI-fxa-label, -#PanelUI-fxa-icon, #PanelUI-customize, #PanelUI-help, #PanelUI-quit { -moz-image-region: rect(0, 16px, 16px, 0); } -#PanelUI-footer-fxa[fxastatus="signedin"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon, -#PanelUI-footer-fxa[fxastatus="error"][fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon { - display: none; -} - -#PanelUI-footer-fxa[fxastatus="error"]:not([fxaprofileimage="set"]) > #PanelUI-fxa-status > #PanelUI-fxa-avatar { - display: none; -} - -#PanelUI-fxa-status[disabled], -#PanelUI-fxa-icon[disabled] { - pointer-events: none; -} - -#PanelUI-fxa-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background-repeat: no-repeat; - background-position: 0 0; - background-size: contain; - align-self: center; - margin: 0px 7px; - padding: 0px; - border: 0px none; - margin-inline-end: 0; -} - -#PanelUI-footer-fxa[fxaprofileimage="enabled"] > #PanelUI-fxa-status > #PanelUI-fxa-avatar { - list-style-image: url(chrome://browser/skin/fxa/default-avatar.svg); -} - #PanelUI-customize:hover, #PanelUI-help:not([disabled]):hover, #PanelUI-quit:not([disabled]):hover { @@ -837,16 +622,10 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { } #PanelUI-help[disabled], -#PanelUI-quit[disabled], -#PanelUI-fxa-icon[disabled], -#PanelUI-fxa-avatar[disabled], -#PanelUI-fxa-label[disabled] > .toolbarbutton-icon, -#PanelUI-fxa-status::after { +#PanelUI-quit[disabled] { opacity: 0.4; } -#PanelUI-fxa-status:not([disabled]):hover, -#PanelUI-fxa-icon:not([disabled]):hover, #PanelUI-help:not([disabled]):hover, #PanelUI-customize:hover, #PanelUI-quit:not([disabled]):hover { @@ -854,8 +633,6 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { background-color: var(--arrowpanel-dimmed); } -#PanelUI-fxa-status:not([disabled]):hover:active, -#PanelUI-fxa-icon:not([disabled]):hover:active, #PanelUI-help:not([disabled]):hover:active, #PanelUI-customize:hover:active, #PanelUI-quit:not([disabled]):hover:active { @@ -864,27 +641,6 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton { box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset; } -#PanelUI-fxa-status:not([disabled]):hover, -#PanelUI-fxa-status:not([disabled]):hover:active, -#PanelUI-fxa-icon:not([disabled]):hover, -#PanelUI-fxa-icon:not([disabled]):hover:active { - outline: none; -} - -#PanelUI-footer-fxa[fxastatus="error"] { - background-color: hsl(42,94%,88%); - border-top: 1px solid hsl(42,94%,70%); -} - -#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover { - background-color: hsl(42,94%,85%); -} - -#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active { - background-color: hsl(42,94%,82%); - box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset; -} - #PanelUI-update-status { color: black; } @@ -1150,19 +906,16 @@ menuitem.panel-subview-footer@menuStateActive@, color: GrayText; } -#PanelUI-remotetabs-tabslist > toolbarbutton, #PanelUI-historyItems > toolbarbutton { list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png"); } @media (min-resolution: 1.1dppx) { - #PanelUI-remotetabs-tabslist > toolbarbutton, #PanelUI-historyItems > toolbarbutton { list-style-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png"); } } -#PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon, #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon, #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon, #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon { @@ -1616,15 +1369,6 @@ menuitem[checked="true"].subviewbutton > .menu-iconic-left { list-style-image: url(chrome://branding/content/icon32.png); } - #PanelUI-fxa-label, - #PanelUI-fxa-icon { - list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png); - } - - #PanelUI-fxa-icon[syncstatus="active"]:not([disabled]) { - list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar@2x.png); - } - #PanelUI-customize { list-style-image: url(chrome://browser/skin/menuPanel-customize@2x.png); } @@ -1641,8 +1385,6 @@ menuitem[checked="true"].subviewbutton > .menu-iconic-left { list-style-image: url(chrome://browser/skin/menuPanel-exit@2x.png); } - #PanelUI-fxa-label, - #PanelUI-fxa-icon, #PanelUI-customize, #PanelUI-help, #PanelUI-quit { @@ -1650,8 +1392,6 @@ menuitem[checked="true"].subviewbutton > .menu-iconic-left { } #PanelUI-update-status > .toolbarbutton-icon, - #PanelUI-fxa-label > .toolbarbutton-icon, - #PanelUI-fxa-icon > .toolbarbutton-icon, #PanelUI-customize > .toolbarbutton-icon, #PanelUI-help > .toolbarbutton-icon, #PanelUI-quit > .toolbarbutton-icon { diff --git a/application/basilisk/themes/shared/menupanel.inc.css b/application/basilisk/themes/shared/menupanel.inc.css index 266e1c83e..da2f07e1e 100644 --- a/application/basilisk/themes/shared/menupanel.inc.css +++ b/application/basilisk/themes/shared/menupanel.inc.css @@ -48,10 +48,29 @@ toolbarpaletteitem[place="palette"] > #save-page-button { -moz-image-region: rect(0px, 352px, 32px, 320px); } +%ifdef MOZ_SERVICES_SYNC #sync-button[cui-areatype="menu-panel"], toolbarpaletteitem[place="palette"] > #sync-button { + -moz-image-region: rect(0px, 384px, 32px, 352px) +} + +#sync-button[cui-areatype="menu-panel"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-menuPanel.png"); + -moz-image-region: rect(0, 32px, 32px, 0); +} + +@media (min-resolution: 1.1dppx) { + #sync-button[cui-areatype="menu-panel"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-menuPanel@2x.png"); + -moz-image-region: rect(0, 64px, 64px, 0); + } +} + +#sync-tabs-button[cui-areatype="menu-panel"], +toolbarpaletteitem[place="palette"] > #sync-tabs-button { -moz-image-region: rect(0px, 1024px, 32px, 992px); } +%endif #containers-panelmenu[cui-areatype="menu-panel"], toolbarpaletteitem[place="palette"] > #containers-panelmenu { diff --git a/application/basilisk/themes/shared/syncedtabs/sidebar.inc.css b/application/basilisk/themes/shared/syncedtabs/sidebar.inc.css deleted file mode 100644 index 4e76a7fc5..000000000 --- a/application/basilisk/themes/shared/syncedtabs/sidebar.inc.css +++ /dev/null @@ -1,234 +0,0 @@ -% This Source Code Form is subject to the terms of the Mozilla Public -% License, v. 2.0. If a copy of the MPL was not distributed with this -% file, You can obtain one at http://mozilla.org/MPL/2.0/. - -/* These styles are intended to mimic XUL trees and the XUL search box. */ - -html { - height: 100%; -} - -body { - height: 100%; - margin: 0; - font: message-box; - color: #333333; - -moz-user-select: none; -} - -/* The content-container holds the non-scrollable header and the scrollable - content area. -*/ -.content-container { - display: flex; - flex-flow: column; - height: 100%; -} - -/* The content header is not scrollable */ -.content-header { - flex: 0 1 auto; -} - -/* The main content area is scrollable and fills the rest of the area */ -.content-scrollable { - flex: 1 1 auto; - overflow: auto; -} - -.emptyListInfo { - cursor: default; - padding: 3em 1em; - text-align: center; -} - -.list, -.item-tabs-list { - display: flex; - flex-flow: column; - flex-grow: 1; -} - -.item.client { - opacity: 1; - max-height: unset; - display: unset; -} - -.item.client.closed .item-tabs-list { - display: none; -} - -.item { - display: inline-block; - opacity: 1; - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - outline: none; - color: -moz-FieldText; -} - -.item.selected > .item-title-container { - background-color: -moz-cellhighlight; - color: -moz-cellhighlighttext; - font-weight: bold; -} - -.item.selected:focus > .item-title-container { - background-color: Highlight; - color: HighlightText; -} - -.client .item.tab > .item-title-container { - padding-inline-start: 35px; -} - -.item.tab > .item-title-container { - padding-inline-start: 20px; -} - -.item.client.device-image-desktop > .item-title-container > .item-icon-container { - background-image: url("chrome://browser/skin/sync-desktopIcon.svg#icon"); -} - -.item.client.device-image-desktop.selected:focus > .item-title-container > .item-icon-container { - background-image: url("chrome://browser/skin/sync-desktopIcon.svg#icon-inverted"); -} - -.item.client.device-image-mobile > .item-title-container > .item-icon-container { - background-image: url("chrome://browser/skin/sync-mobileIcon.svg#icon"); -} - -.item.client.device-image-mobile.selected:focus > .item-title-container > .item-icon-container { - background-image: url("chrome://browser/skin/sync-mobileIcon.svg#icon-inverted"); -} - -.item.tab > .item-title-container > .item-icon-container { - background-image: url("chrome://mozapps/skin/places/defaultFavicon.png"); -} - -@media (min-resolution: 1.1dppx) { -.item.tab > .item-title-container > .item-icon-container { - background-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png"); - } -} - -.item-icon-container { - min-width: 16px; - max-width: 16px; - min-height: 16px; - max-height: 16px; - margin-right: 5px; - margin-left: 5px; - background-size: 16px 16px; - background-size: contain; - background-repeat: no-repeat; - background-position: center; -} - -.item-title-container { - display: flex; - flex-flow: row; - overflow: hidden; - flex-grow: 1; - padding: 1px 0px 1px 0px; -} - -.item-title { - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - margin: 0px; - line-height: 1.3; - cursor: default; -} - -.item[hidden] { - opacity: 0; - max-height: 0; - transition: opacity 150ms ease-in-out, max-height 150ms ease-in-out 150ms; -} - -.item.empty .item-title-container { - color: #aeaeae; -} - -.client .item.empty > .item-title-container { - padding-inline-start: 35px; -} - -.text-input-box { - display: flex; - flex-flow: row nowrap; -} - -.textbox-input-box { - display: flex; - flex-direction: row; -} - -.tabsFilter { - flex: 1; - /* min-width of anything to override the implicit "-moz-min-content" value. - 0px is safe as the sidebar itself has a constrained size meaning we will - never actually hit this minimum - */ - min-width: 0px; -} - -.sync-state > p { - padding-inline-end: 10px; - padding-inline-start: 10px; - color: #888; -} - -.text-link { - color: rgb(0, 149, 221); - cursor: pointer; -} - -.text-link:hover { - text-decoration: underline; -} - -.text-link, -.text-link:focus { - margin: 0px; - padding: 0px; - border: 0px; -} - -.deck .sync-state { - display: none; - opacity: 0; - transition: opacity 1.5s; - border-top: 1px solid #bdbdbd; -} - -.deck .sync-state.tabs-container { - border-top: 0px; -} - -.deck .sync-state.selected { - display: unset; - opacity: 100; -} - -.sidebar-search-container.tabs-container:not(.selected) { - display: none; -} - -.textbox-search-clear:not([disabled]) { - cursor: default; -} - -.textbox-search-icons .textbox-search-clear, -.filtered .textbox-search-icons .textbox-search-icon { - display: none; -} - -.filtered .textbox-search-icons .textbox-search-clear { - display: block; -} diff --git a/application/basilisk/themes/shared/toolbarbuttons.inc.css b/application/basilisk/themes/shared/toolbarbuttons.inc.css index c043b8192..8992bfcb1 100644 --- a/application/basilisk/themes/shared/toolbarbuttons.inc.css +++ b/application/basilisk/themes/shared/toolbarbuttons.inc.css @@ -52,9 +52,32 @@ toolbar[brighttext] #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarke -moz-image-region: rect(0, 252px, 18px, 234px); } +%ifdef MOZ_SERVICES_SYNC #sync-button[cui-areatype="toolbar"] { + -moz-image-region: rect(0, 270px, 18px, 252px); +} + +#sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar.png"); + -moz-image-region: rect(0, 18px, 18px, 0); +} + +@media (-moz-os-version: windows-win7) { + #sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar-win7.png"); + -moz-image-region: rect(0, 18px, 18px, 0); + } +} + +toolbar[brighttext] #sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar-inverted.png"); + -moz-image-region: rect(0, 18px, 18px, 0); +} + +#sync-tabs-button[cui-areatype="toolbar"] { -moz-image-region: rect(0, 792px, 18px, 774px); } +%endif #containers-panelmenu[cui-areatype="toolbar"] { -moz-image-region: rect(0, 810px, 18px, 792px); @@ -226,9 +249,32 @@ toolbar[brighttext] #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarke -moz-image-region: rect(0, 504px, 36px, 468px); } +%ifdef MOZ_SERVICES_SYNC #sync-button[cui-areatype="toolbar"] { + -moz-image-region: rect(0, 540px, 36px, 504px); + } + + #sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar@2x.png"); + -moz-image-region: rect(0, 36px, 36px, 0); + } + + @media (-moz-os-version: windows-win7) { + #sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar-win7@2x.png"); + -moz-image-region: rect(0, 36px, 36px, 0); + } + } + + toolbar[brighttext] #sync-button[cui-areatype="toolbar"][status="active"] { + list-style-image: url("chrome://browser/skin/syncProgress-toolbar-inverted@2x.png"); + -moz-image-region: rect(0, 36px, 36px, 0); + } + + #sync-tabs-button[cui-areatype="toolbar"] { -moz-image-region: rect(0, 1584px, 36px, 1548px); } +%endif #containers-panelmenu[cui-areatype="toolbar"] { -moz-image-region: rect(0, 1620px, 36px, 1584px); diff --git a/application/basilisk/themes/windows/jar.mn b/application/basilisk/themes/windows/jar.mn index e8db7eed2..28c6c6465 100644 --- a/application/basilisk/themes/windows/jar.mn +++ b/application/basilisk/themes/windows/jar.mn @@ -7,8 +7,9 @@ browser.jar: #include ../shared/jar.inc.mn skin/classic/browser/sanitizeDialog.css skin/classic/browser/aboutSessionRestore-window-icon.png +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/aboutSyncTabs.css -* skin/classic/browser/syncedtabs/sidebar.css (syncedtabs/sidebar.css) +#endif skin/classic/browser/actionicon-tab.png skin/classic/browser/actionicon-tab@2x.png skin/classic/browser/actionicon-tab-win7.png @@ -141,6 +142,7 @@ browser.jar: skin/classic/browser/tabbrowser/tab-stroke-start.png (tabbrowser/tab-stroke-start.png) skin/classic/browser/tabbrowser/tab-stroke-start@2x.png (tabbrowser/tab-stroke-start@2x.png) skin/classic/browser/tabbrowser/tabDragIndicator.png (tabbrowser/tabDragIndicator.png) +#ifdef MOZ_SERVICES_SYNC skin/classic/browser/sync-16.png skin/classic/browser/sync-32.png skin/classic/browser/sync-128.png @@ -167,6 +169,8 @@ browser.jar: skin/classic/browser/syncProgress-toolbar-inverted@2x.png skin/classic/browser/syncProgress-toolbar-win7.png skin/classic/browser/syncProgress-toolbar-win7@2x.png + skin/classic/browser/syncProgress.css +#endif [extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar: % override chrome://browser/skin/page-livemarks.png chrome://browser/skin/feeds/feedIcon16.png @@ -182,12 +186,14 @@ browser.jar: % override chrome://browser/skin/privatebrowsing-mask-titlebar.png chrome://browser/skin/privatebrowsing-mask-titlebar-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/reload-stop-go.png chrome://browser/skin/reload-stop-go-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/reload-stop-go@2x.png chrome://browser/skin/reload-stop-go-win7@2x.png os=WINNT osversion<=6.1 +#ifdef MOZ_SERVICES_SYNC % override chrome://browser/skin/sync-horizontalbar.png chrome://browser/skin/sync-horizontalbar-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/sync-horizontalbar@2x.png chrome://browser/skin/sync-horizontalbar-win7@2x.png os=WINNT osversion<=6.1 % override chrome://browser/skin/syncProgress-horizontalbar.png chrome://browser/skin/syncProgress-horizontalbar-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/syncProgress-horizontalbar@2x.png chrome://browser/skin/syncProgress-horizontalbar-win7@2x.png os=WINNT osversion<=6.1 % override chrome://browser/skin/syncProgress-toolbar.png chrome://browser/skin/syncProgress-toolbar-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/syncProgress-toolbar@2x.png chrome://browser/skin/syncProgress-toolbar-win7@2x.png os=WINNT osversion<=6.1 +#endif % override chrome://browser/skin/toolbarbutton-dropdown-arrow.png chrome://browser/skin/toolbarbutton-dropdown-arrow-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/urlbar-history-dropmarker.png chrome://browser/skin/urlbar-history-dropmarker-win7.png os=WINNT osversion<=6.1 % override chrome://browser/skin/urlbar-history-dropmarker@2x.png chrome://browser/skin/urlbar-history-dropmarker-win7@2x.png os=WINNT osversion<=6.1 diff --git a/application/basilisk/themes/windows/syncProgress.css b/application/basilisk/themes/windows/syncProgress.css new file mode 100644 index 000000000..d7aa59976 --- /dev/null +++ b/application/basilisk/themes/windows/syncProgress.css @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@import url(chrome://global/skin/inContentUI.css); + +:root { + height: 100%; + width: 100%; + padding: 0; +} + +body { + margin: 0; + padding: 0 2em; +} + +#floatingBox { + margin: 4em auto; + max-width: 40em; + min-width: 23em; + padding: 1em 1.5em; + position: relative; + text-align: center; +} + +#successLogo { + margin: 1em 2em; +} + +#loadingText { + margin: 2em 6em; +} + +#progressBar { + margin: 2em 10em; +} + +#uploadProgressBar{ + width: 100%; +} + +#bottomRow { + margin-top: 2em; + padding: 0; + text-align: end; +} diff --git a/application/basilisk/themes/windows/syncedtabs/sidebar.css b/application/basilisk/themes/windows/syncedtabs/sidebar.css deleted file mode 100644 index 6473206bc..000000000 --- a/application/basilisk/themes/windows/syncedtabs/sidebar.css +++ /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/. */ - -%include ../../shared/syncedtabs/sidebar.inc.css - -/* These styles are intended to mimic XUL trees and the XUL search box. */ - -html { - background-color: #EEF3FA; -} - -.item { - padding-inline-end: 0; -} - -.item-title { - margin: 1px 0 0; -} - -.item-title { - margin-inline-end: 6px; -} - -.search-box { - -moz-appearance: textfield; - cursor: text; - margin: 2px 4px; - padding: 2px 2px 3px; - padding-inline-start: 4px; - color: -moz-FieldText; -} - -.textbox-search-icon { - width: 16px; - height: 16px; - background-image: url(chrome://global/skin/icons/Search-glass.png); - background-repeat: no-repeat; - display: block; -} - -.textbox-search-icon:-moz-locale-dir(rtl) { - transform: scaleX(-1); -} - -.textbox-search-icon[searchbutton]:not([disabled]) { - cursor: pointer; -} - -.textbox-search-clear { - width: 16px; - height: 16px; - background-image: url(chrome://global/skin/icons/Search-close.png); - background-repeat: no-repeat; -} - -.textbox-search-clear:not([disabled]) { - cursor: default; -} - -.textbox-search-icon:not([disabled]) { - cursor: text; -} - -.textbox-search-clear:not([disabled]):hover , -.textbox-search-icon:not([disabled]):hover { - background-position: -16px 0; -} - -.textbox-search-clear:not([disabled]):hover:active , -.textbox-search-icon:not([disabled]):hover:active { - background-position: -32px 0; -} - -.client .item.tab > .item-title-container { - padding-inline-start: 26px; -} -.item.tab > .item-title-container { - padding-inline-start: 14px; -} - -.item-icon-container { - min-width: 16px; - max-width: 16px; - min-height: 16px; - max-height: 16px; - margin-right: 5px; - background-size: 16px 16px; - background-repeat: no-repeat; - background-position: center; -} - -.item-twisty-container { - background-size: contain; - background-repeat: no-repeat; - background-position: center; - padding-top: 5px; - min-width: 9px; /* The image's width is 9 pixels */ - height: 9px; -} - -.item.client .item-twisty-container { - background-image: url("chrome://global/skin/tree/twisty.svg#open"); -} - -.item.client.closed .item-twisty-container { - background-image: url("chrome://global/skin/tree/twisty.svg#clsd"); -} - -.item.client .item-twisty-container:hover { - background-image: url("chrome://global/skin/tree/twisty.svg#open-hover"); -} - -.item.client.closed .item-twisty-container:hover { - background-image: url("chrome://global/skin/tree/twisty.svg#clsd-hover"); -} - -.item.client .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/twisty.svg#open-rtl"); -} - -.item.client.closed .item-twisty-container:dir(rtl) { - background-image: url("chrome://global/skin/tree/twisty.svg#clsd-rtl"); -} - -.item.client .item-twisty-container:hover:dir(rtl) { - background-image: url("chrome://global/skin/tree/twisty.svg#open-hover-rtl"); -} - -.item.client.closed .item-twisty-container:hover:dir(rtl) { - background-image: url("chrome://global/skin/tree/twisty.svg#clsd-hover-rtl"); -} diff --git a/application/palemoon/configure.in b/application/palemoon/configure.in index 96f63781e..eb02af3c0 100644 --- a/application/palemoon/configure.in +++ b/application/palemoon/configure.in @@ -36,7 +36,3 @@ MOZ_ARG_DISABLE_BOOL(sync, MOZ_SERVICES_SYNC=, MOZ_SERVICES_SYNC=1) -if test -z "$MOZ_SERVICES_SYNC"; then - MOZ_SERVICES_CLOUDSYNC= -fi - 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/Preferences.cpp b/modules/libpref/Preferences.cpp index c56cad98d..19487704f 100644 --- a/modules/libpref/Preferences.cpp +++ b/modules/libpref/Preferences.cpp @@ -1306,8 +1306,6 @@ static nsresult pref_InitInitialObjects() "winpref.js" #elif defined(XP_UNIX) "unix.js" -#elif defined(XP_BEOS) - "beos.js" #endif }; 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/netwerk/base/nsFileStreams.cpp b/netwerk/base/nsFileStreams.cpp index 6508b33b9..5a82dea1b 100644 --- a/netwerk/base/nsFileStreams.cpp +++ b/netwerk/base/nsFileStreams.cpp @@ -5,7 +5,7 @@ #include "ipc/IPCMessageUtils.h" -#if defined(XP_UNIX) || defined(XP_BEOS) +#if defined(XP_UNIX) #include <unistd.h> #elif defined(XP_WIN) #include <windows.h> @@ -97,14 +97,14 @@ nsFileStreamBase::SetEOF() if (mFD == nullptr) return NS_BASE_STREAM_CLOSED; -#if defined(XP_UNIX) || defined(XP_BEOS) +#if defined(XP_UNIX) // Some system calls require an EOF offset. int64_t offset; rv = Tell(&offset); if (NS_FAILED(rv)) return rv; #endif -#if defined(XP_UNIX) || defined(XP_BEOS) +#if defined(XP_UNIX) if (ftruncate(PR_FileDesc2NativeHandle(mFD), offset) != 0) { NS_ERROR("ftruncate failed"); return NS_ERROR_FAILURE; diff --git a/old-configure.in b/old-configure.in index 0ccc0540e..6a7bc701f 100644 --- a/old-configure.in +++ b/old-configure.in @@ -2246,7 +2246,6 @@ MOZ_DEVTOOLS= MOZ_PLACES=1 MOZ_SERVICES_HEALTHREPORT=1 MOZ_SERVICES_SYNC=1 -MOZ_SERVICES_CLOUDSYNC=1 MOZ_USERINFO=1 case "$target_os" in @@ -5006,12 +5005,6 @@ if test -n "$MOZ_SERVICES_SYNC"; then AC_DEFINE(MOZ_SERVICES_SYNC) fi -dnl Build Services/CloudSync if required -AC_SUBST(MOZ_SERVICES_CLOUDSYNC) -if test -n "$MOZ_SERVICES_CLOUDSYNC"; then - AC_DEFINE(MOZ_SERVICES_CLOUDSYNC) -fi - dnl ======================================================== if test "$MOZ_DEBUG"; then diff --git a/services/cloudsync/CloudSync.jsm b/services/cloudsync/CloudSync.jsm deleted file mode 100644 index 2c1057ea9..000000000 --- a/services/cloudsync/CloudSync.jsm +++ /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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["CloudSync"]; - -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Adapters", - "resource://gre/modules/CloudSyncAdapters.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Local", - "resource://gre/modules/CloudSyncLocal.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks", - "resource://gre/modules/CloudSyncBookmarks.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Tabs", - "resource://gre/modules/CloudSyncTabs.jsm"); - -var API_VERSION = 1; - -var _CloudSync = function () { -}; - -_CloudSync.prototype = { - _adapters: null, - - get adapters () { - if (!this._adapters) { - this._adapters = new Adapters(); - } - return this._adapters; - }, - - _bookmarks: null, - - get bookmarks () { - if (!this._bookmarks) { - this._bookmarks = new Bookmarks(); - } - return this._bookmarks; - }, - - _local: null, - - get local () { - if (!this._local) { - this._local = new Local(); - } - return this._local; - }, - - _tabs: null, - - get tabs () { - if (!this._tabs) { - this._tabs = new Tabs(); - } - return this._tabs; - }, - - get tabsReady () { - return this._tabs ? true: false; - }, - - get version () { - return API_VERSION; - }, -}; - -this.CloudSync = function CloudSync () { - return _cloudSyncInternal.instance; -}; - -Object.defineProperty(CloudSync, "ready", { - get: function () { - return _cloudSyncInternal.ready; - } -}); - -var _cloudSyncInternal = { - instance: null, - ready: false, -}; - -XPCOMUtils.defineLazyGetter(_cloudSyncInternal, "instance", function () { - _cloudSyncInternal.ready = true; - return new _CloudSync(); -}.bind(this)); diff --git a/services/cloudsync/CloudSyncAdapters.jsm b/services/cloudsync/CloudSyncAdapters.jsm deleted file mode 100644 index 16264a4f7..000000000 --- a/services/cloudsync/CloudSyncAdapters.jsm +++ /dev/null @@ -1,88 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Adapters"]; - -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/CloudSyncEventSource.jsm"); - -this.Adapters = function () { - let eventTypes = [ - "sync", - ]; - - let suspended = true; - - let suspend = function () { - if (!suspended) { - Services.obs.removeObserver(observer, "cloudsync:user-sync", false); - suspended = true; - } - }.bind(this); - - let resume = function () { - if (suspended) { - Services.obs.addObserver(observer, "cloudsync:user-sync", false); - suspended = false; - } - }.bind(this); - - let eventSource = new EventSource(eventTypes, suspend, resume); - let registeredAdapters = new Map(); - - function register (name, opts) { - opts = opts || {}; - registeredAdapters.set(name, opts); - } - - function unregister (name) { - if (!registeredAdapters.has(name)) { - throw new Error("adapter is not registered: " + name) - } - registeredAdapters.delete(name); - } - - function getAdapterNames () { - let result = []; - for (let name of registeredAdapters.keys()) { - result.push(name); - } - return result; - } - - function getAdapter (name) { - if (!registeredAdapters.has(name)) { - throw new Error("adapter is not registered: " + name) - } - return registeredAdapters.get(name); - } - - function countAdapters () { - return registeredAdapters.size; - } - - let observer = { - observe: function (subject, topic, data) { - switch (topic) { - case "cloudsync:user-sync": - eventSource.emit("sync"); - break; - } - } - }; - - this.addEventListener = eventSource.addEventListener; - this.removeEventListener = eventSource.removeEventListener; - this.register = register.bind(this); - this.get = getAdapter.bind(this); - this.unregister = unregister.bind(this); - this.__defineGetter__("names", getAdapterNames); - this.__defineGetter__("count", countAdapters); -}; - -Adapters.prototype = { - -}; diff --git a/services/cloudsync/CloudSyncBookmarks.jsm b/services/cloudsync/CloudSyncBookmarks.jsm deleted file mode 100644 index bb2e48d59..000000000 --- a/services/cloudsync/CloudSyncBookmarks.jsm +++ /dev/null @@ -1,795 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Bookmarks"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://gre/modules/PlacesUtils.jsm"); -Cu.import("resource:///modules/PlacesUIUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/CloudSyncPlacesWrapper.jsm"); -Cu.import("resource://gre/modules/CloudSyncEventSource.jsm"); -Cu.import("resource://gre/modules/CloudSyncBookmarksFolderCache.jsm"); - -const ITEM_TYPES = [ - "NULL", - "BOOKMARK", - "FOLDER", - "SEPARATOR", - "DYNAMIC_CONTAINER", // no longer used by Places, but this ID should not be used for future item types -]; - -const CS_UNKNOWN = 0x1; -const CS_FOLDER = 0x1 << 1; -const CS_SEPARATOR = 0x1 << 2; -const CS_QUERY = 0x1 << 3; -const CS_LIVEMARK = 0x1 << 4; -const CS_BOOKMARK = 0x1 << 5; - -const EXCLUDE_BACKUP_ANNO = "places/excludeFromBackup"; - -const DATA_VERSION = 1; - -function asyncCallback(ctx, func, args) { - function invoke() { - func.apply(ctx, args); - } - CommonUtils.nextTick(invoke); -} - -var Record = function (params) { - this.id = params.guid; - this.parent = params.parent || null; - this.index = params.position; - this.title = params.title; - this.dateAdded = Math.floor(params.dateAdded/1000); - this.lastModified = Math.floor(params.lastModified/1000); - this.uri = params.url; - - let annos = params.annos || {}; - Object.defineProperty(this, "annos", { - get: function () { - return annos; - }, - enumerable: false - }); - - switch (params.type) { - case PlacesUtils.bookmarks.TYPE_FOLDER: - if (PlacesUtils.LMANNO_FEEDURI in annos) { - this.type = CS_LIVEMARK; - this.feed = annos[PlacesUtils.LMANNO_FEEDURI]; - this.site = annos[PlacesUtils.LMANNO_SITEURI]; - } else { - this.type = CS_FOLDER; - } - break; - case PlacesUtils.bookmarks.TYPE_BOOKMARK: - if (this.uri.startsWith("place:")) { - this.type = CS_QUERY; - } else { - this.type = CS_BOOKMARK; - } - break; - case PlacesUtils.bookmarks.TYPE_SEPARATOR: - this.type = CS_SEPARATOR; - break; - default: - this.type = CS_UNKNOWN; - } -}; - -Record.prototype = { - version: DATA_VERSION, -}; - -var Bookmarks = function () { - let createRootFolder = function (name) { - let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; - let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; - - let deferred = Promise.defer(); - let placesRootId = PlacesUtils.placesRootId; - let rootFolderId; - let rootShortcutId; - - function createAdapterShortcut(result) { - rootFolderId = result; - let uri = "place:folder=" + rootFolderId; - return PlacesWrapper.insertBookmark(PlacesUIUtils.allBookmarksFolderId, uri, - PlacesUtils.bookmarks.DEFAULT_INDEX, name); - } - - function setRootFolderCloudSyncAnnotation(result) { - rootShortcutId = result; - return PlacesWrapper.setItemAnnotation(rootFolderId, ROOT_FOLDER_ANNO, - 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); - } - - function setRootShortcutCloudSyncAnnotation() { - return PlacesWrapper.setItemAnnotation(rootShortcutId, ROOT_SHORTCUT_ANNO, - 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); - } - - function setRootFolderExcludeFromBackupAnnotation() { - return PlacesWrapper.setItemAnnotation(rootFolderId, EXCLUDE_BACKUP_ANNO, - 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); - } - - function finish() { - deferred.resolve(rootFolderId); - } - - Promise.resolve(PlacesUtils.bookmarks.createFolder(placesRootId, name, PlacesUtils.bookmarks.DEFAULT_INDEX)) - .then(createAdapterShortcut) - .then(setRootFolderCloudSyncAnnotation) - .then(setRootShortcutCloudSyncAnnotation) - .then(setRootFolderExcludeFromBackupAnnotation) - .then(finish, deferred.reject); - - return deferred.promise; - }; - - let getRootFolder = function (name) { - let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; - let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; - let deferred = Promise.defer(); - - function checkRootFolder(folderIds) { - if (!folderIds.length) { - return createRootFolder(name); - } - return Promise.resolve(folderIds[0]); - } - - function createFolderObject(folderId) { - return new RootFolder(folderId, name); - } - - PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO) - .then(checkRootFolder, deferred.reject) - .then(createFolderObject) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }; - - let deleteRootFolder = function (name) { - let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name; - let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name; - - let deferred = Promise.defer(); - let placesRootId = PlacesUtils.placesRootId; - - function getRootShortcutId() { - return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_SHORTCUT_ANNO); - } - - function deleteShortcut(shortcutIds) { - if (!shortcutIds.length) { - return Promise.resolve(); - } - return PlacesWrapper.removeItem(shortcutIds[0]); - } - - function getRootFolderId() { - return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO); - } - - function deleteFolder(folderIds) { - let deleteFolderDeferred = Promise.defer(); - - if (!folderIds.length) { - return Promise.resolve(); - } - - let rootFolderId = folderIds[0]; - PlacesWrapper.removeFolderChildren(rootFolderId).then( - function () { - return PlacesWrapper.removeItem(rootFolderId); - } - ).then(deleteFolderDeferred.resolve, deleteFolderDeferred.reject); - - return deleteFolderDeferred.promise; - } - - getRootShortcutId().then(deleteShortcut) - .then(getRootFolderId) - .then(deleteFolder) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }; - - /* PUBLIC API */ - this.getRootFolder = getRootFolder.bind(this); - this.deleteRootFolder = deleteRootFolder.bind(this); - -}; - -this.Bookmarks = Bookmarks; - -var RootFolder = function (rootId, rootName) { - let suspended = true; - let ignoreAll = false; - - let suspend = function () { - if (!suspended) { - PlacesUtils.bookmarks.removeObserver(observer); - suspended = true; - } - }.bind(this); - - let resume = function () { - if (suspended) { - PlacesUtils.bookmarks.addObserver(observer, false); - suspended = false; - } - }.bind(this); - - let eventTypes = [ - "add", - "remove", - "change", - "move", - ]; - - let eventSource = new EventSource(eventTypes, suspend, resume); - - let folderCache = new FolderCache; - folderCache.insert(rootId, null); - - let getCachedFolderIds = function (cache, roots) { - let nodes = [...roots]; - let results = []; - - while (nodes.length) { - let node = nodes.shift(); - results.push(node); - let children = cache.getChildren(node); - nodes = nodes.concat([...children]); - } - return results; - }; - - let getLocalItems = function () { - let deferred = Promise.defer(); - - let folders = getCachedFolderIds(folderCache, folderCache.getChildren(rootId)); - - function getFolders(ids) { - let types = [ - PlacesUtils.bookmarks.TYPE_FOLDER, - ]; - return PlacesWrapper.getItemsById(ids, types); - } - - function getContents(parents) { - parents.push(rootId); - let types = [ - PlacesUtils.bookmarks.TYPE_BOOKMARK, - PlacesUtils.bookmarks.TYPE_SEPARATOR, - ]; - return PlacesWrapper.getItemsByParentId(parents, types) - } - - function getParentGuids(results) { - results = Array.prototype.concat.apply([], results); - let promises = []; - results.map(function (result) { - let promise = PlacesWrapper.localIdToGuid(result.parent).then( - function (guidResult) { - result.parent = guidResult; - return Promise.resolve(result); - }, - Promise.reject.bind(Promise) - ); - promises.push(promise); - }); - return Promise.all(promises); - } - - function getAnnos(results) { - results = Array.prototype.concat.apply([], results); - let promises = []; - results.map(function (result) { - let promise = PlacesWrapper.getItemAnnotationsForLocalId(result.id).then( - function (annos) { - result.annos = annos; - return Promise.resolve(result); - }, - Promise.reject.bind(Promise) - ); - promises.push(promise); - }); - return Promise.all(promises); - } - - let promises = [ - getFolders(folders), - getContents(folders), - ]; - - Promise.all(promises) - .then(getParentGuids) - .then(getAnnos) - .then(function (results) { - results = results.map((result) => new Record(result)); - deferred.resolve(results); - }, - deferred.reject); - - return deferred.promise; - }; - - let getLocalItemsById = function (guids) { - let deferred = Promise.defer(); - - let types = [ - PlacesUtils.bookmarks.TYPE_BOOKMARK, - PlacesUtils.bookmarks.TYPE_FOLDER, - PlacesUtils.bookmarks.TYPE_SEPARATOR, - PlacesUtils.bookmarks.TYPE_DYNAMIC_CONTAINER, - ]; - - function getParentGuids(results) { - let promises = []; - results.map(function (result) { - let promise = PlacesWrapper.localIdToGuid(result.parent).then( - function (guidResult) { - result.parent = guidResult; - return Promise.resolve(result); - }, - Promise.reject.bind(Promise) - ); - promises.push(promise); - }); - return Promise.all(promises); - } - - PlacesWrapper.getItemsByGuid(guids, types) - .then(getParentGuids) - .then(function (results) { - results = results.map((result) => new Record(result)); - deferred.resolve(results); - }, - deferred.reject); - - return deferred.promise; - }; - - let _createItem = function (item) { - let deferred = Promise.defer(); - - function getFolderId() { - if (item.parent) { - return PlacesWrapper.guidToLocalId(item.parent); - } - return Promise.resolve(rootId); - } - - function create(folderId) { - let deferred = Promise.defer(); - - if (!folderId) { - folderId = rootId; - } - let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX; - - function complete(localId) { - folderCache.insert(localId, folderId); - deferred.resolve(localId); - } - - switch (item.type) { - case CS_BOOKMARK: - case CS_QUERY: - PlacesWrapper.insertBookmark(folderId, item.uri, index, item.title, item.id) - .then(complete, deferred.reject); - break; - case CS_FOLDER: - PlacesWrapper.createFolder(folderId, item.title, index, item.id) - .then(complete, deferred.reject); - break; - case CS_SEPARATOR: - PlacesWrapper.insertSeparator(folderId, index, item.id) - .then(complete, deferred.reject); - break; - case CS_LIVEMARK: - let livemark = { - title: item.title, - parentId: folderId, - index: item.index, - feedURI: item.feed, - siteURI: item.site, - guid: item.id, - }; - PlacesUtils.livemarks.addLivemark(livemark) - .then(complete, deferred.reject); - break; - default: - deferred.reject("invalid item type: " + item.type); - } - - return deferred.promise; - } - - getFolderId().then(create) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }; - - let _deleteItem = function (item) { - let deferred = Promise.defer(); - - PlacesWrapper.guidToLocalId(item.id).then( - function (localId) { - folderCache.remove(localId); - return PlacesWrapper.removeItem(localId); - } - ).then(deferred.resolve, deferred.reject); - - return deferred.promise; - }; - - let _updateItem = function (item) { - let deferred = Promise.defer(); - - PlacesWrapper.guidToLocalId(item.id).then( - function (localId) { - let promises = []; - - if (item.hasOwnProperty("dateAdded")) { - promises.push(PlacesWrapper.setItemDateAdded(localId, item.dateAdded)); - } - - if (item.hasOwnProperty("lastModified")) { - promises.push(PlacesWrapper.setItemLastModified(localId, item.lastModified)); - } - - if ((CS_BOOKMARK | CS_FOLDER) & item.type && item.hasOwnProperty("title")) { - promises.push(PlacesWrapper.setItemTitle(localId, item.title)); - } - - if (CS_BOOKMARK & item.type && item.hasOwnProperty("uri")) { - promises.push(PlacesWrapper.changeBookmarkURI(localId, item.uri)); - } - - if (item.hasOwnProperty("parent")) { - let deferred = Promise.defer(); - PlacesWrapper.guidToLocalId(item.parent) - .then( - function (parent) { - let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX; - if (CS_FOLDER & item.type) { - folderCache.setParent(localId, parent); - } - return PlacesWrapper.moveItem(localId, parent, index); - } - ) - .then(deferred.resolve, deferred.reject); - promises.push(deferred.promise); - } - - if (item.hasOwnProperty("index") && !item.hasOwnProperty("parent")) { - promises.push(Task.spawn(function* () { - let localItem = (yield getLocalItemsById([item.id]))[0]; - let parent = yield PlacesWrapper.guidToLocalId(localItem.parent); - let index = item.index; - if (CS_FOLDER & item.type) { - folderCache.setParent(localId, parent); - } - yield PlacesWrapper.moveItem(localId, parent, index); - })); - } - - Promise.all(promises) - .then(deferred.resolve, deferred.reject); - } - ); - - return deferred.promise; - }; - - let mergeRemoteItems = function (items) { - ignoreAll = true; - let deferred = Promise.defer(); - - let newFolders = {}; - let newItems = []; - let updatedItems = []; - let deletedItems = []; - - let sortItems = function () { - let promises = []; - - let exists = function (item) { - let existsDeferred = Promise.defer(); - if (!item.id) { - Object.defineProperty(item, "__exists__", { - value: false, - enumerable: false - }); - existsDeferred.resolve(item); - } else { - PlacesWrapper.guidToLocalId(item.id).then( - function (localId) { - Object.defineProperty(item, "__exists__", { - value: localId ? true : false, - enumerable: false - }); - existsDeferred.resolve(item); - }, - existsDeferred.reject - ); - } - return existsDeferred.promise; - } - - let handleSortedItem = function (item) { - if (!item.__exists__ && !item.deleted) { - if (CS_FOLDER == item.type) { - newFolders[item.id] = item; - item._children = []; - } else { - newItems.push(item); - } - } else if (item.__exists__ && item.deleted) { - deletedItems.push(item); - } else if (item.__exists__) { - updatedItems.push(item); - } - } - - for (let item of items) { - if (!item || 'object' !== typeof(item)) { - continue; - } - - let promise = exists(item).then(handleSortedItem, Promise.reject.bind(Promise)); - promises.push(promise); - } - - return Promise.all(promises); - } - - let processNewFolders = function () { - let newFolderGuids = Object.keys(newFolders); - let newFolderRoots = []; - - for (let guid of newFolderGuids) { - let item = newFolders[guid]; - if (item.parent && newFolderGuids.indexOf(item.parent) >= 0) { - let parent = newFolders[item.parent]; - parent._children.push(item.id); - } else { - newFolderRoots.push(guid); - } - }; - - let promises = []; - for (let guid of newFolderRoots) { - let root = newFolders[guid]; - let promise = Promise.resolve(); - promise = promise.then( - function () { - return _createItem(root); - }, - Promise.reject.bind(Promise) - ); - let items = [].concat(root._children); - - while (items.length) { - let item = newFolders[items.shift()]; - items = items.concat(item._children); - promise = promise.then( - function () { - return _createItem(item); - }, - Promise.reject.bind(Promise) - ); - } - promises.push(promise); - } - - return Promise.all(promises); - } - - let processItems = function () { - let promises = []; - - for (let item of newItems) { - promises.push(_createItem(item)); - } - - for (let item of updatedItems) { - promises.push(_updateItem(item)); - } - - for (let item of deletedItems) { - _deleteItem(item); - } - - return Promise.all(promises); - } - - sortItems().then(processNewFolders) - .then(processItems) - .then(function () { - ignoreAll = false; - deferred.resolve(items); - }, - function (err) { - ignoreAll = false; - deferred.reject(err); - }); - - return deferred.promise; - }; - - let ignore = function (id, parent) { - if (ignoreAll) { - return true; - } - - if (rootId == parent || folderCache.has(parent)) { - return false; - } - - return true; - }; - - let handleItemAdded = function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) { - let deferred = Promise.defer(); - - if (PlacesUtils.bookmarks.TYPE_FOLDER == type) { - folderCache.insert(id, parent); - } - - eventSource.emit("add", guid); - deferred.resolve(); - - return deferred.promise; - }; - - let handleItemRemoved = function (id, parent, index, type, uri, guid, parentGuid) { - let deferred = Promise.defer(); - - if (PlacesUtils.bookmarks.TYPE_FOLDER == type) { - folderCache.remove(id); - } - - eventSource.emit("remove", guid); - deferred.resolve(); - - return deferred.promise; - }; - - let handleItemChanged = function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) { - let deferred = Promise.defer(); - - eventSource.emit('change', guid); - deferred.resolve(); - - return deferred.promise; - }; - - let handleItemMoved = function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) { - let deferred = Promise.defer(); - - function complete() { - eventSource.emit('move', guid); - deferred.resolve(); - } - - if (PlacesUtils.bookmarks.TYPE_FOLDER != type) { - complete(); - return deferred.promise; - } - - if (folderCache.has(oldParent) && folderCache.has(newParent)) { - // Folder move inside cloudSync root, so just update parents/children. - folderCache.setParent(id, newParent); - complete(); - } else if (!folderCache.has(oldParent)) { - // Folder moved in from ouside cloudSync root. - PlacesWrapper.updateCachedFolderIds(folderCache, newParent) - .then(complete, complete); - } else if (!folderCache.has(newParent)) { - // Folder moved out from inside cloudSync root. - PlacesWrapper.updateCachedFolderIds(folderCache, oldParent) - .then(complete, complete); - } - - return deferred.promise; - }; - - let observer = { - onBeginBatchUpdate: function () { - }, - - onEndBatchUpdate: function () { - }, - - onItemAdded: function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) { - if (ignore(id, parent)) { - return; - } - - asyncCallback(this, handleItemAdded, Array.prototype.slice.call(arguments)); - }, - - onItemRemoved: function (id, parent, index, type, uri, guid, parentGuid) { - if (ignore(id, parent)) { - return; - } - - asyncCallback(this, handleItemRemoved, Array.prototype.slice.call(arguments)); - }, - - onItemChanged: function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) { - if (ignore(id, parent)) { - return; - } - - asyncCallback(this, handleItemChanged, Array.prototype.slice.call(arguments)); - }, - - onItemMoved: function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) { - if (ignore(id, oldParent) && ignore(id, newParent)) { - return; - } - - asyncCallback(this, handleItemMoved, Array.prototype.slice.call(arguments)); - } - }; - - /* PUBLIC API */ - this.addEventListener = eventSource.addEventListener; - this.removeEventListener = eventSource.removeEventListener; - this.getLocalItems = getLocalItems.bind(this); - this.getLocalItemsById = getLocalItemsById.bind(this); - this.mergeRemoteItems = mergeRemoteItems.bind(this); - - let rootGuid = null; // resolved before becoming ready (below) - this.__defineGetter__("id", function () { - return rootGuid; - }); - this.__defineGetter__("name", function () { - return rootName; - }); - - let deferred = Promise.defer(); - let getGuidForRootFolder = function () { - return PlacesWrapper.localIdToGuid(rootId); - } - PlacesWrapper.updateCachedFolderIds(folderCache, rootId) - .then(getGuidForRootFolder, getGuidForRootFolder) - .then(function (guid) { - rootGuid = guid; - deferred.resolve(this); - }.bind(this), - deferred.reject); - return deferred.promise; -}; - -RootFolder.prototype = { - BOOKMARK: CS_BOOKMARK, - FOLDER: CS_FOLDER, - SEPARATOR: CS_SEPARATOR, - QUERY: CS_QUERY, - LIVEMARK: CS_LIVEMARK, -}; diff --git a/services/cloudsync/CloudSyncBookmarksFolderCache.jsm b/services/cloudsync/CloudSyncBookmarksFolderCache.jsm deleted file mode 100644 index f3c3fc8f2..000000000 --- a/services/cloudsync/CloudSyncBookmarksFolderCache.jsm +++ /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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["FolderCache"]; - -// Cache for bookmarks folder heirarchy. -var FolderCache = function () { - this.cache = new Map(); -} - -FolderCache.prototype = { - has: function (id) { - return this.cache.has(id); - }, - - insert: function (id, parentId) { - if (this.cache.has(id)) { - return; - } - - if (parentId && !(this.cache.has(parentId))) { - throw new Error("insert :: parentId not found in cache: " + parentId); - } - - this.cache.set(id, { - parent: parentId || null, - children: new Set(), - }); - - if (parentId) { - this.cache.get(parentId).children.add(id); - } - }, - - remove: function (id) { - if (!(this.cache.has(id))) { - throw new Error("remote :: id not found in cache: " + id); - } - - let parentId = this.cache.get(id).parent; - if (parentId) { - this.cache.get(parentId).children.delete(id); - } - - for (let child of this.cache.get(id).children) { - this.cache.get(child).parent = null; - } - - this.cache.delete(id); - }, - - setParent: function (id, parentId) { - if (!(this.cache.has(id))) { - throw new Error("setParent :: id not found in cache: " + id); - } - - if (parentId && !(this.cache.has(parentId))) { - throw new Error("setParent :: parentId not found in cache: " + parentId); - } - - let oldParent = this.cache.get(id).parent; - if (oldParent) { - this.cache.get(oldParent).children.delete(id); - } - this.cache.get(id).parent = parentId; - this.cache.get(parentId).children.add(id); - - return true; - }, - - getParent: function (id) { - if (this.cache.has(id)) { - return this.cache.get(id).parent; - } - - throw new Error("getParent :: id not found in cache: " + id); - }, - - getChildren: function (id) { - if (this.cache.has(id)) { - return this.cache.get(id).children; - } - - throw new Error("getChildren :: id not found in cache: " + id); - }, - - setChildren: function (id, children) { - for (let child of children) { - if (!this.cache.has(child)) { - this.insert(child, id); - } else { - this.setParent(child, id); - } - } - }, - - dump: function () { - dump("FolderCache: " + JSON.stringify(this.cache) + "\n"); - }, -}; - -this.FolderCache = FolderCache; diff --git a/services/cloudsync/CloudSyncEventSource.jsm b/services/cloudsync/CloudSyncEventSource.jsm deleted file mode 100644 index edb9c426b..000000000 --- a/services/cloudsync/CloudSyncEventSource.jsm +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["EventSource"]; - -Components.utils.import("resource://services-common/utils.js"); - -var EventSource = function (types, suspendFunc, resumeFunc) { - this.listeners = new Map(); - for (let type of types) { - this.listeners.set(type, new Set()); - } - - this.suspend = suspendFunc || function () {}; - this.resume = resumeFunc || function () {}; - - this.addEventListener = this.addEventListener.bind(this); - this.removeEventListener = this.removeEventListener.bind(this); -}; - -EventSource.prototype = { - addEventListener: function (type, listener) { - if (!this.listeners.has(type)) { - return; - } - this.listeners.get(type).add(listener); - this.resume(); - }, - - removeEventListener: function (type, listener) { - if (!this.listeners.has(type)) { - return; - } - this.listeners.get(type).delete(listener); - if (!this.hasListeners()) { - this.suspend(); - } - }, - - hasListeners: function () { - for (let l of this.listeners.values()) { - if (l.size > 0) { - return true; - } - } - return false; - }, - - emit: function (type, arg) { - if (!this.listeners.has(type)) { - return; - } - CommonUtils.nextTick( - function () { - for (let listener of this.listeners.get(type)) { - listener.call(undefined, arg); - } - }, - this - ); - }, -}; - -this.EventSource = EventSource; diff --git a/services/cloudsync/CloudSyncLocal.jsm b/services/cloudsync/CloudSyncLocal.jsm deleted file mode 100644 index 998c0c3c4..000000000 --- a/services/cloudsync/CloudSyncLocal.jsm +++ /dev/null @@ -1,87 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Local"]; - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://services-common/stringbundle.js"); -Cu.import("resource://services-common/utils.js"); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://gre/modules/Preferences.jsm"); - -function lazyStrings(name) { - let bundle = "chrome://weave/locale/services/" + name + ".properties"; - return () => new StringBundle(bundle); -} - -this.Str = {}; -XPCOMUtils.defineLazyGetter(Str, "errors", lazyStrings("errors")); -XPCOMUtils.defineLazyGetter(Str, "sync", lazyStrings("sync")); - -function makeGUID() { - return CommonUtils.encodeBase64URL(CryptoUtils.generateRandomBytes(9)); -} - -this.Local = function () { - let prefs = new Preferences("services.cloudsync."); - this.__defineGetter__("prefs", function () { - return prefs; - }); -}; - -Local.prototype = { - get id() { - let clientId = this.prefs.get("client.GUID", ""); - return clientId == "" ? this.id = makeGUID(): clientId; - }, - - set id(value) { - this.prefs.set("client.GUID", value); - }, - - get name() { - let clientName = this.prefs.get("client.name", ""); - - if (clientName != "") { - return clientName; - } - - // Generate a client name if we don't have a useful one yet - let env = Cc["@mozilla.org/process/environment;1"] - .getService(Ci.nsIEnvironment); - let user = env.get("USER") || env.get("USERNAME"); - let appName; - let brand = new StringBundle("chrome://branding/locale/brand.properties"); - let brandName = brand.get("brandShortName"); - - try { - let syncStrings = new StringBundle("chrome://browser/locale/sync.properties"); - appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]); - } catch (ex) { - } - - appName = appName || brandName; - - let system = - // 'device' is defined on unix systems - Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") || - // hostname of the system, usually assigned by the user or admin - Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") || - // fall back on ua info string - Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu; - - return this.name = Str.sync.get("client.name2", [user, appName, system]); - }, - - set name(value) { - this.prefs.set("client.name", value); - }, -}; - diff --git a/services/cloudsync/CloudSyncPlacesWrapper.jsm b/services/cloudsync/CloudSyncPlacesWrapper.jsm deleted file mode 100644 index dd8c5c52e..000000000 --- a/services/cloudsync/CloudSyncPlacesWrapper.jsm +++ /dev/null @@ -1,375 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["PlacesWrapper"]; - -const {interfaces: Ci, utils: Cu} = Components; -const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR; - -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/PlacesUtils.jsm"); -Cu.import("resource:///modules/PlacesUIUtils.jsm"); -Cu.import("resource://services-common/utils.js"); - -var PlacesQueries = function () { -} - -PlacesQueries.prototype = { - cachedStmts: {}, - - getQuery: function (queryString) { - if (queryString in this.cachedStmts) { - return this.cachedStmts[queryString]; - } - - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; - return this.cachedStmts[queryString] = db.createAsyncStatement(queryString); - } -}; - -var PlacesWrapper = function () { -} - -PlacesWrapper.prototype = { - placesQueries: new PlacesQueries(), - - guidToLocalId: function (guid) { - let deferred = Promise.defer(); - - let stmt = "SELECT id AS item_id " + - "FROM moz_bookmarks " + - "WHERE guid = :guid"; - let query = this.placesQueries.getQuery(stmt); - - function getLocalId(results) { - let result = results[0] && results[0]["item_id"]; - return Promise.resolve(result); - } - - query.params.guid = guid.toString(); - - this.asyncQuery(query, ["item_id"]) - .then(getLocalId, deferred.reject) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }, - - localIdToGuid: function (id) { - let deferred = Promise.defer(); - - let stmt = "SELECT guid " + - "FROM moz_bookmarks " + - "WHERE id = :item_id"; - let query = this.placesQueries.getQuery(stmt); - - function getGuid(results) { - let result = results[0] && results[0]["guid"]; - return Promise.resolve(result); - } - - query.params.item_id = id; - - this.asyncQuery(query, ["guid"]) - .then(getGuid, deferred.reject) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }, - - getItemsById: function (ids, types) { - let deferred = Promise.defer(); - let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " + - "FROM moz_bookmarks b " + - "LEFT JOIN moz_places p ON b.fk = p.id " + - "WHERE b.id in (" + ids.join(",") + ") AND b.type in (" + types.join(",") + ")"; - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; - let query = db.createAsyncStatement(stmt); - - this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"]) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }, - - getItemsByParentId: function (parents, types) { - let deferred = Promise.defer(); - let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " + - "FROM moz_bookmarks b " + - "LEFT JOIN moz_places p ON b.fk = p.id " + - "WHERE b.parent in (" + parents.join(",") + ") AND b.type in (" + types.join(",") + ")"; - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; - let query = db.createAsyncStatement(stmt); - - this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"]) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }, - - getItemsByGuid: function (guids, types) { - let deferred = Promise.defer(); - guids = guids.map(JSON.stringify); - let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " + - "FROM moz_bookmarks b " + - "LEFT JOIN moz_places p ON b.fk = p.id " + - "WHERE b.guid in (" + guids.join(",") + ") AND b.type in (" + types.join(",") + ")"; - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; - let query = db.createAsyncStatement(stmt); - - this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"]) - .then(deferred.resolve, deferred.reject); - - return deferred.promise; - }, - - updateCachedFolderIds: function (folderCache, folder) { - let deferred = Promise.defer(); - let stmt = "SELECT id, guid " + - "FROM moz_bookmarks " + - "WHERE parent = :parent_id AND type = :item_type"; - let query = this.placesQueries.getQuery(stmt); - - query.params.parent_id = folder; - query.params.item_type = PlacesUtils.bookmarks.TYPE_FOLDER; - - this.asyncQuery(query, ["id", "guid"]).then( - function (items) { - let previousIds = folderCache.getChildren(folder); - let currentIds = new Set(); - for (let item of items) { - currentIds.add(item.id); - } - let newIds = new Set(); - let missingIds = new Set(); - - for (let currentId of currentIds) { - if (!previousIds.has(currentId)) { - newIds.add(currentId); - } - } - for (let previousId of previousIds) { - if (!currentIds.has(previousId)) { - missingIds.add(previousId); - } - } - - folderCache.setChildren(folder, currentIds); - - let promises = []; - for (let newId of newIds) { - promises.push(this.updateCachedFolderIds(folderCache, newId)); - } - Promise.all(promises) - .then(deferred.resolve, deferred.reject); - - for (let missingId of missingIds) { - folderCache.remove(missingId); - } - }.bind(this) - ); - - return deferred.promise; - }, - - getLocalIdsWithAnnotation: function (anno) { - let deferred = Promise.defer(); - let stmt = "SELECT a.item_id " + - "FROM moz_anno_attributes n " + - "JOIN moz_items_annos a ON n.id = a.anno_attribute_id " + - "WHERE n.name = :anno_name"; - let query = this.placesQueries.getQuery(stmt); - - query.params.anno_name = anno.toString(); - - this.asyncQuery(query, ["item_id"]) - .then(function (items) { - let results = []; - for (let item of items) { - results.push(item.item_id); - } - deferred.resolve(results); - }, - deferred.reject); - - return deferred.promise; - }, - - getItemAnnotationsForLocalId: function (id) { - let deferred = Promise.defer(); - let stmt = "SELECT a.name, b.content " + - "FROM moz_anno_attributes a " + - "JOIN moz_items_annos b ON a.id = b.anno_attribute_id " + - "WHERE b.item_id = :item_id"; - let query = this.placesQueries.getQuery(stmt); - - query.params.item_id = id; - - this.asyncQuery(query, ["name", "content"]) - .then(function (results) { - let annos = {}; - for (let result of results) { - annos[result.name] = result.content; - } - deferred.resolve(annos); - }, - deferred.reject); - - return deferred.promise; - }, - - insertBookmark: function (parent, uri, index, title, guid) { - let parsedURI; - try { - parsedURI = CommonUtils.makeURI(uri) - } catch (e) { - return Promise.reject("unable to parse URI '" + uri + "': " + e); - } - - try { - let id = PlacesUtils.bookmarks.insertBookmark(parent, parsedURI, index, title, guid); - return Promise.resolve(id); - } catch (e) { - return Promise.reject("unable to insert bookmark " + JSON.stringify(arguments) + ": " + e); - } - }, - - setItemAnnotation: function (item, anno, value, flags, exp) { - try { - return Promise.resolve(PlacesUtils.annotations.setItemAnnotation(item, anno, value, flags, exp)); - } catch (e) { - return Promise.reject(e); - } - }, - - itemHasAnnotation: function (item, anno) { - try { - return Promise.resolve(PlacesUtils.annotations.itemHasAnnotation(item, anno)); - } catch (e) { - return Promise.reject(e); - } - }, - - createFolder: function (parent, name, index, guid) { - try { - return Promise.resolve(PlacesUtils.bookmarks.createFolder(parent, name, index, guid)); - } catch (e) { - return Promise.reject("unable to create folder ['" + name + "']: " + e); - } - }, - - removeFolderChildren: function (folder) { - try { - PlacesUtils.bookmarks.removeFolderChildren(folder); - return Promise.resolve(); - } catch (e) { - return Promise.reject(e); - } - }, - - insertSeparator: function (parent, index, guid) { - try { - return Promise.resolve(PlacesUtils.bookmarks.insertSeparator(parent, index, guid)); - } catch (e) { - return Promise.reject(e); - } - }, - - removeItem: function (item) { - try { - return Promise.resolve(PlacesUtils.bookmarks.removeItem(item)); - } catch (e) { - return Promise.reject(e); - } - }, - - setItemDateAdded: function (item, dateAdded) { - try { - return Promise.resolve(PlacesUtils.bookmarks.setItemDateAdded(item, dateAdded)); - } catch (e) { - return Promise.reject(e); - } - }, - - setItemLastModified: function (item, lastModified) { - try { - return Promise.resolve(PlacesUtils.bookmarks.setItemLastModified(item, lastModified)); - } catch (e) { - return Promise.reject(e); - } - }, - - setItemTitle: function (item, title) { - try { - return Promise.resolve(PlacesUtils.bookmarks.setItemTitle(item, title)); - } catch (e) { - return Promise.reject(e); - } - }, - - changeBookmarkURI: function (item, uri) { - try { - uri = CommonUtils.makeURI(uri); - return Promise.resolve(PlacesUtils.bookmarks.changeBookmarkURI(item, uri)); - } catch (e) { - return Promise.reject(e); - } - }, - - moveItem: function (item, parent, index) { - try { - return Promise.resolve(PlacesUtils.bookmarks.moveItem(item, parent, index)); - } catch (e) { - return Promise.reject(e); - } - }, - - setItemIndex: function (item, index) { - try { - return Promise.resolve(PlacesUtils.bookmarks.setItemIndex(item, index)); - } catch (e) { - return Promise.reject(e); - } - }, - - asyncQuery: function (query, names) { - let deferred = Promise.defer(); - let storageCallback = { - results: [], - handleResult: function (results) { - if (!names) { - return; - } - - let row; - while ((row = results.getNextRow()) != null) { - let item = {}; - for (let name of names) { - item[name] = row.getResultByName(name); - } - this.results.push(item); - } - }, - - handleError: function (error) { - deferred.reject(error); - }, - - handleCompletion: function (reason) { - if (REASON_ERROR == reason) { - return; - } - - deferred.resolve(this.results); - } - }; - - query.executeAsync(storageCallback); - return deferred.promise; - }, -}; - -this.PlacesWrapper = new PlacesWrapper(); diff --git a/services/cloudsync/CloudSyncTabs.jsm b/services/cloudsync/CloudSyncTabs.jsm deleted file mode 100644 index 7debc2678..000000000 --- a/services/cloudsync/CloudSyncTabs.jsm +++ /dev/null @@ -1,318 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Tabs"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/CloudSyncEventSource.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-common/observers.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "Session", "@mozilla.org/browser/sessionstore;1", "nsISessionStore"); - -const DATA_VERSION = 1; - -var ClientRecord = function (params) { - this.id = params.id; - this.name = params.name || "?"; - this.tabs = new Set(); -} - -ClientRecord.prototype = { - version: DATA_VERSION, - - update: function (params) { - if (this.id !== params.id) { - throw new Error("expected " + this.id + " to equal " + params.id); - } - - this.name = params.name; - } -}; - -var TabRecord = function (params) { - this.url = params.url || ""; - this.update(params); -}; - -TabRecord.prototype = { - version: DATA_VERSION, - - update: function (params) { - if (this.url && this.url !== params.url) { - throw new Error("expected " + this.url + " to equal " + params.url); - } - - if (params.lastUsed && params.lastUsed < this.lastUsed) { - return; - } - - this.title = params.title || ""; - this.icon = params.icon || ""; - this.lastUsed = params.lastUsed || 0; - }, -}; - -var TabCache = function () { - this.tabs = new Map(); - this.clients = new Map(); -}; - -TabCache.prototype = { - merge: function (client, tabs) { - if (!client || !client.id) { - return; - } - - if (!tabs) { - return; - } - - let cRecord; - if (this.clients.has(client.id)) { - try { - cRecord = this.clients.get(client.id); - } catch (e) { - throw new Error("unable to update client: " + e); - } - } else { - cRecord = new ClientRecord(client); - this.clients.set(cRecord.id, cRecord); - } - - for (let tab of tabs) { - if (!tab || 'object' !== typeof(tab)) { - continue; - } - - let tRecord; - if (this.tabs.has(tab.url)) { - tRecord = this.tabs.get(tab.url); - try { - tRecord.update(tab); - } catch (e) { - throw new Error("unable to update tab: " + e); - } - } else { - tRecord = new TabRecord(tab); - this.tabs.set(tRecord.url, tRecord); - } - - if (tab.deleted) { - cRecord.tabs.delete(tRecord); - } else { - cRecord.tabs.add(tRecord); - } - } - }, - - clear: function (client) { - if (client) { - this.clients.delete(client.id); - } else { - this.clients = new Map(); - this.tabs = new Map(); - } - }, - - get: function () { - let results = []; - for (let client of this.clients.values()) { - results.push(client); - } - return results; - }, - - isEmpty: function () { - return 0 == this.clients.size; - }, - -}; - -this.Tabs = function () { - let suspended = true; - - let topics = [ - "pageshow", - "TabOpen", - "TabClose", - "TabSelect", - ]; - - let update = function (event) { - if (event.originalTarget.linkedBrowser) { - if (PrivateBrowsingUtils.isBrowserPrivate(event.originalTarget.linkedBrowser) && - !PrivateBrowsingUtils.permanentPrivateBrowsing) { - return; - } - } - - eventSource.emit("change"); - }; - - let registerListenersForWindow = function (window) { - for (let topic of topics) { - window.addEventListener(topic, update, false); - } - window.addEventListener("unload", unregisterListeners, false); - }; - - let unregisterListenersForWindow = function (window) { - window.removeEventListener("unload", unregisterListeners, false); - for (let topic of topics) { - window.removeEventListener(topic, update, false); - } - }; - - let unregisterListeners = function (event) { - unregisterListenersForWindow(event.target); - }; - - let observer = { - observe: function (subject, topic, data) { - switch (topic) { - case "domwindowopened": - let onLoad = () => { - subject.removeEventListener("load", onLoad, false); - // Only register after the window is done loading to avoid unloads. - registerListenersForWindow(subject); - }; - - // Add tab listeners now that a window has opened. - subject.addEventListener("load", onLoad, false); - break; - } - } - }; - - let resume = function () { - if (suspended) { - Observers.add("domwindowopened", observer); - let wins = Services.wm.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - registerListenersForWindow(wins.getNext()); - } - } - }.bind(this); - - let suspend = function () { - if (!suspended) { - Observers.remove("domwindowopened", observer); - let wins = Services.wm.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - unregisterListenersForWindow(wins.getNext()); - } - } - }.bind(this); - - let eventTypes = [ - "change", - ]; - - let eventSource = new EventSource(eventTypes, suspend, resume); - - let tabCache = new TabCache(); - - let getWindowEnumerator = function () { - return Services.wm.getEnumerator("navigator:browser"); - }; - - let shouldSkipWindow = function (win) { - return win.closed || - PrivateBrowsingUtils.isWindowPrivate(win); - }; - - let getTabState = function (tab) { - return JSON.parse(Session.getTabState(tab)); - }; - - let getLocalTabs = function (filter) { - let deferred = Promise.defer(); - - filter = (undefined === filter) ? true : filter; - let filteredUrls = new RegExp("^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); // FIXME: should be a pref (B#1044304) - - let allTabs = []; - - let currentState = JSON.parse(Session.getBrowserState()); - currentState.windows.forEach(function (window) { - if (window.isPrivate) { - return; - } - window.tabs.forEach(function (tab) { - if (!tab.entries.length) { - return; - } - - // Get only the latest entry - // FIXME: support full history (B#1044306) - let entry = tab.entries[tab.index - 1]; - - if (!entry.url || filter && filteredUrls.test(entry.url)) { - return; - } - - allTabs.push(new TabRecord({ - title: entry.title, - url: entry.url, - icon: tab.attributes && tab.attributes.image || "", - lastUsed: tab.lastAccessed, - })); - }); - }); - - deferred.resolve(allTabs); - - return deferred.promise; - }; - - let mergeRemoteTabs = function (client, tabs) { - let deferred = Promise.defer(); - - deferred.resolve(tabCache.merge(client, tabs)); - Observers.notify("cloudsync:tabs:update"); - - return deferred.promise; - }; - - let clearRemoteTabs = function (client) { - let deferred = Promise.defer(); - - deferred.resolve(tabCache.clear(client)); - Observers.notify("cloudsync:tabs:update"); - - return deferred.promise; - }; - - let getRemoteTabs = function () { - let deferred = Promise.defer(); - - deferred.resolve(tabCache.get()); - - return deferred.promise; - }; - - let hasRemoteTabs = function () { - return !tabCache.isEmpty(); - }; - - /* PUBLIC API */ - this.addEventListener = eventSource.addEventListener; - this.removeEventListener = eventSource.removeEventListener; - this.getLocalTabs = getLocalTabs.bind(this); - this.mergeRemoteTabs = mergeRemoteTabs.bind(this); - this.clearRemoteTabs = clearRemoteTabs.bind(this); - this.getRemoteTabs = getRemoteTabs.bind(this); - this.hasRemoteTabs = hasRemoteTabs.bind(this); -}; - -Tabs.prototype = { -}; -this.Tabs = Tabs; diff --git a/services/cloudsync/docs/api.md b/services/cloudsync/docs/api.md deleted file mode 100644 index bca3193a4..000000000 --- a/services/cloudsync/docs/api.md +++ /dev/null @@ -1,234 +0,0 @@ -### Importing the JS module - -```` -Cu.import("resource://gre/modules/CloudSync.jsm"); - -let cloudSync = CloudSync(); -console.log(cloudSync); // Module is imported -```` - -### cloudSync.local - -#### id - -Local device ID. Is unique. - -```` -let localId = cloudSync.local.id; -```` - -#### name - -Local device name. - -```` -let localName = cloudSync.local.name; -```` - -### CloudSync.tabs - -#### addEventListener(type, callback) - -Add an event handler for Tabs events. Valid type is `change`. The callback receives no arguments. - -```` -function handleTabChange() { - // Tabs have changed. -} - -cloudSync.tabs.addEventListener("change", handleTabChange); -```` - -Change events are emitted when a tab is opened or closed, when a tab is selected, or when the page changes for an open tab. - -#### removeEventListener(type, callback) - -Remove an event handler. Pass the type and function that were passed to addEventListener. - -```` -cloudSync.tabs.removeEventListener("change", handleTabChange); -```` - -#### mergeRemoteTabs(client, tabs) - -Merge remote tabs from upstream by updating existing items, adding new tabs, and deleting existing tabs. Accepts a client and a list of tabs. Returns a promise. - -```` -let remoteClient = { - id: "fawe78", - name: "My Firefox client", -}; - -let remoteTabs = [ - {title: "Google", - url: "https://www.google.com", - icon: "https://www.google.com/favicon.ico", - lastUsed: 1400799296192}, - {title: "Reddit", - url: "http://www.reddit.com", - icon: "http://www.reddit.com/favicon.ico", - lastUsed: 1400799296192 - deleted: true}, -]; - -cloudSync.tabs.mergeRemoteTabs(client, tabs).then( - function() { - console.log("merge complete"); - } -); -```` - -#### getLocalTabs() - -Returns a promise. Passes a list of local tabs when complete. - -```` -cloudSync.tabs.getLocalTabs().then( - function(tabs) { - console.log(JSON.stringify(tabs)); - } -); -```` - -#### clearRemoteTabs(client) - -Clears all tabs for a remote client. - -```` -let remoteClient = { - id: "fawe78", - name: "My Firefox client", -}; - -cloudSync.tabs.clearRemoteTabs(client); -```` - -### cloudSync.bookmarks - -#### getRootFolder(name) - -Gets the named root folder, creating it if it doesn't exist. The root folder object has a number of methods (see the next section for details). - -```` -cloudSync.bookmarks.getRootFolder("My Bookmarks").then( - function(rootFolder) { - console.log(rootFolder); - } -); -```` - -### cloudSync.bookmarks.RootFolder - -This is a root folder object for bookmarks, created by `cloudSync.bookmarks.getRootFolder`. - -#### BOOKMARK - -Bookmark type. Used in results objects. - -```` -let bookmarkType = rootFolder.BOOKMARK; -```` - -#### FOLDER - -Folder type. Used in results objects. - -```` -let folderType = rootFolder.FOLDER; -```` - -#### SEPARATOR - -Separator type. Used in results objects. - -```` -let separatorType = rootFolder.SEPARATOR; -```` - -#### addEventListener(type, callback) - -Add an event handler for Tabs events. Valid types are `add, remove, change, move`. The callback receives an ID corresponding to the target item. - -```` -function handleBoookmarkEvent(id) { - console.log("event for id:", id); -} - -rootFolder.addEventListener("add", handleBookmarkEvent); -rootFolder.addEventListener("remove", handleBookmarkEvent); -rootFolder.addEventListener("change", handleBookmarkEvent); -rootFolder.addEventListener("move", handleBookmarkEvent); -```` - -#### removeEventListener(type, callback) - -Remove an event handler. Pass the type and function that were passed to addEventListener. - -```` -rootFolder.removeEventListener("add", handleBookmarkEvent); -rootFolder.removeEventListener("remove", handleBookmarkEvent); -rootFolder.removeEventListener("change", handleBookmarkEvent); -rootFolder.removeEventListener("move", handleBookmarkEvent); -```` - -#### getLocalItems() - -Callback receives a list of items on the local client. Results have the following form: - -```` -{ - id: "faw8e7f", // item guid - parent: "f7sydf87y", // parent folder guid - dateAdded: 1400799296192, // timestamp - lastModified: 1400799296192, // timestamp - uri: "https://www.google.ca", // null for FOLDER and SEPARATOR - title: "Google" - type: rootFolder.BOOKMARK, // should be one of rootFolder.{BOOKMARK, FOLDER, SEPARATOR}, - index: 0 // must be unique among folder items -} -```` - -```` -rootFolder.getLocalItems().then( - function(items) { - console.log(JSON.stringify(items)); - } -); -```` - -#### getLocalItemsById([...]) - -Callback receives a list of items, specified by ID, on the local client. Results have the same form as `getLocalItems()` above. - -```` -rootFolder.getLocalItemsById(["213r23f", "f22fy3f3"]).then( - function(items) { - console.log(JSON.stringify(items)); - } -); -```` - -#### mergeRemoteItems([...]) - -Merge remote items from upstream by updating existing items, adding new items, and deleting existing items. Folders are created first so that subsequent operations will succeed. Items have the same form as `getLocalItems()` above. Items that do not have an ID will have an ID generated for them. The results structure will contain this generated ID. - -```` -rootFolder.mergeRemoteItems([ - { - id: 'f2398f23', - type: rootFolder.FOLDER, - title: 'Folder 1', - parent: '9f8237f928' - }, - { - id: '9f8237f928', - type: rootFolder.FOLDER, - title: 'Folder 0', - } - ]).then( - function(items) { - console.log(items); // any generated IDs are filled in now - console.log("merge completed"); - } -); -````
\ No newline at end of file diff --git a/services/cloudsync/docs/architecture.rst b/services/cloudsync/docs/architecture.rst deleted file mode 100644 index a7a8aa7ba..000000000 --- a/services/cloudsync/docs/architecture.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _cloudsync_architecture: - -============ -Architecture -============ - -CloudSync offers functionality similar to Firefox Sync for data sources. Third-party addons -(sync adapters) consume local data, send and receive updates from the cloud, and merge remote data. - - -Files -===== - -CloudSync.jsm - Main module; Includes other modules and exposes them. - -CloudSyncAdapters.jsm - Provides an API for addons to register themselves. Will be used to - list available adapters and to notify adapters when sync operations - are requested manually by the user. - -CloudSyncBookmarks.jsm - Provides operations for interacting with bookmarks. - -CloudSyncBookmarksFolderCache.jsm - Implements a cache used to store folder hierarchy for filtering bookmark events. - -CloudSyncEventSource.jsm - Implements an event emitter. Used to provide addEventListener and removeEventListener - for tabs and bookmarks. - -CloudSyncLocal.jsm - Provides information about the local device, such as name and a unique id. - -CloudSyncPlacesWrapper.jsm - Wraps parts of the Places API in promises. Some methods are implemented to be asynchronous - where they are not in the places API. - -CloudSyncTabs.jsm - Provides operations for fetching local tabs and for populating the about:sync-tabs page. - - -Data Sources -============ - -CloudSync provides data for tabs and bookmarks. For tabs, local open pages can be enumerated and -remote tabs can be merged for displaying in about:sync-tabs. For bookmarks, updates are tracked -for a named folder (given by each adapter) and handled by callbacks registered using addEventListener, -and remote changes can be merged into the local database. - -Versioning -========== - -The API carries an integer version number (clouySync.version). Data records are versioned separately and individually. diff --git a/services/cloudsync/docs/dataformat.rst b/services/cloudsync/docs/dataformat.rst deleted file mode 100644 index 916581459..000000000 --- a/services/cloudsync/docs/dataformat.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. _cloudsync_dataformat: - -=========== -Data Format -=========== - -All fields are required unless noted otherwise. - -Bookmarks -========= - -Record ------- - -type: - record type; one of CloudSync.bookmarks.{BOOKMARK, FOLDER, SEPARATOR, QUERY, LIVEMARK} - -id: - GUID for this bookmark item - -parent: - id of parent folder - -index: - item index in parent folder; should be unique and contiguous, or they will be adjusted internally - -title: - bookmark or folder title; not meaningful for separators - -dateAdded: - timestamp (in milliseconds) for item added - -lastModified: - timestamp (in milliseconds) for last modification - -uri: - bookmark URI; not meaningful for folders or separators - -version: - data layout version - -Tabs -==== - -ClientRecord ------------- - -id: - GUID for this client - -name: - name for this client; not guaranteed to be unique - -tabs: - list of tabs open on this client; see TabRecord - -version: - data layout version - - -TabRecord ---------- - -title: - name for this tab - -url: - URL for this tab; only one tab for each URL is stored - -icon: - favicon URL for this tab; optional - -lastUsed: - timetamp (in milliseconds) for last use - -version: - data layout version diff --git a/services/cloudsync/docs/example.rst b/services/cloudsync/docs/example.rst deleted file mode 100644 index 33d0f0531..000000000 --- a/services/cloudsync/docs/example.rst +++ /dev/null @@ -1,132 +0,0 @@ -.. _cloudsync_example: - -======= -Example -======= - -.. code-block:: javascript - - Cu.import("resource://gre/modules/CloudSync.jsm"); - - let HelloWorld = { - onLoad: function() { - let cloudSync = CloudSync(); - console.log("CLOUDSYNC -- hello world", cloudSync.local.id, cloudSync.local.name, cloudSync.adapters); - cloudSync.adapters.register('helloworld', {}); - console.log("CLOUDSYNC -- " + JSON.stringify(cloudSync.adapters.getAdapterNames())); - - - cloudSync.tabs.addEventListener("change", function() { - console.log("tab change"); - cloudSync.tabs.getLocalTabs().then( - function(records) { - console.log(JSON.stringify(records)); - } - ); - }); - - cloudSync.tabs.getLocalTabs().then( - function(records) { - console.log(JSON.stringify(records)); - } - ); - - let remoteClient = { - id: "001", - name: "FakeClient", - }; - let remoteTabs1 = [ - {url:"https://www.google.ca",title:"Google",icon:"https://www.google.ca/favicon.ico",lastUsed:Date.now()}, - ]; - let remoteTabs2 = [ - {url:"https://www.google.ca",title:"Google Canada",icon:"https://www.google.ca/favicon.ico",lastUsed:Date.now()}, - {url:"http://www.reddit.com",title:"Reddit",icon:"http://www.reddit.com/favicon.ico",lastUsed:Date.now()}, - ]; - cloudSync.tabs.mergeRemoteTabs(remoteClient, remoteTabs1).then( - function() { - return cloudSync.tabs.mergeRemoteTabs(remoteClient, remoteTabs2); - } - ).then( - function() { - return cloudSync.tabs.getRemoteTabs(); - } - ).then( - function(tabs) { - console.log("remote tabs:", tabs); - } - ); - - cloudSync.bookmarks.getRootFolder("Hello World").then( - function(rootFolder) { - console.log(rootFolder.name, rootFolder.id); - rootFolder.addEventListener("add", function(guid) { - console.log("CLOUDSYNC -- bookmark item added: " + guid); - rootFolder.getLocalItemsById([guid]).then( - function(items) { - console.log("CLOUDSYNC -- items: " + JSON.stringify(items)); - } - ); - }); - rootFolder.addEventListener("remove", function(guid) { - console.log("CLOUDSYNC -- bookmark item removed: " + guid); - rootFolder.getLocalItemsById([guid]).then( - function(items) { - console.log("CLOUDSYNC -- items: " + JSON.stringify(items)); - } - ); - }); - rootFolder.addEventListener("change", function(guid) { - console.log("CLOUDSYNC -- bookmark item changed: " + guid); - rootFolder.getLocalItemsById([guid]).then( - function(items) { - console.log("CLOUDSYNC -- items: " + JSON.stringify(items)); - } - ); - }); - rootFolder.addEventListener("move", function(guid) { - console.log("CLOUDSYNC -- bookmark item moved: " + guid); - rootFolder.getLocalItemsById([guid]).then( - function(items) { - console.log("CLOUDSYNC -- items: " + JSON.stringify(items)); - } - ); - }); - - function logLocalItems() { - return rootFolder.getLocalItems().then( - function(items) { - console.log("CLOUDSYNC -- local items: " + JSON.stringify(items)); - } - ); - } - - let items = [ - {"id":"9fdoci2KOME6","type":rootFolder.FOLDER,"parent":rootFolder.id,"title":"My Bookmarks 1"}, - {"id":"1fdoci2KOME5","type":rootFolder.FOLDER,"parent":rootFolder.id,"title":"My Bookmarks 2"}, - {"id":"G_UL4ZhOyX8m","type":rootFolder.BOOKMARK,"parent":"1fdoci2KOME5","title":"reddit: the front page of the internet","uri":"http://www.reddit.com/"}, - ]; - function mergeSomeItems() { - return rootFolder.mergeRemoteItems(items); - } - - logLocalItems().then( - mergeSomeItems - ).then( - function(processedItems) { - console.log("!!!", processedItems); - console.log("merge complete"); - }, - function(error) { - console.log("merge failed:", error); - } - ).then( - logLocalItems - ); - } - ); - - - }, - }; - - window.addEventListener("load", function(e) { HelloWorld.onLoad(e); }, false); diff --git a/services/cloudsync/docs/index.rst b/services/cloudsync/docs/index.rst deleted file mode 100644 index d7951776d..000000000 --- a/services/cloudsync/docs/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _cloudsync: - -===================== -CloudSync -===================== - -CloudSync is a service that provides access to tabs and bookmarks data -for third-party sync addons. Addons can read local bookmarks and tabs. -Bookmarks and tab data can be merged from remote devices. - -Addons are responsible for maintaining an upstream representation, as -well as sending and receiving data over the network. - -.. toctree:: - :maxdepth: 1 - - architecture - dataformat - example diff --git a/services/cloudsync/moz.build b/services/cloudsync/moz.build deleted file mode 100644 index 93197c9fe..000000000 --- a/services/cloudsync/moz.build +++ /dev/null @@ -1,21 +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/. - -SPHINX_TREES['cloudsync'] = 'docs' - -EXTRA_JS_MODULES += [ - 'CloudSync.jsm', - 'CloudSyncAdapters.jsm', - 'CloudSyncBookmarks.jsm', - 'CloudSyncBookmarksFolderCache.jsm', - 'CloudSyncEventSource.jsm', - 'CloudSyncLocal.jsm', - 'CloudSyncPlacesWrapper.jsm', - 'CloudSyncTabs.jsm', -] - -XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini'] -BROWSER_CHROME_MANIFESTS += ['tests/mochitest/browser.ini'] diff --git a/services/cloudsync/tests/mochitest/browser.ini b/services/cloudsync/tests/mochitest/browser.ini deleted file mode 100644 index c9eddbf71..000000000 --- a/services/cloudsync/tests/mochitest/browser.ini +++ /dev/null @@ -1,5 +0,0 @@ -[DEFAULT] -support-files= - other_window.html - -[browser_tabEvents.js]
\ No newline at end of file diff --git a/services/cloudsync/tests/mochitest/browser_tabEvents.js b/services/cloudsync/tests/mochitest/browser_tabEvents.js deleted file mode 100644 index 9d80090a0..000000000 --- a/services/cloudsync/tests/mochitest/browser_tabEvents.js +++ /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/. */ - -function test() { - - let local = {}; - - Components.utils.import("resource://gre/modules/CloudSync.jsm", local); - Components.utils.import("resource:///modules/sessionstore/TabStateFlusher.jsm", local); - - let cloudSync = local.CloudSync(); - let opentabs = []; - - waitForExplicitFinish(); - - let testURL = "chrome://mochitests/content/browser/services/cloudsync/tests/mochitest/other_window.html"; - let expected = [ - testURL, - testURL+"?x=1", - testURL+"?x=%20a", - // testURL+"?x=å", - ]; - - let nevents = 0; - let nflushed = 0; - function handleTabChangeEvent () { - cloudSync.tabs.removeEventListener("change", handleTabChangeEvent); - ++ nevents; - info("tab change event " + nevents); - next(); - } - - function getLocalTabs() { - cloudSync.tabs.getLocalTabs().then( - function (tabs) { - for (let tab of tabs) { - ok(expected.indexOf(tab.url) >= 0, "found an expected tab"); - } - - is(tabs.length, expected.length, "found the right number of tabs"); - - opentabs.forEach(function (tab) { - gBrowser.removeTab(tab); - }); - - is(nevents, 1, "expected number of change events"); - - finish(); - } - ) - } - - cloudSync.tabs.addEventListener("change", handleTabChangeEvent); - - expected.forEach(function(url) { - let tab = gBrowser.addTab(url); - - function flush() { - tab.linkedBrowser.removeEventListener("load", flush, true); - local.TabStateFlusher.flush(tab.linkedBrowser).then(() => { - ++ nflushed; - info("flushed " + nflushed); - next(); - }); - } - - tab.linkedBrowser.addEventListener("load", flush, true); - - opentabs.push(tab); - }); - - function next() { - if (nevents == 1 && nflushed == expected.length) { - getLocalTabs(); - } - } - -} diff --git a/services/cloudsync/tests/mochitest/other_window.html b/services/cloudsync/tests/mochitest/other_window.html deleted file mode 100644 index a9ded2bd6..000000000 --- a/services/cloudsync/tests/mochitest/other_window.html +++ /dev/null @@ -1,7 +0,0 @@ -<!-- - Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ ---> -<!DOCTYPE HTML> -<html> -</html> diff --git a/services/cloudsync/tests/xpcshell/head.js b/services/cloudsync/tests/xpcshell/head.js deleted file mode 100644 index bd517cafa..000000000 --- a/services/cloudsync/tests/xpcshell/head.js +++ /dev/null @@ -1,10 +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"; - -(function initCloudSyncTestingInfrastructure () { - do_get_profile(); -}).call(this); diff --git a/services/cloudsync/tests/xpcshell/test_bookmarks.js b/services/cloudsync/tests/xpcshell/test_bookmarks.js deleted file mode 100644 index d4e1d2b75..000000000 --- a/services/cloudsync/tests/xpcshell/test_bookmarks.js +++ /dev/null @@ -1,73 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/CloudSync.jsm"); - -function run_test () { - run_next_test(); -} - -function cleanup () { - -} - -add_task(function* test_merge_bookmarks_flat () { - try { - let rootFolder = yield CloudSync().bookmarks.getRootFolder("TEST"); - ok(rootFolder.id, "root folder id is ok"); - - let items = [ - {"id":"G_UL4ZhOyX8m","type":rootFolder.BOOKMARK,"title":"reddit: the front page of the internet 1","uri":"http://www.reddit.com",index:2}, - {"id":"G_UL4ZhOyX8n","type":rootFolder.BOOKMARK,"title":"reddit: the front page of the internet 2","uri":"http://www.reddit.com?1",index:1}, - ]; - yield rootFolder.mergeRemoteItems(items); - - let localItems = yield rootFolder.getLocalItems(); - equal(Object.keys(localItems).length, items.length, "found merged items"); - } finally { - yield CloudSync().bookmarks.deleteRootFolder("TEST"); - } -}); - -add_task(function* test_merge_bookmarks_in_folders () { - try { - let rootFolder = yield CloudSync().bookmarks.getRootFolder("TEST"); - ok(rootFolder.id, "root folder id is ok"); - - let items = [ - {"id":"G_UL4ZhOyX8m","type":rootFolder.BOOKMARK,"title":"reddit: the front page of the internet 1","uri":"http://www.reddit.com",index:2}, - {"id":"G_UL4ZhOyX8n","type":rootFolder.BOOKMARK,parent:"G_UL4ZhOyX8x","title":"reddit: the front page of the internet 2","uri":"http://www.reddit.com/?a=å%20ä%20ö",index:1}, - {"id":"G_UL4ZhOyX8x","type":rootFolder.FOLDER}, - ]; - yield rootFolder.mergeRemoteItems(items); - - let localItems = yield rootFolder.getLocalItems(); - equal(localItems.length, items.length, "found merged items"); - - localItems.forEach(function(item) { - ok(item.id == "G_UL4ZhOyX8m" || - item.id == "G_UL4ZhOyX8n" || - item.id == "G_UL4ZhOyX8x"); - if (item.id == "G_UL4ZhOyX8n") { - equal(item.parent, "G_UL4ZhOyX8x") - } else { - equal(item.parent, rootFolder.id); - } - }); - - let folder = (yield rootFolder.getLocalItemsById(["G_UL4ZhOyX8x"]))[0]; - equal(folder.id, "G_UL4ZhOyX8x"); - equal(folder.type, rootFolder.FOLDER); - - let bookmark = (yield rootFolder.getLocalItemsById(["G_UL4ZhOyX8n"]))[0]; - equal(bookmark.id, "G_UL4ZhOyX8n"); - equal(bookmark.parent, "G_UL4ZhOyX8x"); - equal(bookmark.title, "reddit: the front page of the internet 2"); - equal(bookmark.index, 0); - equal(bookmark.uri, "http://www.reddit.com/?a=%C3%A5%20%C3%A4%20%C3%B6"); - } finally { - yield CloudSync().bookmarks.deleteRootFolder("TEST"); - } -});
\ No newline at end of file diff --git a/services/cloudsync/tests/xpcshell/test_lazyload.js b/services/cloudsync/tests/xpcshell/test_lazyload.js deleted file mode 100644 index 5928875d5..000000000 --- a/services/cloudsync/tests/xpcshell/test_lazyload.js +++ /dev/null @@ -1,18 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/CloudSync.jsm"); - -function run_test() { - run_next_test(); -} - -add_task(function test_lazyload() { - ok(!CloudSync.ready, "CloudSync.ready is false before CloudSync() invoked"); - let cs1 = CloudSync(); - ok(CloudSync.ready, "CloudSync.ready is true after CloudSync() invoked"); - let cs2 = CloudSync(); - ok(cs1 === cs2, "CloudSync() returns the same instance on multiple invocations"); -}); diff --git a/services/cloudsync/tests/xpcshell/test_module.js b/services/cloudsync/tests/xpcshell/test_module.js deleted file mode 100644 index 6d31345ed..000000000 --- a/services/cloudsync/tests/xpcshell/test_module.js +++ /dev/null @@ -1,19 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/CloudSync.jsm"); - -function run_test () { - run_next_test(); -} - -add_task(function test_module_load () { - ok(CloudSync); - let cloudSync = CloudSync(); - ok(cloudSync.adapters); - ok(cloudSync.bookmarks); - ok(cloudSync.local); - ok(cloudSync.tabs); -}); diff --git a/services/cloudsync/tests/xpcshell/test_tabs.js b/services/cloudsync/tests/xpcshell/test_tabs.js deleted file mode 100644 index 50f7a73de..000000000 --- a/services/cloudsync/tests/xpcshell/test_tabs.js +++ /dev/null @@ -1,29 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -Cu.import("resource://gre/modules/CloudSync.jsm"); - -function run_test () { - run_next_test(); -} - -add_task(function* test_get_remote_tabs () { - let cloudSync = CloudSync(); - let clients = yield cloudSync.tabs.getRemoteTabs(); - equal(clients.length, 0); - - yield cloudSync.tabs.mergeRemoteTabs({ - id: "001", - name: "FakeClient", - },[ - {url:"https://www.google.ca?a=å%20ä%20ö",title:"Google Canada",icon:"https://www.google.ca/favicon.ico",lastUsed:0}, - {url:"http://www.reddit.com",title:"Reddit",icon:"http://www.reddit.com/favicon.ico",lastUsed:1}, - ]); - ok(cloudSync.tabs.hasRemoteTabs()); - - clients = yield cloudSync.tabs.getRemoteTabs(); - equal(clients.length, 1); - equal(clients[0].tabs.size, 2); -}); diff --git a/services/cloudsync/tests/xpcshell/xpcshell.ini b/services/cloudsync/tests/xpcshell/xpcshell.ini deleted file mode 100644 index 08d2eff3a..000000000 --- a/services/cloudsync/tests/xpcshell/xpcshell.ini +++ /dev/null @@ -1,10 +0,0 @@ -[DEFAULT] -head = head.js -tail = -firefox-appdir = browser -skip-if = toolkit == 'android' - -[test_module.js] -[test_tabs.js] -[test_bookmarks.js] -[test_lazyload.js] 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 0e072ee74..000000000 --- a/services/fxaccounts/FxAccounts.jsm +++ /dev/null @@ -1,1725 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials", false); - 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 = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp", false); - 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/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 2109d512a..e98d15275 100644 --- a/services/moz.build +++ b/services/moz.build @@ -9,11 +9,5 @@ DIRS += [ 'crypto', ] -if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': - DIRS += ['fxaccounts'] - if CONFIG['MOZ_SERVICES_SYNC']: DIRS += ['sync'] - -if CONFIG['MOZ_SERVICES_CLOUDSYNC']: - DIRS += ['cloudsync'] 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_fxa_node_reassignment.js b/services/sync/tests/unit/test_fxa_node_reassignment.js deleted file mode 100644 index 2f61afd6f..000000000 --- a/services/sync/tests/unit/test_fxa_node_reassignment.js +++ /dev/null @@ -1,321 +0,0 @@ -/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-_("Test that node reassignment happens correctly using the FxA identity mgr.");
-// The node-reassignment logic is quite different for FxA than for the legacy
-// provider. In particular, there's no special request necessary for
-// reassignment - it comes from the token server - so we need to ensure the
-// Fxa cluster manager grabs a new token.
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/rotaryengine.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-Service.engineManager.clear();
-
-function run_test() {
- Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
- initTestLogging();
-
- Service.engineManager.register(RotaryEngine);
-
- // Setup the FxA identity manager and cluster manager.
- Status.__authManager = Service.identity = new BrowserIDManager();
- Service._clusterManager = Service.identity.createClusterManager(Service);
-
- // None of the failures in this file should result in a UI error.
- function onUIError() {
- do_throw("Errors should not be presented in the UI.");
- }
- Svc.Obs.add("weave:ui:login:error", onUIError);
- Svc.Obs.add("weave:ui:sync:error", onUIError);
-
- run_next_test();
-}
-
-
-// API-compatible with SyncServer handler. Bind `handler` to something to use
-// as a ServerCollection handler.
-function handleReassign(handler, req, resp) {
- resp.setStatusLine(req.httpVersion, 401, "Node reassignment");
- resp.setHeader("Content-Type", "application/json");
- let reassignBody = JSON.stringify({error: "401inator in place"});
- resp.bodyOutputStream.write(reassignBody, reassignBody.length);
-}
-
-let numTokenRequests = 0;
-
-function prepareServer(cbAfterTokenFetch) {
- let config = makeIdentityConfig({username: "johndoe"});
- let server = new SyncServer();
- server.registerUser("johndoe");
- server.start();
-
- // Set the token endpoint for the initial token request that's done implicitly
- // via configureIdentity.
- config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe";
- // And future token fetches will do magic around numReassigns.
- let numReassigns = 0;
- return configureIdentity(config).then(() => {
- Service.identity._tokenServerClient = {
- getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
- // Build a new URL with trailing zeros for the SYNC_VERSION part - this
- // will still be seen as equivalent by the test server, but different
- // by sync itself.
- numReassigns += 1;
- let trailingZeros = new Array(numReassigns + 1).join('0');
- let token = config.fxaccount.token;
- token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe";
- token.uid = config.username;
- numTokenRequests += 1;
- cb(null, token);
- if (cbAfterTokenFetch) {
- cbAfterTokenFetch();
- }
- },
- };
- Service.clusterURL = config.fxaccount.token.endpoint;
- return server;
- });
-}
-
-function getReassigned() {
- try {
- return Services.prefs.getBoolPref("services.sync.lastSyncReassigned");
- } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) {
- return false;
- } catch (ex) {
- do_throw("Got exception retrieving lastSyncReassigned: " +
- Utils.exceptionStr(ex));
- }
-}
-
-/**
- * Make a test request to `url`, then watch the result of two syncs
- * to ensure that a node request was made.
- * Runs `between` between the two. This can be used to undo deliberate failure
- * setup, detach observers, etc.
- */
-function syncAndExpectNodeReassignment(server, firstNotification, between,
- secondNotification, url) {
- _("Starting syncAndExpectNodeReassignment\n");
- let deferred = Promise.defer();
- function onwards() {
- let numTokenRequestsBefore;
- function onFirstSync() {
- _("First sync completed.");
- Svc.Obs.remove(firstNotification, onFirstSync);
- Svc.Obs.add(secondNotification, onSecondSync);
-
- do_check_eq(Service.clusterURL, "");
-
- // Track whether we fetched a new token.
- numTokenRequestsBefore = numTokenRequests;
-
- // Allow for tests to clean up error conditions.
- between();
- }
- function onSecondSync() {
- _("Second sync completed.");
- Svc.Obs.remove(secondNotification, onSecondSync);
- Service.scheduler.clearSyncTriggers();
-
- // Make absolutely sure that any event listeners are done with their work
- // before we proceed.
- waitForZeroTimer(function () {
- _("Second sync nextTick.");
- do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token");
- Service.startOver();
- server.stop(deferred.resolve);
- });
- }
-
- Svc.Obs.add(firstNotification, onFirstSync);
- Service.sync();
- }
-
- // Make sure that it works!
- _("Making request to " + url + " which should 401");
- let request = new RESTRequest(url);
- request.get(function () {
- do_check_eq(request.response.status, 401);
- Utils.nextTick(onwards);
- });
- yield deferred.promise;
-}
-
-add_task(function test_momentary_401_engine() {
- _("Test a failure for engine URLs that's resolved by reassignment.");
- let server = yield prepareServer();
- let john = server.user("johndoe");
-
- _("Enabling the Rotary engine.");
- let engine = Service.engineManager.get("rotary");
- engine.enabled = true;
-
- // We need the server to be correctly set up prior to experimenting. Do this
- // through a sync.
- let global = {syncID: Service.syncID,
- storageVersion: STORAGE_VERSION,
- rotary: {version: engine.version,
- syncID: engine.syncID}}
- john.createCollection("meta").insert("global", global);
-
- _("First sync to prepare server contents.");
- Service.sync();
-
- _("Setting up Rotary collection to 401.");
- let rotary = john.createCollection("rotary");
- let oldHandler = rotary.collectionHandler;
- rotary.collectionHandler = handleReassign.bind(this, undefined);
-
- // We want to verify that the clusterURL pref has been cleared after a 401
- // inside a sync. Flag the Rotary engine to need syncing.
- john.collection("rotary").timestamp += 1000;
-
- function between() {
- _("Undoing test changes.");
- rotary.collectionHandler = oldHandler;
-
- function onLoginStart() {
- // lastSyncReassigned shouldn't be cleared until a sync has succeeded.
- _("Ensuring that lastSyncReassigned is still set at next sync start.");
- Svc.Obs.remove("weave:service:login:start", onLoginStart);
- do_check_true(getReassigned());
- }
-
- _("Adding observer that lastSyncReassigned is still set on login.");
- Svc.Obs.add("weave:service:login:start", onLoginStart);
- }
-
- yield syncAndExpectNodeReassignment(server,
- "weave:service:sync:finish",
- between,
- "weave:service:sync:finish",
- Service.storageURL + "rotary");
-});
-
-// This test ends up being a failing info fetch *after we're already logged in*.
-add_task(function test_momentary_401_info_collections_loggedin() {
- _("Test a failure for info/collections after login that's resolved by reassignment.");
- let server = yield prepareServer();
-
- _("First sync to prepare server contents.");
- Service.sync();
-
- _("Arrange for info/collections to return a 401.");
- let oldHandler = server.toplevelHandlers.info;
- server.toplevelHandlers.info = handleReassign;
-
- function undo() {
- _("Undoing test changes.");
- server.toplevelHandlers.info = oldHandler;
- }
-
- do_check_true(Service.isLoggedIn, "already logged in");
-
- yield syncAndExpectNodeReassignment(server,
- "weave:service:sync:error",
- undo,
- "weave:service:sync:finish",
- Service.infoURL);
-});
-
-// This test ends up being a failing info fetch *before we're logged in*.
-// In this case we expect to recover during the login phase - so the first
-// sync succeeds.
-add_task(function test_momentary_401_info_collections_loggedout() {
- _("Test a failure for info/collections before login that's resolved by reassignment.");
-
- let oldHandler;
- let sawTokenFetch = false;
-
- function afterTokenFetch() {
- // After a single token fetch, we undo our evil handleReassign hack, so
- // the next /info request returns the collection instead of a 401
- server.toplevelHandlers.info = oldHandler;
- sawTokenFetch = true;
- }
-
- let server = yield prepareServer(afterTokenFetch);
-
- // Return a 401 for the next /info request - it will be reset immediately
- // after a new token is fetched.
- oldHandler = server.toplevelHandlers.info
- server.toplevelHandlers.info = handleReassign;
-
- do_check_false(Service.isLoggedIn, "not already logged in");
-
- Service.sync();
- do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded");
- // sync was successful - check we grabbed a new token.
- do_check_true(sawTokenFetch, "a new token was fetched by this test.")
- // and we are done.
- Service.startOver();
- let deferred = Promise.defer();
- server.stop(deferred.resolve);
- yield deferred.promise;
-});
-
-// This test ends up being a failing meta/global fetch *after we're already logged in*.
-add_task(function test_momentary_401_storage_loggedin() {
- _("Test a failure for any storage URL after login that's resolved by" +
- "reassignment.");
- let server = yield prepareServer();
-
- _("First sync to prepare server contents.");
- Service.sync();
-
- _("Arrange for meta/global to return a 401.");
- let oldHandler = server.toplevelHandlers.storage;
- server.toplevelHandlers.storage = handleReassign;
-
- function undo() {
- _("Undoing test changes.");
- server.toplevelHandlers.storage = oldHandler;
- }
-
- do_check_true(Service.isLoggedIn, "already logged in");
-
- yield syncAndExpectNodeReassignment(server,
- "weave:service:sync:error",
- undo,
- "weave:service:sync:finish",
- Service.storageURL + "meta/global");
-});
-
-// This test ends up being a failing meta/global fetch *before we've logged in*.
-add_task(function test_momentary_401_storage_loggedout() {
- _("Test a failure for any storage URL before login, not just engine parts. " +
- "Resolved by reassignment.");
- let server = yield prepareServer();
-
- // Return a 401 for all storage requests.
- let oldHandler = server.toplevelHandlers.storage;
- server.toplevelHandlers.storage = handleReassign;
-
- function undo() {
- _("Undoing test changes.");
- server.toplevelHandlers.storage = oldHandler;
- }
-
- do_check_false(Service.isLoggedIn, "already logged in");
-
- yield syncAndExpectNodeReassignment(server,
- "weave:service:login:error",
- undo,
- "weave:service:sync:finish",
- Service.storageURL + "meta/global");
-});
-
diff --git a/services/sync/tests/unit/test_fxa_service_cluster.js b/services/sync/tests/unit/test_fxa_service_cluster.js deleted file mode 100644 index f6f97184a..000000000 --- a/services/sync/tests/unit/test_fxa_service_cluster.js +++ /dev/null @@ -1,68 +0,0 @@ -/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/fxa_utils.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-add_task(function test_findCluster() {
- _("Test FxA _findCluster()");
-
- _("_findCluster() throws on 500 errors.");
- initializeIdentityWithTokenServerResponse({
- status: 500,
- headers: [],
- body: "",
- });
-
- yield Service.identity.initializeWithCurrentIdentity();
- yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
- "should reject due to 500");
-
- Assert.throws(function() {
- Service._clusterManager._findCluster();
- });
-
- _("_findCluster() returns null on authentication errors.");
- initializeIdentityWithTokenServerResponse({
- status: 401,
- headers: {"content-type": "application/json"},
- body: "{}",
- });
-
- yield Service.identity.initializeWithCurrentIdentity();
- yield Assert.rejects(Service.identity.whenReadyToAuthenticate.promise,
- "should reject due to 401");
-
- cluster = Service._clusterManager._findCluster();
- Assert.strictEqual(cluster, null);
-
- _("_findCluster() works with correct tokenserver response.");
- let endpoint = "http://example.com/something";
- initializeIdentityWithTokenServerResponse({
- status: 200,
- headers: {"content-type": "application/json"},
- body:
- JSON.stringify({
- api_endpoint: endpoint,
- duration: 300,
- id: "id",
- key: "key",
- uid: "uid",
- })
- });
-
- yield Service.identity.initializeWithCurrentIdentity();
- yield Service.identity.whenReadyToAuthenticate.promise;
- cluster = Service._clusterManager._findCluster();
- // The cluster manager ensures a trailing "/"
- Assert.strictEqual(cluster, endpoint + "/");
-
- Svc.Prefs.resetBranch("");
-});
-
-function run_test() {
- initTestLogging();
- run_next_test();
-}
diff --git a/services/sync/tests/unit/test_fxa_startOver.js b/services/sync/tests/unit/test_fxa_startOver.js deleted file mode 100644 index e27d86ea0..000000000 --- a/services/sync/tests/unit/test_fxa_startOver.js +++ /dev/null @@ -1,63 +0,0 @@ -/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://testing-common/services/sync/utils.js");
-Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/browserid_identity.js");
-Cu.import("resource://services-sync/service.js");
-
-function run_test() {
- initTestLogging("Trace");
- run_next_test();
-}
-
-add_task(function* test_startover() {
- let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
- Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
-
- ensureLegacyIdentityManager();
- yield configureIdentity({username: "johndoe"});
-
- // The boolean flag on the xpcom service should reflect a legacy provider.
- let xps = Cc["@mozilla.org/weave/service;1"]
- .getService(Components.interfaces.nsISupports)
- .wrappedJSObject;
- do_check_false(xps.fxAccountsEnabled);
-
- // we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
- // extends it)
- do_check_false(Service.identity instanceof BrowserIDManager);
-
- Service.serverURL = "https://localhost/";
- Service.clusterURL = Service.serverURL;
-
- Service.login();
- // We should have a cluster URL
- do_check_true(Service.clusterURL.length > 0);
-
- // remember some stuff so we can reset it after.
- let oldIdentity = Service.identity;
- let oldClusterManager = Service._clusterManager;
- let deferred = Promise.defer();
- Services.obs.addObserver(function observeStartOverFinished() {
- Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
- deferred.resolve();
- }, "weave:service:start-over:finish", false);
-
- Service.startOver();
- yield deferred.promise; // wait for the observer to fire.
-
- // the xpcom service should indicate FxA is enabled.
- do_check_true(xps.fxAccountsEnabled);
- // should have swapped identities.
- do_check_true(Service.identity instanceof BrowserIDManager);
- // should have clobbered the cluster URL
- do_check_eq(Service.clusterURL, "");
-
- // we should have thrown away the old identity provider and cluster manager.
- do_check_neq(oldIdentity, Service.identity);
- do_check_neq(oldClusterManager, Service._clusterManager);
-
- // reset the world.
- Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
-});
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 7e97835ac..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] @@ -122,11 +121,6 @@ skip-if = os == "android" [test_syncscheduler.js] [test_upgrade_old_sync_key.js] -# Firefox Accounts specific tests -[test_fxa_startOver.js] -[test_fxa_service_cluster.js] -[test_fxa_node_reassignment.js] - # Finally, we test each engine. [test_addons_engine.js] run-sequentially = Hardcoded port in static files. 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/toolkit/modules/AppConstants.jsm b/toolkit/modules/AppConstants.jsm index 1e2204f7e..ae0eea1c4 100644 --- a/toolkit/modules/AppConstants.jsm +++ b/toolkit/modules/AppConstants.jsm @@ -102,13 +102,6 @@ MOZ_SAFE_BROWSING: false, #endif - MOZ_SERVICES_CLOUDSYNC: -#ifdef MOZ_SERVICES_CLOUDSYNC - true, -#else - false, -#endif - MOZ_UPDATER: #ifdef MOZ_UPDATER true, diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index e0acdb19e..8f66417c1 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -151,8 +151,7 @@ for var in ('ANDROID_PACKAGE_NAME', for var in ('MOZ_TOOLKIT_SEARCH', 'MOZ_SYSTEM_NSS', 'MOZ_UPDATER', - 'MOZ_SWITCHBOARD', - 'MOZ_SERVICES_CLOUDSYNC'): + 'MOZ_SWITCHBOARD'): if CONFIG[var]: DEFINES[var] = True diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index 26e432b3c..4ae70fd43 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -2462,7 +2462,7 @@ RemoveComponentRegistries(nsIFile* aProfileDir, nsIFile* aLocalProfileDir, if (!file) return false; -#if defined(XP_UNIX) || defined(XP_BEOS) +#if defined(XP_UNIX) #define PLATFORM_FASL_SUFFIX ".mfasl" #elif defined(XP_WIN) #define PLATFORM_FASL_SUFFIX ".mfl" 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"], diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm index 3c8695442..597c25a48 100644 --- a/widget/cocoa/nsNativeThemeCocoa.mm +++ b/widget/cocoa/nsNativeThemeCocoa.mm @@ -2778,7 +2778,8 @@ nsNativeThemeCocoa::DrawWidgetBackground(nsRenderingContext* aContext, NSMutableDictionary* options = [NSMutableDictionary dictionaryWithObjectsAndKeys: (isOverlay ? @"kCUIWidgetOverlayScrollBar" : @"scrollbar"), @"widget", (isSmall ? @"small" : @"regular"), @"size", - (isHorizontal ? @"kCUIOrientHorizontal" : @"kCUIOrientVertical"), @"kCUIOrientationKey", + (isOverlay && isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), + @"kCUIVariantKey", (isOnTopOfDarkBackground ? @"kCUIVariantWhite" : @""), @"kCUIVariantKey", [NSNumber numberWithBool:YES], @"indiconly", [NSNumber numberWithBool:YES], @"kCUIThumbProportionKey", |