diff options
author | Matt A. Tobin <email@mattatobin.com> | 2022-04-18 18:55:17 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2022-04-18 18:55:17 -0500 |
commit | e6e51061aff70d98899c11ab1352fdb0911ce529 (patch) | |
tree | 1045462f28f9faec3dd0a5691114ff7ead18ccde | |
parent | be1f005cffbb29b815a7aee659fb05bb32cb9793 (diff) | |
download | aura-central-e6e51061aff70d98899c11ab1352fdb0911ce529.tar.gz |
Issue #7 - Remove Weave
94 files changed, 0 insertions, 23198 deletions
diff --git a/apps/mail/confvars.sh b/apps/mail/confvars.sh index f1087e432..91857b8ca 100644 --- a/apps/mail/confvars.sh +++ b/apps/mail/confvars.sh @@ -31,10 +31,6 @@ THUNDERBIRD_VERSION=$MOZ_APP_VERSION # Platform build options MOZ_PLACES=1 MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES=1 -MOZ_SERVICES_SYNC= -MOZ_JETPACK= -MOZ_DEVTOOLS_SERVER= -MOZ_DEVTOOLS= MOZ_GAMEPAD= MOZ_NECKO_WIFI= MOZ_AV1= diff --git a/apps/navigator/app/defaults/pref/application.js b/apps/navigator/app/defaults/pref/application.js index 3cfb57e24..2ca55b5d6 100644 --- a/apps/navigator/app/defaults/pref/application.js +++ b/apps/navigator/app/defaults/pref/application.js @@ -23,7 +23,3 @@ #include places-prefs.inc.js #include secpriv-prefs.inc.js - -#ifdef MOZ_SERVICES_SYNC -#include secpriv-prefs.inc.js -#endif
\ No newline at end of file diff --git a/apps/navigator/confvars.sh b/apps/navigator/confvars.sh index 80069bcab..2dd6bb10a 100644 --- a/apps/navigator/confvars.sh +++ b/apps/navigator/confvars.sh @@ -28,9 +28,5 @@ MOZ_PROFILE_MIGRATOR=1 MOZ_APP_STATIC_INI=1 MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES=1 MOZ_WEBGL_CONFORMANT=1 -MOZ_SERVICES_SYNC= -MOZ_JETPACK= -MOZ_DEVTOOLS_SERVER= -MOZ_DEVTOOLS= MOZ_GAMEPAD= MOZ_NECKO_WIFI= diff --git a/build/autoconf/features.configure b/build/autoconf/features.configure index d04f42cf2..f8075b61e 100644 --- a/build/autoconf/features.configure +++ b/build/autoconf/features.configure @@ -45,7 +45,6 @@ ACCESSIBILITY=1 MOZ_TIME_MANAGER= MOZ_AUDIO_CHANNEL_MANAGER= MOZ_PLACES=1 -MOZ_SERVICES_SYNC=1 MOZ_USERINFO=1 MOZ_MAILNEWS= MOZ_MAILNEWS_OAUTH2= @@ -2359,13 +2358,6 @@ if test "$MOZ_PLACES"; then AC_DEFINE(MOZ_PLACES) fi -dnl Build Sync Services if required -AC_SUBST(MOZ_SERVICES_SYNC) -if test -n "$MOZ_SERVICES_SYNC"; then - AC_DEFINE(MOZ_SERVICES_SYNC) -fi - - dnl ======================================================== if test "$MOZ_DEBUG"; then MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS= diff --git a/components/moz.build b/components/moz.build index 5bb3ba83a..78af3359f 100644 --- a/components/moz.build +++ b/components/moz.build @@ -110,9 +110,6 @@ if CONFIG['MOZ_ENABLE_XREMOTE']: if CONFIG['MOZ_TOOLKIT_SEARCH']: DIRS += ['search'] -if CONFIG['MOZ_SERVICES_SYNC']: - DIRS += ['weave'] - DIRS += ['build'] EXTRA_COMPONENTS += [ diff --git a/components/weave/WeaveComponents.manifest b/components/weave/WeaveComponents.manifest deleted file mode 100644 index 816a9e2b4..000000000 --- a/components/weave/WeaveComponents.manifest +++ /dev/null @@ -1,13 +0,0 @@ -# WeaveService.js -component {74b89fb0-f200-4ae8-a3ec-dd164117f6de} WeaveService.js -contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de} -category app-startup WeaveService service,@mozilla.org/weave/service;1 - -# WeaveService.js (about:sync-log) -component {d28f8a0b-95da-48f4-b712-caf37097be41} WeaveService.js -contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41} - -# Register resource aliases -resource services-common resource://gre/modules/services-common/ -resource services-crypto resource://gre/modules/services-crypto/ -resource services-sync resource://gre/modules/services-sync/ diff --git a/components/weave/content/aboutSyncTabs-bindings.xml b/components/weave/content/aboutSyncTabs-bindings.xml deleted file mode 100644 index e6108209a..000000000 --- a/components/weave/content/aboutSyncTabs-bindings.xml +++ /dev/null @@ -1,46 +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/. --> - -<bindings id="tabBindings" - xmlns="http://www.mozilla.org/xbl" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xbl="http://www.mozilla.org/xbl"> - - <binding id="tab-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> - <content> - <xul:hbox flex="1"> - <xul:vbox pack="start"> - <xul:image class="tabIcon" - xbl:inherits="src=icon"/> - </xul:vbox> - <xul:vbox pack="start" flex="1"> - <xul:label xbl:inherits="value=title,selected" - crop="end" flex="1" class="title"/> - <xul:label xbl:inherits="value=url,selected" - crop="end" flex="1" class="url"/> - </xul:vbox> - </xul:hbox> - </content> - <handlers> - <handler event="dblclick" button="0"> - <![CDATA[ - RemoteTabViewer.openSelected(); - ]]> - </handler> - </handlers> - </binding> - - <binding id="client-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> - <content> - <xul:hbox pack="start" align="center" onfocus="event.target.blur()" onselect="return false;"> - <xul:image/> - <xul:label xbl:inherits="value=clientName" - class="clientName" - crop="center" flex="1"/> - </xul:hbox> - </content> - </binding> -</bindings> diff --git a/components/weave/content/aboutSyncTabs.css b/components/weave/content/aboutSyncTabs.css deleted file mode 100644 index 2d71e254d..000000000 --- a/components/weave/content/aboutSyncTabs.css +++ /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/. */ - -richlistitem[type="tab"] { - -moz-binding: url(chrome://weave/content/aboutSyncTabs-bindings.xml#tab-listing); -} - -richlistitem[type="client"] { - -moz-binding: url(chrome://weave/content/aboutSyncTabs-bindings.xml#client-listing); -} diff --git a/components/weave/content/aboutSyncTabs.js b/components/weave/content/aboutSyncTabs.js deleted file mode 100644 index dafb79b9d..000000000 --- a/components/weave/content/aboutSyncTabs.js +++ /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/. */ - -var Cu = Components.utils; - -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-sync/main.js"); -Cu.import("resource:///modules/PlacesUIUtils.jsm"); -Cu.import("resource://gre/modules/PlacesUtils.jsm", this); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -var RemoteTabViewer = { - _tabsList: null, - - init: function () { - Services.obs.addObserver(this, "weave:service:login:finish", false); - Services.obs.addObserver(this, "weave:engine:sync:finish", false); - - this._tabsList = document.getElementById("tabsList"); - - this.buildList(true); - }, - - uninit: function () { - Services.obs.removeObserver(this, "weave:service:login:finish"); - Services.obs.removeObserver(this, "weave:engine:sync:finish"); - }, - - createItem: function(attrs) { - let item = document.createElement("richlistitem"); - - // Copy the attributes from the argument into the item - for (let attr in attrs) { - item.setAttribute(attr, attrs[attr]); - } - - if (attrs["type"] == "tab") { - item.label = attrs.title != "" ? attrs.title : attrs.url; - } - - return item; - }, - - filterTabs: function(event) { - let val = event.target.value.toLowerCase(); - let numTabs = this._tabsList.getRowCount(); - let clientTabs = 0; - let currentClient = null; - - for (let i = 0; i < numTabs; i++) { - let item = this._tabsList.getItemAtIndex(i); - let hide = false; - if (item.getAttribute("type") == "tab") { - if (!item.getAttribute("url").toLowerCase().includes(val) && - !item.getAttribute("title").toLowerCase().includes(val)) { - hide = true; - } else { - clientTabs++; - } - } - else if (item.getAttribute("type") == "client") { - if (currentClient) { - if (clientTabs == 0) { - currentClient.hidden = true; - } - } - currentClient = item; - clientTabs = 0; - } - item.hidden = hide; - } - if (clientTabs == 0) { - currentClient.hidden = true; - } - }, - - openSelected: function() { - let items = this._tabsList.selectedItems; - let urls = []; - 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]); - this._tabsList.removeItemAt(index); - } - } - if (urls.length) { - getTopWin().gBrowser.loadTabs(urls); - this._tabsList.clearSelection(); - } - }, - - bookmarkSingleTab: function() { - let item = this._tabsList.selectedItems[0]; - let uri = Weave.Utils.makeURI(item.getAttribute("url")); - let title = item.getAttribute("title"); - PlacesUIUtils.showBookmarkDialog({ action: "add" - , type: "bookmark" - , uri: uri - , title: title - , hiddenRows: [ "description" - , "location" - , "loadInSidebar" - , "keyword" ] - }, window.top); - }, - - bookmarkSelectedTabs: function() { - let items = this._tabsList.selectedItems; - let URIs = []; - 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) { - continue; - } - - URIs.push(uri); - } - } - if (URIs.length) { - PlacesUIUtils.showBookmarkDialog({ action: "add" - , type: "folder" - , URIList: URIs - , hiddenRows: [ "description" ] - }, window.top); - } - }, - - getIcon: function (iconUri, defaultIcon) { - try { - let iconURI = Weave.Utils.makeURI(iconUri); - return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec; - } catch (ex) { - // Do nothing. - } - - // Just give the provided default icon or the system's default. - return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec; - }, - - _waitingForBuildList: false, - - _buildListRequested: false, - - buildList: function (force) { - if (this._waitingForBuildList) { - this._buildListRequested = true; - return; - } - - this._waitingForBuildList = true; - this._buildListRequested = false; - - this._clearTabList(); - - if (Weave.Service.isLoggedIn && this._refetchTabs(force)) { - this._generateWeaveTabList(); - } else { - //XXXzpao We should say something about not being logged in & not having data - // or tell the appropriate condition. (bug 583344) - } - - function complete() { - this._waitingForBuildList = false; - if (this._buildListRequested) { - CommonUtils.nextTick(this.buildList, this); - } - } - - complete(); - }, - - _clearTabList: function () { - let list = this._tabsList; - - // Clear out existing richlistitems - let count = list.getRowCount(); - if (count > 0) { - for (let i = count - 1; i >= 0; i--) { - list.removeItemAt(i); - } - } - }, - - _generateWeaveTabList: function () { - let engine = Weave.Service.engineManager.get("tabs"); - let list = this._tabsList; - - let seenURLs = new Set(); - let localURLs = engine.getOpenURLs(); - - 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; - - client.tabs.forEach(function({title, urlHistory, icon}) { - let url = urlHistory[0]; - if (!url || localURLs.has(url) || seenURLs.has(url)) { - return; - } - seenURLs.add(url); - - if (appendClient) { - let attrs = { - type: "client", - clientName: client.clientName, - class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop" - }; - let clientEnt = this.createItem(attrs); - list.appendChild(clientEnt); - appendClient = false; - clientEnt.disabled = true; - } - let attrs = { - type: "tab", - title: title || url, - url: url, - icon: this.getIcon(icon), - } - let tab = this.createItem(attrs); - list.appendChild(tab); - }, this); - } - }, - - adjustContextMenu: function(event) { - let mode = "all"; - switch (this._tabsList.selectedItems.length) { - case 0: - break; - case 1: - mode = "single" - break; - default: - mode = "multiple"; - break; - } - - let menu = document.getElementById("tabListContext"); - let el = menu.firstChild; - while (el) { - let showFor = el.getAttribute("showFor"); - if (showFor) { - el.hidden = showFor != mode && showFor != "all"; - } - - el = el.nextSibling; - } - }, - - _refetchTabs: function(force) { - if (!force) { - // Don't bother refetching tabs if we already did so recently - let lastFetch = 0; - try { - lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch"); - } - catch (e) { - /* Just use the default value of 0 */ - } - - let now = Math.floor(Date.now() / 1000); - if (now - lastFetch < 30) { - return false; - } - } - - // 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) { - switch (topic) { - case "weave:service:login:finish": - this.buildList(true); - break; - case "weave:engine:sync:finish": - if (subject == "tabs") { - this.buildList(false); - } - break; - } - }, - - handleClick: function(event) { - if (event.target.getAttribute("type") != "tab") { - return; - } - - - if (event.button == 1) { - let url = event.target.getAttribute("url"); - openUILink(url, event); - let index = this._tabsList.getIndexOfItem(event.target); - this._tabsList.removeItemAt(index); - } - } -} - diff --git a/components/weave/content/aboutSyncTabs.xul b/components/weave/content/aboutSyncTabs.xul deleted file mode 100644 index 2300c0b42..000000000 --- a/components/weave/content/aboutSyncTabs.xul +++ /dev/null @@ -1,67 +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/. --> - -<?xml-stylesheet href="chrome://weave/skin/aboutSyncTabs.css" type="text/css"?> -<?xml-stylesheet href="chrome://weave/content/aboutSyncTabs.css" type="text/css"?> - -<!DOCTYPE window [ - <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://weave/locale/aboutSyncTabs.dtd"> - %aboutSyncTabsDTD; -]> - -<window id="tabs-display" - onload="RemoteTabViewer.init()" - onunload="RemoteTabViewer.uninit()" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - title="&tabs.otherDevices.label;"> - <script type="application/javascript;version=1.8" src="chrome://weave/content/aboutSyncTabs.js"/> - <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> - <html:head> - <html:link rel="icon" href="chrome://weave/skin/sync-16.png"/> - </html:head> - - <popupset id="contextmenus"> - <menupopup id="tabListContext"> - <menuitem label="&tabs.context.openTab.label;" - accesskey="&tabs.context.openTab.accesskey;" - oncommand="RemoteTabViewer.openSelected()" - showFor="single"/> - <menuitem label="&tabs.context.bookmarkSingleTab.label;" - accesskey="&tabs.context.bookmarkSingleTab.accesskey;" - oncommand="RemoteTabViewer.bookmarkSingleTab(event)" - showFor="single"/> - <menuitem label="&tabs.context.openMultipleTabs.label;" - accesskey="&tabs.context.openMultipleTabs.accesskey;" - oncommand="RemoteTabViewer.openSelected()" - showFor="multiple"/> - <menuitem label="&tabs.context.bookmarkMultipleTabs.label;" - accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;" - oncommand="RemoteTabViewer.bookmarkSelectedTabs()" - showFor="multiple"/> - <menuseparator/> - <menuitem label="&tabs.context.refreshList.label;" - accesskey="&tabs.context.refreshList.accesskey;" - oncommand="RemoteTabViewer.buildList()" - showFor="all"/> - </menupopup> - </popupset> - <richlistbox context="tabListContext" id="tabsList" seltype="multiple" - align="center" flex="1" - onclick="RemoteTabViewer.handleClick(event)" - oncontextmenu="RemoteTabViewer.adjustContextMenu(event)"> - <hbox id="headers" align="center"> - <label id="tabsListHeading" - value="&tabs.otherDevices.label;"/> - <spacer flex="1"/> - <textbox type="search" - emptytext="&tabs.searchText.label;" - oncommand="RemoteTabViewer.filterTabs(event)"/> - </hbox> - - </richlistbox> -</window> - diff --git a/components/weave/content/addDevice.js b/components/weave/content/addDevice.js deleted file mode 100644 index 98ac74d7d..000000000 --- a/components/weave/content/addDevice.js +++ /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/. */ - -var Ci = Components.interfaces; -var Cc = Components.classes; -var Cu = Components.utils; - -Cu.import("resource://services-sync/main.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -const PIN_PART_LENGTH = 4; - -const ADD_DEVICE_PAGE = 0; -const SYNC_KEY_PAGE = 1; -const DEVICE_CONNECTED_PAGE = 2; - -var gSyncAddDevice = { - - init: function() { - this.pin1.setAttribute("maxlength", PIN_PART_LENGTH); - this.pin2.setAttribute("maxlength", PIN_PART_LENGTH); - this.pin3.setAttribute("maxlength", PIN_PART_LENGTH); - - this.nextFocusEl = {pin1: this.pin2, - pin2: this.pin3, - pin3: this.wizard.getButton("next")}; - - this.throbber = document.getElementById("pairDeviceThrobber"); - this.errorRow = document.getElementById("errorRow"); - - // Kick off a sync. That way the server will have the most recent data from - // this computer and it will show up immediately on the new device. - Weave.Service.scheduler.scheduleNextSync(0); - }, - - onPageShow: function() { - this.wizard.getButton("back").hidden = true; - - switch (this.wizard.pageIndex) { - case ADD_DEVICE_PAGE: - this.onTextBoxInput(); - this.wizard.canRewind = false; - this.wizard.getButton("next").hidden = false; - this.pin1.focus(); - break; - case SYNC_KEY_PAGE: - this.wizard.canAdvance = false; - this.wizard.canRewind = true; - this.wizard.getButton("back").hidden = false; - this.wizard.getButton("next").hidden = true; - document.getElementById("weavePassphrase").value = - Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey); - break; - case DEVICE_CONNECTED_PAGE: - this.wizard.canAdvance = true; - this.wizard.canRewind = false; - this.wizard.getButton("cancel").hidden = true; - break; - } - }, - - onWizardAdvance: function() { - switch (this.wizard.pageIndex) { - case ADD_DEVICE_PAGE: - this.startTransfer(); - return false; - case DEVICE_CONNECTED_PAGE: - window.close(); - return false; - } - return true; - }, - - startTransfer: function() { - this.errorRow.hidden = true; - // When onAbort is called, Weave may already be gone. - const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; - - let self = this; - let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({ - onPaired: function() { - let credentials = {account: Weave.Service.identity.account, - password: Weave.Service.identity.basicPassword, - synckey: Weave.Service.identity.syncKey, - serverURL: Weave.Service.serverURL}; - jpakeclient.sendAndComplete(credentials); - }, - onComplete: function() { - delete self._jpakeclient; - self.wizard.pageIndex = DEVICE_CONNECTED_PAGE; - - // Schedule a Sync for soonish to fetch the data uploaded by the - // device with which we just paired. - Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval); - }, - onAbort: function(error) { - delete self._jpakeclient; - - // Aborted by user, ignore. - if (error == JPAKE_ERROR_USERABORT) { - return; - } - - self.errorRow.hidden = false; - self.throbber.hidden = true; - self.pin1.value = self.pin2.value = self.pin3.value = ""; - self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false; - self.pin1.focus(); - } - }); - this.throbber.hidden = false; - this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true; - this.wizard.canAdvance = false; - - let pin = this.pin1.value + this.pin2.value + this.pin3.value; - let expectDelay = false; - jpakeclient.pairWithPIN(pin, expectDelay); - }, - - onWizardBack: function() { - if (this.wizard.pageIndex != SYNC_KEY_PAGE) - return true; - - this.wizard.pageIndex = ADD_DEVICE_PAGE; - return false; - }, - - onWizardCancel: function() { - if (this._jpakeclient) { - this._jpakeclient.abort(); - delete this._jpakeclient; - } - return true; - }, - - onTextBoxInput: function(textbox) { - if (textbox && textbox.value.length == PIN_PART_LENGTH) - this.nextFocusEl[textbox.id].focus(); - - this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH - && this.pin2.value.length == PIN_PART_LENGTH - && this.pin3.value.length == PIN_PART_LENGTH); - }, - - goToSyncKeyPage: function() { - this.wizard.pageIndex = SYNC_KEY_PAGE; - } - -}; -// onWizardAdvance() and onPageShow() are run before init() so we'll set -// these up as lazy getters. -["wizard", "pin1", "pin2", "pin3"].forEach(function(id) { - XPCOMUtils.defineLazyGetter(gSyncAddDevice, id, function() { - return document.getElementById(id); - }); -}); diff --git a/components/weave/content/addDevice.xul b/components/weave/content/addDevice.xul deleted file mode 100644 index ab640a068..000000000 --- a/components/weave/content/addDevice.xul +++ /dev/null @@ -1,129 +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://weave/skin/syncSetup.css" type="text/css"?> -<?xml-stylesheet href="chrome://weave/skin/syncCommon.css" type="text/css"?> - -<!DOCTYPE window [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> -<!ENTITY % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> -<!ENTITY % syncSetupDTD SYSTEM "chrome://weave/locale/syncSetup.dtd"> -%brandDTD; -%syncBrandDTD; -%syncSetupDTD; -]> -<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - id="wizard" - title="&pairDevice.title.label;" - windowtype="Sync:AddDevice" - persist="screenX screenY" - onwizardnext="return gSyncAddDevice.onWizardAdvance();" - onwizardback="return gSyncAddDevice.onWizardBack();" - onwizardcancel="gSyncAddDevice.onWizardCancel();" - onload="gSyncAddDevice.init();"> - - <script type="application/javascript" - src="chrome://weave/content/addDevice.js"/> - <script type="application/javascript" - src="chrome://weave/content/utils.js"/> - <script type="application/javascript" - src="chrome://browser/content/utilityOverlay.js"/> - <script type="application/javascript" - src="chrome://global/content/printUtils.js"/> - - <wizardpage id="addDevicePage" - label="&pairDevice.title.label;" - onpageshow="gSyncAddDevice.onPageShow();"> - <description> - &pairDevice.dialog.description.label; - <label class="text-link" - value="&addDevice.showMeHow.label;" - href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> - </description> - <separator class="groove-thin"/> - <description> - &addDevice.dialog.enterCode.label; - </description> - <separator class="groove-thin"/> - <vbox align="center"> - <textbox id="pin1" - class="pin" - oninput="gSyncAddDevice.onTextBoxInput(this);" - onfocus="this.select();" - /> - <textbox id="pin2" - class="pin" - oninput="gSyncAddDevice.onTextBoxInput(this);" - onfocus="this.select();" - /> - <textbox id="pin3" - class="pin" - oninput="gSyncAddDevice.onTextBoxInput(this);" - onfocus="this.select();" - /> - </vbox> - <separator class="groove-thin"/> - <vbox id="pairDeviceThrobber" align="center" hidden="true"> - <image/> - </vbox> - <hbox id="errorRow" pack="center" hidden="true"> - <image class="statusIcon" status="error"/> - <label class="status" - value="&addDevice.dialog.tryAgain.label;"/> - </hbox> - <spacer flex="3"/> - <label class="text-link" - value="&addDevice.dontHaveDevice.label;" - onclick="gSyncAddDevice.goToSyncKeyPage();"/> - </wizardpage> - - <!-- Need a non-empty label here, otherwise we get a default label on Mac --> - <wizardpage id="syncKeyPage" - label=" " - onpageshow="gSyncAddDevice.onPageShow();"> - <description> - &addDevice.dialog.recoveryKey.label; - </description> - <spacer/> - - <groupbox> - <label value="&recoveryKeyEntry.label;" - accesskey="&recoveryKeyEntry.accesskey;" - control="weavePassphrase"/> - <textbox id="weavePassphrase" - readonly="true"/> - </groupbox> - - <groupbox align="center"> - <description>&recoveryKeyBackup.description;</description> - <hbox> - <button id="printSyncKeyButton" - label="&button.syncKeyBackup.print.label;" - accesskey="&button.syncKeyBackup.print.accesskey;" - oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/> - <button id="saveSyncKeyButton" - label="&button.syncKeyBackup.save.label;" - accesskey="&button.syncKeyBackup.save.accesskey;" - oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/> - </hbox> - </groupbox> - </wizardpage> - - <wizardpage id="deviceConnectedPage" - label="&addDevice.dialog.connected.label;" - onpageshow="gSyncAddDevice.onPageShow();"> - <vbox align="center"> - <image id="successPageIcon"/> - </vbox> - <separator/> - <description class="normal"> - &addDevice.dialog.successful.label; - </description> - </wizardpage> - -</wizard> diff --git a/components/weave/content/genericChange.js b/components/weave/content/genericChange.js deleted file mode 100644 index 75adc4893..000000000 --- a/components/weave/content/genericChange.js +++ /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/. */ - -var Ci = Components.interfaces; -var Cc = Components.classes; - -Components.utils.import("resource://services-sync/main.js"); -Components.utils.import("resource://gre/modules/Services.jsm"); - -var Change = { - _dialog: null, - _dialogType: null, - _status: null, - _statusIcon: null, - _firstBox: null, - _secondBox: null, - - get _passphraseBox() { - delete this._passphraseBox; - return this._passphraseBox = document.getElementById("passphraseBox"); - }, - - get _currentPasswordInvalid() { - return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; - }, - - get _updatingPassphrase() { - return this._dialogType == "UpdatePassphrase"; - }, - - onLoad: function() { - /* 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 - this._dialog = document.getElementById("change-dialog"); - this._dialogType = window.arguments[0]; - this._duringSetup = window.arguments[1]; - this._status = document.getElementById("status"); - this._statusIcon = document.getElementById("statusIcon"); - this._statusRow = document.getElementById("statusRow"); - this._firstBox = document.getElementById("textBox1"); - this._secondBox = document.getElementById("textBox2"); - - this._dialog.getButton("finish").disabled = true; - this._dialog.getButton("back").hidden = true; - - this._stringBundle = - Services.strings.createBundle("chrome://weave/locale/syncGenericChange.properties"); - - switch (this._dialogType) { - case "UpdatePassphrase": - case "ResetPassphrase": - document.getElementById("textBox1Row").hidden = true; - document.getElementById("textBox2Row").hidden = true; - document.getElementById("passphraseLabel").value - = this._str("new.recoverykey.label"); - document.getElementById("passphraseSpacer").hidden = false; - - if (this._updatingPassphrase) { - document.getElementById("passphraseHelpBox").hidden = false; - document.title = this._str("new.recoverykey.title"); - introText.textContent = this._str("new.recoverykey.introText"); - this._dialog.getButton("finish").label - = this._str("new.recoverykey.acceptButton"); - } - else { - document.getElementById("generatePassphraseButton").hidden = false; - document.getElementById("passphraseBackupButtons").hidden = false; - let pp = Weave.Service.identity.syncKey; - if (Weave.Utils.isPassphrase(pp)) - pp = Weave.Utils.hyphenatePassphrase(pp); - this._passphraseBox.value = pp; - this._passphraseBox.focus(); - document.title = this._str("change.recoverykey.title"); - introText.textContent = this._str("change.synckey.introText2"); - warningText.textContent = this._str("change.recoverykey.warningText"); - this._dialog.getButton("finish").label - = this._str("change.recoverykey.acceptButton"); - if (this._duringSetup) { - this._dialog.getButton("finish").disabled = false; - } - } - break; - case "ChangePassword": - document.getElementById("passphraseRow").hidden = true; - let box1label = document.getElementById("textBox1Label"); - let box2label = document.getElementById("textBox2Label"); - box1label.value = this._str("new.password.label"); - - if (this._currentPasswordInvalid) { - document.title = this._str("new.password.title"); - introText.textContent = this._str("new.password.introText"); - this._dialog.getButton("finish").label - = this._str("new.password.acceptButton"); - document.getElementById("textBox2Row").hidden = true; - } - else { - document.title = this._str("change.password.title"); - box2label.value = this._str("new.password.confirm"); - introText.textContent = this._str("change.password3.introText"); - warningText.textContent = this._str("change.password.warningText"); - this._dialog.getButton("finish").label - = this._str("change.password.acceptButton"); - } - break; - } - document.getElementById("change-page") - .setAttribute("label", document.title); - }, - - _clearStatus: function() { - this._status.value = ""; - this._statusIcon.removeAttribute("status"); - }, - - _updateStatus: function(str, state) { - this._updateStatusWithString(this._str(str), state); - }, - - _updateStatusWithString: function(string, state) { - this._statusRow.hidden = false; - this._status.value = string; - this._statusIcon.setAttribute("status", state); - - let error = state == "error"; - this._dialog.getButton("cancel").disabled = !error; - this._dialog.getButton("finish").disabled = !error; - document.getElementById("printSyncKeyButton").disabled = !error; - document.getElementById("saveSyncKeyButton").disabled = !error; - - if (state == "success") - window.setTimeout(window.close, 1500); - }, - - onDialogAccept: function() { - switch (this._dialogType) { - case "UpdatePassphrase": - case "ResetPassphrase": - return this.doChangePassphrase(); - break; - case "ChangePassword": - return this.doChangePassword(); - break; - } - }, - - doGeneratePassphrase: function() { - let passphrase = Weave.Utils.generatePassphrase(); - this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase); - this._dialog.getButton("finish").disabled = false; - }, - - doChangePassphrase: function() { - let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value); - if (this._updatingPassphrase) { - Weave.Service.identity.syncKey = pp; - if (Weave.Service.login()) { - this._updateStatus("change.recoverykey.success", "success"); - Weave.Service.persistLogin(); - Weave.Service.scheduler.delayedAutoConnect(0); - } - else { - this._updateStatus("new.passphrase.status.incorrect", "error"); - } - } - else { - this._updateStatus("change.recoverykey.label", "active"); - - if (Weave.Service.changePassphrase(pp)) - this._updateStatus("change.recoverykey.success", "success"); - else - this._updateStatus("change.recoverykey.error", "error"); - } - - return false; - }, - - doChangePassword: function() { - if (this._currentPasswordInvalid) { - Weave.Service.identity.basicPassword = this._firstBox.value; - if (Weave.Service.login()) { - this._updateStatus("change.password.status.success", "success"); - Weave.Service.persistLogin(); - } - else { - this._updateStatus("new.password.status.incorrect", "error"); - } - } - else { - this._updateStatus("change.password.status.active", "active"); - - if (Weave.Service.changePassword(this._firstBox.value)) - this._updateStatus("change.password.status.success", "success"); - else - this._updateStatus("change.password.status.error", "error"); - } - - return false; - }, - - validate: function(event) { - let valid = false; - let errorString = ""; - - if (this._dialogType == "ChangePassword") { - if (this._currentPasswordInvalid) - [valid, errorString] = gSyncUtils.validatePassword(this._firstBox); - else - [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox); - } - else { - //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 == "") - this._clearStatus(); - else - this._updateStatusWithString(errorString, "error"); - - this._statusRow.hidden = valid; - this._dialog.getButton("finish").disabled = !valid; - }, - - _str: function(str) { - return this._stringBundle.GetStringFromName(str); - } -}; diff --git a/components/weave/content/genericChange.xul b/components/weave/content/genericChange.xul deleted file mode 100644 index ab6e9b0a2..000000000 --- a/components/weave/content/genericChange.xul +++ /dev/null @@ -1,123 +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://weave/skin/syncSetup.css" type="text/css"?> -<?xml-stylesheet href="chrome://weave/skin/syncCommon.css" type="text/css"?> - -<!DOCTYPE window [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> -<!ENTITY % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> -<!ENTITY % syncSetupDTD SYSTEM "chrome://weave/locale/syncSetup.dtd"> -%brandDTD; -%syncBrandDTD; -%syncSetupDTD; -]> -<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - id="change-dialog" - windowtype="Weave:ChangeSomething" - persist="screenX screenY" - onwizardnext="Change.onLoad()" - onwizardfinish="return Change.onDialogAccept();"> - - <script type="application/javascript" - src="chrome://weave/content/genericChange.js"/> - <script type="application/javascript" - src="chrome://weave/content/utils.js"/> - <script type="application/javascript" - src="chrome://global/content/printUtils.js"/> - - <wizardpage id="change-page" - label=""> - - <description id="introText"> - </description> - - <separator class="thin"/> - - <groupbox> - <grid> - <columns> - <column align="right"/> - <column flex="3"/> - <column flex="1"/> - </columns> - <rows> - <row id="textBox1Row" align="center"> - <label id="textBox1Label" control="textBox1"/> - <textbox id="textBox1" type="password" oninput="Change.validate()"/> - <spacer/> - </row> - <row id="textBox2Row" align="center"> - <label id="textBox2Label" control="textBox2"/> - <textbox id="textBox2" type="password" oninput="Change.validate()"/> - <spacer/> - </row> - </rows> - </grid> - - <vbox id="passphraseRow"> - <hbox flex="1"> - <label id="passphraseLabel" control="passphraseBox"/> - <spacer flex="1"/> - <label id="generatePassphraseButton" - hidden="true" - value="&syncGenerateNewKey.label;" - class="text-link inline-link" - onclick="event.stopPropagation(); - Change.doGeneratePassphrase();"/> - </hbox> - <textbox id="passphraseBox" - flex="1" - onfocus="this.select()" - oninput="Change.validate()"/> - </vbox> - - <vbox id="feedback" pack="center"> - <hbox id="statusRow" align="center"> - <image id="statusIcon" class="statusIcon"/> - <label id="status" class="status" value=" "/> - </hbox> - </vbox> - </groupbox> - - <separator class="thin"/> - - <hbox id="passphraseBackupButtons" - hidden="true" - pack="center"> - <button id="printSyncKeyButton" - label="&button.syncKeyBackup.print.label;" - accesskey="&button.syncKeyBackup.print.accesskey;" - oncommand="gSyncUtils.passphrasePrint('passphraseBox');"/> - <button id="saveSyncKeyButton" - label="&button.syncKeyBackup.save.label;" - accesskey="&button.syncKeyBackup.save.accesskey;" - oncommand="gSyncUtils.passphraseSave('passphraseBox');"/> - </hbox> - - <vbox id="passphraseHelpBox" - hidden="true"> - <description> - &existingRecoveryKey.description; - <label class="text-link" - href="http://www.palemoon.org/sync/help/recoverykey.shtml"> - &addDevice.showMeHow.label; - </label> - </description> - </vbox> - - <spacer id="passphraseSpacer" - flex="1" - hidden="true"/> - - <description id="warningText" class="data"> - </description> - - <spacer flex="1"/> - </wizardpage> -</wizard> diff --git a/components/weave/content/key.xhtml b/components/weave/content/key.xhtml deleted file mode 100644 index ca9d03d42..000000000 --- a/components/weave/content/key.xhtml +++ /dev/null @@ -1,54 +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 % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> - %syncBrandDTD; - <!ENTITY % syncKeyDTD SYSTEM "chrome://weave/locale/syncKey.dtd"> - %syncKeyDTD; - <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > - %globalDTD; -]> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> - <title>&syncKey.page.title;</title> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> - <meta name="robots" content="noindex"/> - <style type="text/css"> - #synckey { font-size: 150% } - footer { font-size: 70% } - /* Bug 575675: Need to have an a:visited rule in a chrome document. */ - a:visited { color: purple; } - </style> -</head> - -<body dir="&locale.dir;"> -<h1>&syncKey.page.title;</h1> - -<p id="synckey" dir="ltr">SYNCKEY</p> - -<p>&syncKey.page.description2;</p> - -<div id="column1"> - <h2>&syncKey.keepItSecret.heading;</h2> - <p>&syncKey.keepItSecret.description;</p> -</div> - -<div id="column2"> - <h2>&syncKey.keepItSafe.heading;</h2> - <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="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; -</footer> - -</body> -</html> diff --git a/components/weave/content/notification.xml b/components/weave/content/notification.xml deleted file mode 100644 index 8ac881e08..000000000 --- a/components/weave/content/notification.xml +++ /dev/null @@ -1,129 +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/. --> - -<!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/components/weave/content/progress.js b/components/weave/content/progress.js deleted file mode 100644 index 101160fa8..000000000 --- a/components/weave/content/progress.js +++ /dev/null @@ -1,71 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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/components/weave/content/progress.xhtml b/components/weave/content/progress.xhtml deleted file mode 100644 index f2233e607..000000000 --- a/components/weave/content/progress.xhtml +++ /dev/null @@ -1,55 +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 % syncProgressDTD - SYSTEM "chrome://weave/locale/syncProgress.dtd"> - %syncProgressDTD; - <!ENTITY % syncSetupDTD - SYSTEM "chrome://weave/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://weave/skin/syncProgress.css"/> - - <link rel="icon" type="image/png" id="favicon" - href="chrome://weave/skin/sync-16.png"/> - - <script type="text/javascript;version=1.8" - src="chrome://weave/content/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://weave/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/components/weave/content/quota.js b/components/weave/content/quota.js deleted file mode 100644 index 90b16f909..000000000 --- a/components/weave/content/quota.js +++ /dev/null @@ -1,247 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 Ci = Components.interfaces; -const Cc = Components.classes; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://services-sync/main.js"); -Cu.import("resource://gre/modules/DownloadUtils.jsm"); - -var gSyncQuota = { - - init: function() { - this.bundle = document.getElementById("quotaStrings"); - let caption = document.getElementById("treeCaption"); - caption.firstChild.nodeValue = this.bundle.getString("quota.treeCaption.label"); - - gUsageTreeView.init(); - this.tree = document.getElementById("usageTree"); - this.tree.view = gUsageTreeView; - - this.loadData(); - }, - - loadData: function() { - this._usage_req = Weave.Service.getStorageInfo(Weave.INFO_COLLECTION_USAGE, - function(error, usage) { - delete gSyncQuota._usage_req; - // displayUsageData handles null values, so no need to check 'error'. - gUsageTreeView.displayUsageData(usage); - }); - - let usageLabel = document.getElementById("usageLabel"); - let bundle = this.bundle; - - this._quota_req = Weave.Service.getStorageInfo(Weave.INFO_QUOTA, - function(error, quota) { - delete gSyncQuota._quota_req; - - if (error) { - usageLabel.value = bundle.getString("quota.usageError.label"); - return; - } - let used = gSyncQuota.convertKB(quota[0]); - if (!quota[1]) { - // No quota on the server. - usageLabel.value = bundle.getFormattedString( - "quota.usageNoQuota.label", used); - return; - } - let percent = Math.round(100 * quota[0] / quota[1]); - let total = gSyncQuota.convertKB(quota[1]); - usageLabel.value = bundle.getFormattedString( - "quota.usagePercentage.label", [percent].concat(used).concat(total)); - }); - }, - - onCancel: function() { - if (this._usage_req) { - this._usage_req.abort(); - } - if (this._quota_req) { - this._quota_req.abort(); - } - return true; - }, - - onAccept: function() { - let engines = gUsageTreeView.getEnginesToDisable(); - for each (let engine in engines) { - Weave.Service.engineManager.get(engine).enabled = false; - } - if (engines.length) { - // The 'Weave' object will disappear once the window closes. - let Service = Weave.Service; - Weave.Utils.nextTick(function() { Service.sync(); }); - } - return this.onCancel(); - }, - - convertKB: function(value) { - return DownloadUtils.convertByteUnits(value * 1024); - } - -}; - -var gUsageTreeView = { - - _ignored: {keys: true, - meta: true, - clients: true}, - - /* - * Internal data structures underlaying the tree. - */ - _collections: [], - _byname: {}, - - init: function() { - let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label"); - for each (let engine in Weave.Service.engineManager.getEnabled()) { - if (this._ignored[engine.name]) - continue; - - // Some engines use the same pref, which means they can only be turned on - // and off together. We need to combine them here as well. - let existing = this._byname[engine.prefName]; - if (existing) { - existing.engines.push(engine.name); - continue; - } - - let obj = {name: engine.prefName, - title: this._collectionTitle(engine), - engines: [engine.name], - enabled: true, - sizeLabel: retrievingLabel}; - this._collections.push(obj); - this._byname[engine.prefName] = obj; - } - }, - - _collectionTitle: function(engine) { - try { - return gSyncQuota.bundle.getString( - "collection." + engine.prefName + ".label"); - } catch (ex) { - return engine.Name; - } - }, - - /* - * Process the quota information as returned by info/collection_usage. - */ - displayUsageData: function(data) { - for each (let coll in this._collections) { - coll.size = 0; - // If we couldn't retrieve any data, just blank out the label. - if (!data) { - coll.sizeLabel = ""; - continue; - } - - for each (let engineName in coll.engines) - coll.size += data[engineName] || 0; - let sizeLabel = ""; - sizeLabel = gSyncQuota.bundle.getFormattedString( - "quota.sizeValueUnit.label", gSyncQuota.convertKB(coll.size)); - coll.sizeLabel = sizeLabel; - } - let sizeColumn = this.treeBox.columns.getNamedColumn("size"); - this.treeBox.invalidateColumn(sizeColumn); - }, - - /* - * Handle click events on the tree. - */ - onTreeClick: function(event) { - if (event.button == 2) - return; - - let cell = this.treeBox.getCellAt(event.clientX, event.clientY); - if (cell.col && cell.col.id == "enabled") - this.toggle(cell.row); - }, - - /* - * Toggle enabled state of an engine. - */ - toggle: function(row) { - // Update the tree - let collection = this._collections[row]; - collection.enabled = !collection.enabled; - this.treeBox.invalidateRow(row); - }, - - /* - * Return a list of engines (or rather their pref names) that should be - * disabled. - */ - getEnginesToDisable: function() { - // Tycho: return [coll.name for each (coll in this._collections) if (!coll.enabled)]; - let engines = []; - for each (let coll in this._collections) { - if (!coll.enabled) { - engines.push(coll.name); - } - } - return engines; - }, - - // nsITreeView - - get rowCount() { - return this._collections.length; - }, - - getRowProperties: function(index) { return ""; }, - getCellProperties: function(row, col) { return ""; }, - getColumnProperties: function(col) { return ""; }, - isContainer: function(index) { return false; }, - isContainerOpen: function(index) { return false; }, - isContainerEmpty: function(index) { return false; }, - isSeparator: function(index) { return false; }, - isSorted: function() { return false; }, - canDrop: function(index, orientation, dataTransfer) { return false; }, - drop: function(row, orientation, dataTransfer) {}, - getParentIndex: function(rowIndex) {}, - hasNextSibling: function(rowIndex, afterIndex) { return false; }, - getLevel: function(index) { return 0; }, - getImageSrc: function(row, col) {}, - - getCellValue: function(row, col) { - return this._collections[row].enabled; - }, - - getCellText: function(row, col) { - let collection = this._collections[row]; - switch (col.id) { - case "collection": - return collection.title; - case "size": - return collection.sizeLabel; - default: - return ""; - } - }, - - setTree: function(tree) { - this.treeBox = tree; - }, - - toggleOpenState: function(index) {}, - cycleHeader: function(col) {}, - selectionChanged: function() {}, - cycleCell: function(row, col) {}, - isEditable: function(row, col) { return false; }, - isSelectable: function(row, col) { return false; }, - setCellValue: function(row, col, value) {}, - setCellText: function(row, col, value) {}, - performAction: function(action) {}, - performActionOnRow: function(action, row) {}, - performActionOnCell: function(action, row, col) {} - -}; diff --git a/components/weave/content/quota.xul b/components/weave/content/quota.xul deleted file mode 100644 index 070736882..000000000 --- a/components/weave/content/quota.xul +++ /dev/null @@ -1,65 +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://weave/skin/syncQuota.css"?> - -<!DOCTYPE dialog [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> -<!ENTITY % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> -<!ENTITY % syncQuotaDTD SYSTEM "chrome://weave/locale/syncQuota.dtd"> -%brandDTD; -%syncBrandDTD; -%syncQuotaDTD; -]> -<dialog id="quotaDialog" - windowtype="Sync:ViewQuota" - persist="screenX screenY width height" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - onload="gSyncQuota.init()" - buttons="accept,cancel" - title=""a.dialogTitle.label;" - ondialogcancel="return gSyncQuota.onCancel();" - ondialogaccept="return gSyncQuota.onAccept();"> - - <script type="application/javascript" - src="chrome://weave/content/quota.js"/> - - <stringbundleset id="stringbundleset"> - <stringbundle id="quotaStrings" - src="chrome://weave/locale/syncQuota.properties"/> - </stringbundleset> - - <vbox flex="1"> - <label id="usageLabel" - value=""a.retrievingInfo.label;"/> - <separator/> - <tree id="usageTree" - seltype="single" - hidecolumnpicker="true" - onclick="gUsageTreeView.onTreeClick(event);" - flex="1"> - <treecols> - <treecol id="enabled" - type="checkbox" - fixed="true"/> - <splitter class="tree-splitter"/> - <treecol id="collection" - label=""a.typeColumn.label;" - flex="1"/> - <splitter class="tree-splitter"/> - <treecol id="size" - label=""a.sizeColumn.label;" - flex="1"/> - </treecols> - <treechildren flex="1"/> - </tree> - <separator/> - <description id="treeCaption"> </description> - </vbox> - -</dialog> diff --git a/components/weave/content/setup.js b/components/weave/content/setup.js deleted file mode 100644 index c44f2bed9..000000000 --- a/components/weave/content/setup.js +++ /dev/null @@ -1,1071 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 Ci = Components.interfaces; -var Cc = Components.classes; -var Cr = Components.results; -var Cu = Components.utils; - -// page consts - -const PAIR_PAGE = 0; -const INTRO_PAGE = 1; -const NEW_ACCOUNT_START_PAGE = 2; -const EXISTING_ACCOUNT_CONNECT_PAGE = 3; -const EXISTING_ACCOUNT_LOGIN_PAGE = 4; -const OPTIONS_PAGE = 5; -const OPTIONS_CONFIRM_PAGE = 6; - -// Broader than we'd like, but after this changed from api-secure.recaptcha.net -// we had no choice. At least we only do this for the duration of setup. -// See discussion in Bugs 508112 and 653307. -const RECAPTCHA_DOMAIN = "https://www.google.com"; - -const PIN_PART_LENGTH = 4; - -Cu.import("resource://services-sync/main.js"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/PlacesUtils.jsm"); -Cu.import("resource://gre/modules/PluralForm.jsm"); - - -function setVisibility(element, visible) { - element.style.visibility = visible ? "visible" : "hidden"; -} - -var gSyncSetup = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, - Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]), - - captchaBrowser: null, - wizard: null, - _disabledSites: [], - - status: { - password: false, - email: false, - server: false - }, - - get _remoteSites() [Weave.Service.serverURL, RECAPTCHA_DOMAIN], - - get _usingMainServers() { - if (this._settingUpNew) - return document.getElementById("server").selectedIndex == 0; - return document.getElementById("existingServer").selectedIndex == 0; - }, - - init: function() { - let obs = [ - ["weave:service:change-passphrase", "onResetPassphrase"], - ["weave:service:login:start", "onLoginStart"], - ["weave:service:login:error", "onLoginEnd"], - ["weave:service:login:finish", "onLoginEnd"]]; - - // Add the observers now and remove them on unload - 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 - // of `this`. Fix in a followup. (bug 583347) - if (add) - Weave.Svc.Obs.add(topic, self[func], self); - else - Weave.Svc.Obs.remove(topic, self[func], self); - }); - }; - addRem(true); - window.addEventListener("unload", function() addRem(false), false); - - window.setTimeout(function() { - // Force Service to be loaded so that engines are registered. - // See Bug 670082. - Weave.Service; - }, 0); - - this.captchaBrowser = document.getElementById("captcha"); - - this.wizardType = null; - if (window.arguments && window.arguments[0]) { - this.wizardType = window.arguments[0]; - } - switch (this.wizardType) { - case null: - this.wizard.pageIndex = INTRO_PAGE; - // Fall through! - case "pair": - this.captchaBrowser.addProgressListener(this); - Weave.Svc.Prefs.set("firstSync", "notReady"); - break; - case "reset": - this._resettingSync = true; - this.wizard.pageIndex = OPTIONS_PAGE; - break; - } - - this.wizard.getButton("extra1").label = - this._stringBundle.GetStringFromName("button.syncOptions.label"); - - // Remember these values because the options pages change them temporarily. - this._nextButtonLabel = this.wizard.getButton("next").label; - this._nextButtonAccesskey = this.wizard.getButton("next") - .getAttribute("accesskey"); - this._backButtonLabel = this.wizard.getButton("back").label; - this._backButtonAccesskey = this.wizard.getButton("back") - .getAttribute("accesskey"); - }, - - startNewAccountSetup: function() { - if (!Weave.Utils.ensureMPUnlocked()) - return false; - this._settingUpNew = true; - this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE; - }, - - useExistingAccount: function() { - if (!Weave.Utils.ensureMPUnlocked()) - return false; - this._settingUpNew = false; - if (this.wizardType == "pair") { - // We're already pairing, so there's no point in pairing again. - // Go straight to the manual login page. - this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; - } else { - this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE; - } - }, - - resetPassphrase: function() { - // Apply the existing form fields so that - // Weave.Service.changePassphrase() has the necessary credentials. - Weave.Service.identity.account = document.getElementById("existingAccountName").value; - Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value; - - // Generate a new passphrase so that Weave.Service.login() will - // actually do something. - let passphrase = Weave.Utils.generatePassphrase(); - Weave.Service.identity.syncKey = passphrase; - - // Only open the dialog if username + password are actually correct. - Weave.Service.login(); - if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE, - Weave.LOGIN_FAILED_NO_PASSPHRASE, - Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) { - return; - } - - // Hide any errors about the passphrase, we know it's not right. - let feedback = document.getElementById("existingPassphraseFeedbackRow"); - feedback.hidden = true; - let el = document.getElementById("existingPassphrase"); - el.value = Weave.Utils.hyphenatePassphrase(passphrase); - - // changePassphrase() will sync, make sure we set the "firstSync" pref - // according to the user's pref. - Weave.Svc.Prefs.reset("firstSync"); - this.setupInitialSync(); - gSyncUtils.resetPassphrase(true); - }, - - onResetPassphrase: function() { - document.getElementById("existingPassphrase").value = - Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey); - this.checkFields(); - this.wizard.advance(); - }, - - onLoginStart: function() { - this.toggleLoginFeedback(false); - }, - - onLoginEnd: function() { - this.toggleLoginFeedback(true); - }, - - sendCredentialsAfterSync: function() { - let send = function() { - Services.obs.removeObserver("weave:service:sync:finish", send); - Services.obs.removeObserver("weave:service:sync:error", send); - let credentials = {account: Weave.Service.identity.account, - password: Weave.Service.identity.basicPassword, - synckey: Weave.Service.identity.syncKey, - serverURL: Weave.Service.serverURL}; - this._jpakeclient.sendAndComplete(credentials); - }.bind(this); - Services.obs.addObserver("weave:service:sync:finish", send, false); - Services.obs.addObserver("weave:service:sync:error", send, false); - }, - - toggleLoginFeedback: function(stop) { - document.getElementById("login-throbber").hidden = stop; - let password = document.getElementById("existingPasswordFeedbackRow"); - let server = document.getElementById("existingServerFeedbackRow"); - let passphrase = document.getElementById("existingPassphraseFeedbackRow"); - - if (!stop || (Weave.Status.login == Weave.LOGIN_SUCCEEDED)) { - password.hidden = server.hidden = passphrase.hidden = true; - return; - } - - let feedback; - switch (Weave.Status.login) { - case Weave.LOGIN_FAILED_NETWORK_ERROR: - case Weave.LOGIN_FAILED_SERVER_ERROR: - feedback = server; - break; - case Weave.LOGIN_FAILED_LOGIN_REJECTED: - case Weave.LOGIN_FAILED_NO_USERNAME: - case Weave.LOGIN_FAILED_NO_PASSWORD: - feedback = password; - break; - case Weave.LOGIN_FAILED_INVALID_PASSPHRASE: - feedback = passphrase; - break; - } - this._setFeedbackMessage(feedback, false, Weave.Status.login); - }, - - setupInitialSync: function() { - let action = document.getElementById("mergeChoiceRadio").selectedItem.id; - switch (action) { - case "resetClient": - // if we're not resetting sync, we don't need to explicitly - // call resetClient - if (!this._resettingSync) - return; - // otherwise, fall through - case "wipeClient": - case "wipeRemote": - Weave.Svc.Prefs.set("firstSync", action); - break; - } - }, - - // fun with validation! - checkFields: function() { - this.wizard.canAdvance = this.readyToAdvance(); - }, - - readyToAdvance: function() { - switch (this.wizard.pageIndex) { - case INTRO_PAGE: - return false; - case NEW_ACCOUNT_START_PAGE: - for (let i in this.status) { - if (!this.status[i]) - return false; - } - if (this._usingMainServers) - return document.getElementById("tos").checked; - - return true; - case EXISTING_ACCOUNT_LOGIN_PAGE: - let hasUser = document.getElementById("existingAccountName").value != ""; - let hasPass = document.getElementById("existingPassword").value != ""; - let hasKey = document.getElementById("existingPassphrase").value != ""; - - if (hasUser && hasPass && hasKey) { - if (this._usingMainServers) - return true; - - if (this._validateServer(document.getElementById("existingServer"))) { - return true; - } - } - return false; - } - // Default, e.g. wizard's special page -1 etc. - return true; - }, - - onPINInput: function onPINInput(textbox) { - if (textbox && textbox.value.length == PIN_PART_LENGTH) { - this.nextFocusEl[textbox.id].focus(); - } - this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH && - this.pin2.value.length == PIN_PART_LENGTH && - this.pin3.value.length == PIN_PART_LENGTH); - }, - - onEmailInput: function() { - // Check account validity when the user stops typing for 1 second. - if (this._checkAccountTimer) - window.clearTimeout(this._checkAccountTimer); - this._checkAccountTimer = window.setTimeout(function() { - gSyncSetup.checkAccount(); - }, 1000); - }, - - checkAccount: function() { - delete this._checkAccountTimer; - let value = Weave.Utils.normalizeAccount( - document.getElementById("weaveEmail").value); - if (!value) { - this.status.email = false; - this.checkFields(); - return; - } - - let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - let feedback = document.getElementById("emailFeedbackRow"); - let valid = re.test(value); - - let str = ""; - if (!valid) { - str = "invalidEmail.label"; - } else { - let availCheck = Weave.Service.checkAccount(value); - valid = availCheck == "available"; - if (!valid) { - if (availCheck == "notAvailable") - str = "usernameNotAvailable.label"; - else - str = availCheck; - } - } - - this._setFeedbackMessage(feedback, valid, str); - this.status.email = valid; - if (valid) - Weave.Service.identity.account = value; - this.checkFields(); - }, - - onPasswordChange: function() { - let password = document.getElementById("weavePassword"); - let pwconfirm = document.getElementById("weavePasswordConfirm"); - let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm); - - let feedback = document.getElementById("passwordFeedbackRow"); - this._setFeedback(feedback, valid, errorString); - - this.status.password = valid; - this.checkFields(); - }, - - onPageShow: function() { - switch (this.wizard.pageIndex) { - case PAIR_PAGE: - this.wizard.getButton("back").hidden = true; - this.wizard.getButton("extra1").hidden = true; - this.onPINInput(); - this.pin1.focus(); - break; - case INTRO_PAGE: - // We may not need the captcha in the Existing Account branch of the - // wizard. However, we want to preload it to avoid any flickering while - // the Create Account page is shown. - this.loadCaptcha(); - this.wizard.getButton("next").hidden = true; - this.wizard.getButton("back").hidden = true; - this.wizard.getButton("extra1").hidden = true; - this.checkFields(); - break; - case NEW_ACCOUNT_START_PAGE: - this.wizard.getButton("extra1").hidden = false; - this.wizard.getButton("next").hidden = false; - this.wizard.getButton("back").hidden = false; - this.onServerCommand(); - this.wizard.canRewind = true; - this.checkFields(); - break; - case EXISTING_ACCOUNT_CONNECT_PAGE: - Weave.Svc.Prefs.set("firstSync", "existingAccount"); - this.wizard.getButton("next").hidden = false; - this.wizard.getButton("back").hidden = false; - this.wizard.getButton("extra1").hidden = false; - this.wizard.canAdvance = false; - this.wizard.canRewind = true; - this.startEasySetup(); - break; - case EXISTING_ACCOUNT_LOGIN_PAGE: - this.wizard.getButton("next").hidden = false; - this.wizard.getButton("back").hidden = false; - this.wizard.getButton("extra1").hidden = false; - this.wizard.canRewind = true; - this.checkFields(); - break; - case OPTIONS_PAGE: - this.wizard.canRewind = false; - this.wizard.canAdvance = true; - if (!this._resettingSync) { - this.wizard.getButton("next").label = - this._stringBundle.GetStringFromName("button.syncOptionsDone.label"); - this.wizard.getButton("next").removeAttribute("accesskey"); - } - this.wizard.getButton("next").hidden = false; - this.wizard.getButton("back").hidden = true; - this.wizard.getButton("cancel").hidden = !this._resettingSync; - this.wizard.getButton("extra1").hidden = true; - document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; - document.getElementById("syncOptions").collapsed = this._resettingSync; - document.getElementById("mergeOptions").collapsed = this._settingUpNew; - break; - case OPTIONS_CONFIRM_PAGE: - this.wizard.canRewind = true; - this.wizard.canAdvance = true; - this.wizard.getButton("back").label = - this._stringBundle.GetStringFromName("button.syncOptionsCancel.label"); - this.wizard.getButton("back").removeAttribute("accesskey"); - this.wizard.getButton("back").hidden = this._resettingSync; - this.wizard.getButton("next").hidden = false; - this.wizard.getButton("finish").hidden = true; - break; - } - }, - - onWizardAdvance: function() { - // Check pageIndex so we don't prompt before the Sync setup wizard appears. - // This is a fallback in case the Master Password gets locked mid-wizard. - if ((this.wizard.pageIndex >= 0) && - !Weave.Utils.ensureMPUnlocked()) { - return false; - } - - switch (this.wizard.pageIndex) { - case PAIR_PAGE: - this.startPairing(); - return false; - case NEW_ACCOUNT_START_PAGE: - // If the user selects Next (e.g. by hitting enter) when we haven't - // executed the delayed checks yet, execute them immediately. - if (this._checkAccountTimer) { - this.checkAccount(); - } - if (this._checkServerTimer) { - this.checkServer(); - } - if (!this.wizard.canAdvance) { - return false; - } - - let doc = this.captchaBrowser.contentDocument; - let getField = function getField(field) { - let node = doc.getElementById("recaptcha_" + field + "_field"); - return node && node.value; - }; - - // Display throbber - let feedback = document.getElementById("captchaFeedback"); - let image = feedback.firstChild; - let label = image.nextSibling; - image.setAttribute("status", "active"); - label.value = this._stringBundle.GetStringFromName("verifying.label"); - setVisibility(feedback, true); - - let password = document.getElementById("weavePassword").value; - let email = Weave.Utils.normalizeAccount( - document.getElementById("weaveEmail").value); - let challenge = getField("challenge"); - let response = getField("response"); - - let error = Weave.Service.createAccount(email, password, - challenge, response); - - if (error == null) { - Weave.Service.identity.account = email; - Weave.Service.identity.basicPassword = password; - Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase(); - this._handleNoScript(false); - Weave.Svc.Prefs.set("firstSync", "newAccount"); - this.wizardFinish(); - return false; - } - - image.setAttribute("status", "error"); - label.value = Weave.Utils.getErrorString(error); - return false; - case EXISTING_ACCOUNT_LOGIN_PAGE: - Weave.Service.identity.account = Weave.Utils.normalizeAccount( - document.getElementById("existingAccountName").value); - Weave.Service.identity.basicPassword = - document.getElementById("existingPassword").value; - let pp = document.getElementById("existingPassphrase").value; - Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp); - if (Weave.Service.login()) { - this.wizardFinish(); - } - return false; - case OPTIONS_PAGE: - let desc = document.getElementById("mergeChoiceRadio").selectedIndex; - // No confirmation needed on new account setup or merge option - // with existing account. - if (this._settingUpNew || (!this._resettingSync && desc == 0)) - return this.returnFromOptions(); - return this._handleChoice(); - case OPTIONS_CONFIRM_PAGE: - if (this._resettingSync) { - this.wizardFinish(); - return false; - } - return this.returnFromOptions(); - } - return true; - }, - - 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: - this.abortEasySetup(); - this.wizard.pageIndex = INTRO_PAGE; - return false; - case EXISTING_ACCOUNT_LOGIN_PAGE: - // If we were already pairing on entry, we went straight to the manual - // login page. If subsequently we go back, return to the page that lets - // us choose whether we already have an account. - if (this.wizardType == "pair") { - this.wizard.pageIndex = INTRO_PAGE; - return false; - } - return true; - case OPTIONS_CONFIRM_PAGE: - // Backing up from the confirmation page = resetting first sync to merge. - document.getElementById("mergeChoiceRadio").selectedIndex = 0; - return this.returnFromOptions(); - } - return true; - }, - - wizardFinish: function() { - this.setupInitialSync(); - - if (this.wizardType == "pair") { - this.completePairing(); - } - - if (!this._resettingSync) { - function isChecked(element) { - return document.getElementById(element).hasAttribute("checked"); - } - - let prefs = ["engine.bookmarks", "engine.passwords", "engine.history", - "engine.tabs", "engine.prefs", "engine.addons"]; - 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(); - }, - - onWizardCancel: function() { - if (this._resettingSync) - return; - - this.abortEasySetup(); - this._handleNoScript(false); - Weave.Service.startOver(); - }, - - onSyncOptions: function() { - this._beforeOptionsPage = this.wizard.pageIndex; - this.wizard.pageIndex = OPTIONS_PAGE; - }, - - returnFromOptions: function() { - this.wizard.getButton("next").label = this._nextButtonLabel; - this.wizard.getButton("next").setAttribute("accesskey", - this._nextButtonAccesskey); - this.wizard.getButton("back").label = this._backButtonLabel; - this.wizard.getButton("back").setAttribute("accesskey", - this._backButtonAccesskey); - this.wizard.getButton("cancel").hidden = false; - this.wizard.getButton("extra1").hidden = false; - this.wizard.pageIndex = this._beforeOptionsPage; - return false; - }, - - startPairing: function() { - this.pairDeviceErrorRow.hidden = true; - // When onAbort is called, Weave may already be gone. - const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; - - let self = this; - let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({ - onPaired: function onPaired() { - self.wizard.pageIndex = INTRO_PAGE; - }, - onComplete: function() { - // This method will never be called since SendCredentialsController - // will take over after the wizard completes. - }, - onAbort: function(error) { - delete self._jpakeclient; - - // Aborted by user, ignore. The window is almost certainly going to close - // or is already closed. - if (error == JPAKE_ERROR_USERABORT) { - return; - } - - self.pairDeviceErrorRow.hidden = false; - self.pairDeviceThrobber.hidden = true; - self.pin1.value = self.pin2.value = self.pin3.value = ""; - self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false; - if (self.wizard.pageIndex == PAIR_PAGE) { - self.pin1.focus(); - } - } - }); - this.pairDeviceThrobber.hidden = false; - this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true; - this.wizard.canAdvance = false; - - let pin = this.pin1.value + this.pin2.value + this.pin3.value; - let expectDelay = true; - jpakeclient.pairWithPIN(pin, expectDelay); - }, - - completePairing: function() { - if (!this._jpakeclient) { - // The channel was aborted while we were setting up the account - // locally. XXX TODO should we do anything here, e.g. tell - // the user on the last wizard page that it's ok, they just - // have to pair again? - return; - } - let controller = new Weave.SendCredentialsController(this._jpakeclient, - Weave.Service); - this._jpakeclient.controller = controller; - }, - - startEasySetup: function() { - // Don't do anything if we have a client already (e.g. we went to - // Sync Options and just came back). - if (this._jpakeclient) - return; - - // When onAbort is called, Weave may already be gone - const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; - - let self = this; - this._jpakeclient = new Weave.JPAKEClient({ - displayPIN: function(pin) { - document.getElementById("easySetupPIN1").value = pin.slice(0, 4); - document.getElementById("easySetupPIN2").value = pin.slice(4, 8); - document.getElementById("easySetupPIN3").value = pin.slice(8); - }, - - onPairingStart: function() {}, - - onComplete: function(credentials) { - Weave.Service.identity.account = credentials.account; - Weave.Service.identity.basicPassword = credentials.password; - Weave.Service.identity.syncKey = credentials.synckey; - Weave.Service.serverURL = credentials.serverURL; - gSyncSetup.wizardFinish(); - }, - - onAbort: function(error) { - delete self._jpakeclient; - - // Ignore if wizard is aborted. - if (error == JPAKE_ERROR_USERABORT) - return; - - // Automatically go to manual setup if we couldn't acquire a channel. - if (error == Weave.JPAKE_ERROR_CHANNEL) { - self.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; - return; - } - - // Restart on all other errors. - self.startEasySetup(); - } - }); - this._jpakeclient.receiveNoPIN(); - }, - - abortEasySetup: function() { - document.getElementById("easySetupPIN1").value = ""; - document.getElementById("easySetupPIN2").value = ""; - document.getElementById("easySetupPIN3").value = ""; - if (!this._jpakeclient) - return; - - this._jpakeclient.abort(); - delete this._jpakeclient; - }, - - manualSetup: function() { - this.abortEasySetup(); - this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; - }, - - // _handleNoScript is needed because it blocks the captcha. So we temporarily - // allow the necessary sites so that we can verify the user is in fact a human. - // This was done with the help of Giorgio (NoScript author). See bug 508112. - _handleNoScript: function(addExceptions) { - // if NoScript isn't installed, or is disabled, bail out. - let ns = Cc["@maone.net/noscript-service;1"]; - if (ns == null) - return; - - ns = ns.getService().wrappedJSObject; - if (addExceptions) { - this._remoteSites.forEach(function(site) { - site = ns.getSite(site); - if (!ns.isJSEnabled(site)) { - this._disabledSites.push(site); // save status - ns.setJSEnabled(site, true); // allow site - } - }, this); - } - else { - this._disabledSites.forEach(function(site) { - ns.setJSEnabled(site, false); - }); - this._disabledSites = []; - } - }, - - onExistingServerCommand: function() { - let control = document.getElementById("existingServer"); - if (control.selectedIndex == 0) { - control.removeAttribute("editable"); - Weave.Svc.Prefs.reset("serverURL"); - } else { - control.setAttribute("editable", "true"); - // Force a style flush to ensure that the binding is attached. - control.clientTop; - control.value = ""; - control.inputField.focus(); - } - document.getElementById("existingServerFeedbackRow").hidden = true; - this.checkFields(); - }, - - onExistingServerInput: function() { - // Check custom server validity when the user stops typing for 1 second. - if (this._existingServerTimer) - window.clearTimeout(this._existingServerTimer); - this._existingServerTimer = window.setTimeout(function() { - gSyncSetup.checkFields(); - }, 1000); - }, - - onServerCommand: function() { - setVisibility(document.getElementById("TOSRow"), this._usingMainServers); - let control = document.getElementById("server"); - if (!this._usingMainServers) { - control.setAttribute("editable", "true"); - // Force a style flush to ensure that the binding is attached. - control.clientTop; - control.value = ""; - control.inputField.focus(); - // checkServer() will call checkAccount() and checkFields(). - this.checkServer(); - return; - } - control.removeAttribute("editable"); - Weave.Svc.Prefs.reset("serverURL"); - if (this._settingUpNew) { - this.loadCaptcha(); - } - this.checkAccount(); - this.status.server = true; - document.getElementById("serverFeedbackRow").hidden = true; - this.checkFields(); - }, - - onServerInput: function() { - // Check custom server validity when the user stops typing for 1 second. - if (this._checkServerTimer) - window.clearTimeout(this._checkServerTimer); - this._checkServerTimer = window.setTimeout(function() { - gSyncSetup.checkServer(); - }, 1000); - }, - - checkServer: function() { - delete this._checkServerTimer; - 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"; - this._setFeedbackMessage(feedback, valid, str); - } - else - this._setFeedbackMessage(feedback, true); - - // Recheck account against the new server. - if (valid) - this.checkAccount(); - - this.status.server = valid; - this.checkFields(); - }, - - _validateServer: function(element) { - let valid = false; - let val = element.value; - if (!val) - return false; - - let uri = Weave.Utils.makeURI(val); - - if (!uri) - uri = Weave.Utils.makeURI("https://" + val); - - if (uri && this._settingUpNew) { - function isValid(uri) { - Weave.Service.serverURL = uri.spec; - let check = Weave.Service.checkAccount("a"); - return (check == "available" || check == "notAvailable"); - } - - if (uri.schemeIs("http")) { - uri.scheme = "https"; - if (isValid(uri)) - valid = true; - else - // setting the scheme back to http - uri.scheme = "http"; - } - if (!valid) - valid = isValid(uri); - - if (valid) { - this.loadCaptcha(); - } - } - else if (uri) { - valid = true; - Weave.Service.serverURL = uri.spec; - } - - if (valid) - element.value = Weave.Service.serverURL; - else - Weave.Svc.Prefs.reset("serverURL"); - - return valid; - }, - - _handleChoice: function() { - let desc = document.getElementById("mergeChoiceRadio").selectedIndex; - document.getElementById("chosenActionDeck").selectedIndex = desc; - switch (desc) { - case 1: - if (this._case1Setup) - break; - - let places_db = PlacesUtils.history - .QueryInterface(Ci.nsPIPlacesDatabase) - .DBConnection; - if (Weave.Service.engineManager.get("history").enabled) { - let daysOfHistory = 0; - let stm = places_db.createStatement( - "SELECT ROUND(( " + - "strftime('%s','now','localtime','utc') - " + - "( " + - "SELECT visit_date FROM moz_historyvisits " + - "ORDER BY visit_date ASC LIMIT 1 " + - ")/1000000 " + - ")/86400) AS daysOfHistory "); - - if (stm.step()) - daysOfHistory = stm.getInt32(0); - // Support %S for historical reasons (see bug 600141) - document.getElementById("historyCount").value = - PluralForm.get(daysOfHistory, - this._stringBundle.GetStringFromName("historyDaysCount.label")) - .replace("%S", daysOfHistory) - .replace("#1", daysOfHistory); - } else { - document.getElementById("historyCount").hidden = true; - } - - if (Weave.Service.engineManager.get("bookmarks").enabled) { - let bookmarks = 0; - let stm = places_db.createStatement( - "SELECT count(*) AS bookmarks " + - "FROM moz_bookmarks b " + - "LEFT JOIN moz_bookmarks t ON " + - "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag"); - stm.params.tag = PlacesUtils.tagsFolderId; - if (stm.executeStep()) - bookmarks = stm.row.bookmarks; - // Support %S for historical reasons (see bug 600141) - document.getElementById("bookmarkCount").value = - PluralForm.get(bookmarks, - this._stringBundle.GetStringFromName("bookmarksCount.label")) - .replace("%S", bookmarks) - .replace("#1", bookmarks); - } else { - document.getElementById("bookmarkCount").hidden = true; - } - - if (Weave.Service.engineManager.get("passwords").enabled) { - let logins = Services.logins.getAllLogins({}); - // Support %S for historical reasons (see bug 600141) - document.getElementById("passwordCount").value = - PluralForm.get(logins.length, - this._stringBundle.GetStringFromName("passwordsCount.label")) - .replace("%S", logins.length) - .replace("#1", logins.length); - } else { - document.getElementById("passwordCount").hidden = true; - } - - if (!Weave.Service.engineManager.get("prefs").enabled) { - document.getElementById("prefsWipe").hidden = true; - } - - let addonsEngine = Weave.Service.engineManager.get("addons"); - if (addonsEngine.enabled) { - let ids = addonsEngine._store.getAllIDs(); - 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, - this._stringBundle.GetStringFromName("addonsCount.label")) - .replace("#1", blessedcount); - } else { - document.getElementById("addonCount").hidden = true; - } - - this._case1Setup = true; - break; - case 2: - if (this._case2Setup) - break; - let count = 0; - function appendNode(label) { - let box = document.getElementById("clientList"); - let node = document.createElement("label"); - node.setAttribute("value", label); - node.setAttribute("class", "data indent"); - box.appendChild(node); - } - - for each (let name in Weave.Service.clientsEngine.stats.names) { - // Don't list the current client - if (name == Weave.Service.clientsEngine.localName) - continue; - - // Only show the first several client names - if (++count <= 5) - appendNode(name); - } - if (count > 5) { - // Support %S for historical reasons (see bug 600141) - let label = - PluralForm.get(count - 5, - this._stringBundle.GetStringFromName("additionalClientCount.label")) - .replace("%S", count - 5) - .replace("#1", count - 5); - appendNode(label); - } - this._case2Setup = true; - break; - } - - return true; - }, - - // sets class and string on a feedback element - // if no property string is passed in, we clear label/style - _setFeedback: function(element, success, string) { - element.hidden = success || !string; - let classname = success ? "success" : "error"; - let image = element.getElementsByAttribute("class", "statusIcon")[0]; - image.setAttribute("status", classname); - let label = element.getElementsByAttribute("class", "status")[0]; - label.value = string; - }, - - // shim - _setFeedbackMessage: function(element, success, string) { - let str = ""; - if (string) { - try { - str = this._stringBundle.GetStringFromName(string); - } catch(e) {} - - if (!str) - str = Weave.Utils.getErrorString(string); - } - this._setFeedback(element, success, str); - }, - - loadCaptcha: function() { - let captchaURI = Weave.Service.miscAPI + "captcha_html"; - // First check for NoScript and whitelist the right sites. - this._handleNoScript(true); - if (this.captchaBrowser.currentURI.spec != captchaURI) { - this.captchaBrowser.loadURI(captchaURI); - } - }, - - onStateChange: function(webProgress, request, stateFlags, status) { - // We're only looking for the end of the frame load - if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0) - return; - if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0) - return; - if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0) - return; - - // 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 - }, - onProgressChange: function() {}, - onStatusChange: function() {}, - onSecurityChange: function() {}, - onLocationChange: function() {} -}; - -// Define lazy getters for various XUL elements. -// -// onWizardAdvance() and onPageShow() are run before init(), so we'll even -// define things that will almost certainly be used (like 'wizard') as a lazy -// getter here. -["wizard", - "pin1", - "pin2", - "pin3", - "pairDeviceErrorRow", - "pairDeviceThrobber"].forEach(function(id) { - XPCOMUtils.defineLazyGetter(gSyncSetup, id, function() { - return document.getElementById(id); - }); -}); -XPCOMUtils.defineLazyGetter(gSyncSetup, "nextFocusEl", function() { - return {pin1: this.pin2, - pin2: this.pin3, - pin3: this.wizard.getButton("next")}; -}); -XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() { - return Services.strings.createBundle("chrome://weave/locale/syncSetup.properties"); -}); diff --git a/components/weave/content/setup.xul b/components/weave/content/setup.xul deleted file mode 100644 index da7e26ff5..000000000 --- a/components/weave/content/setup.xul +++ /dev/null @@ -1,491 +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://weave/skin/syncSetup.css" type="text/css"?> -<?xml-stylesheet href="chrome://weave/skin/syncCommon.css" type="text/css"?> - -<!DOCTYPE window [ -<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> -<!ENTITY % syncBrandDTD SYSTEM "chrome://branding/locale/syncBrand.dtd"> -<!ENTITY % syncSetupDTD SYSTEM "chrome://weave/locale/syncSetup.dtd"> -%brandDTD; -%syncBrandDTD; -%syncSetupDTD; -]> -<wizard id="wizard" - title="&accountSetupTitle.label;" - windowtype="Weave:AccountSetup" - persist="screenX screenY" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:html="http://www.w3.org/1999/xhtml" - onwizardnext="return gSyncSetup.onWizardAdvance()" - onwizardback="return gSyncSetup.onWizardBack()" - onwizardcancel="gSyncSetup.onWizardCancel()" - onload="gSyncSetup.init()"> - - <script type="application/javascript" - src="chrome://weave/content/setup.js"/> - <script type="application/javascript" - src="chrome://weave/content/utils.js"/> - <script type="application/javascript" - src="chrome://browser/content/utilityOverlay.js"/> - <script type="application/javascript" - src="chrome://global/content/printUtils.js"/> - - <wizardpage id="addDevicePage" - label="&pairDevice.title.label;" - onpageshow="gSyncSetup.onPageShow()"> - <description> - &pairDevice.dialog.description.label; - <label class="text-link" - value="&addDevice.showMeHow.label;" - href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> - </description> - <separator class="groove-thin"/> - <description> - &addDevice.dialog.enterCode.label; - </description> - <separator class="groove-thin"/> - <vbox align="center"> - <textbox id="pin1" - class="pin" - oninput="gSyncSetup.onPINInput(this);" - onfocus="this.select();" - /> - <textbox id="pin2" - class="pin" - oninput="gSyncSetup.onPINInput(this);" - onfocus="this.select();" - /> - <textbox id="pin3" - class="pin" - oninput="gSyncSetup.onPINInput(this);" - onfocus="this.select();" - /> - </vbox> - <separator class="groove-thin"/> - <vbox id="pairDeviceThrobber" align="center" hidden="true"> - <image/> - </vbox> - <hbox id="pairDeviceErrorRow" pack="center" hidden="true"> - <image class="statusIcon" status="error"/> - <label class="status" - value="&addDevice.dialog.tryAgain.label;"/> - </hbox> - </wizardpage> - - <wizardpage id="pickSetupType" - label="&syncBrand.fullName.label;" - onpageshow="gSyncSetup.onPageShow()"> - <vbox align="center" flex="1"> - <description style="padding: 0 7em;"> - &setup.pickSetupType.description2; - </description> - <spacer flex="3"/> - <button id="newAccount" - class="accountChoiceButton" - label="&button.createNewAccount.label;" - oncommand="gSyncSetup.startNewAccountSetup()" - align="center"/> - <spacer flex="1"/> - </vbox> - <separator class="groove"/> - <vbox align="center" flex="1"> - <spacer flex="1"/> - <button id="existingAccount" - class="accountChoiceButton" - label="&button.haveAccount.label;" - oncommand="gSyncSetup.useExistingAccount()"/> - <spacer flex="3"/> - </vbox> - </wizardpage> - - <wizardpage label="&setup.newAccountDetailsPage.title.label;" - id="newAccountStart" - onextra1="gSyncSetup.onSyncOptions()" - onpageshow="gSyncSetup.onPageShow();"> - <grid> - <columns> - <column/> - <column class="inputColumn" flex="1"/> - </columns> - <rows> - <row id="emailRow" align="center"> - <label value="&setup.emailAddress.label;" - accesskey="&setup.emailAddress.accesskey;" - control="weaveEmail"/> - <textbox id="weaveEmail" - oninput="gSyncSetup.onEmailInput()"/> - </row> - <row id="emailFeedbackRow" align="center" hidden="true"> - <spacer/> - <hbox> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </row> - <row id="passwordRow" align="center"> - <label value="&setup.choosePassword.label;" - accesskey="&setup.choosePassword.accesskey;" - control="weavePassword"/> - <textbox id="weavePassword" - type="password" - onchange="gSyncSetup.onPasswordChange()"/> - </row> - <row id="confirmRow" align="center"> - <label value="&setup.confirmPassword.label;" - accesskey="&setup.confirmPassword.accesskey;" - control="weavePasswordConfirm"/> - <textbox id="weavePasswordConfirm" - type="password" - onchange="gSyncSetup.onPasswordChange()"/> - </row> - <row id="passwordFeedbackRow" align="center" hidden="true"> - <spacer/> - <hbox> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </row> - <row align="center"> - <label control="server" - value="&server.label;"/> - <menulist id="server" - oncommand="gSyncSetup.onServerCommand()" - oninput="gSyncSetup.onServerInput()"> - <menupopup> - <menuitem label="&serverType.default.label;" - value="main"/> - <menuitem label="&serverType.custom2.label;" - value="custom"/> - </menupopup> - </menulist> - </row> - <row id="serverFeedbackRow" align="center" hidden="true"> - <spacer/> - <hbox> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </row> - <row id="TOSRow" align="center"> - <spacer/> - <hbox align="center"> - <checkbox id="tos" - accesskey="&setup.tosAgree1.accesskey;" - oncommand="this.focus(); gSyncSetup.checkFields();"/> - <description id="tosDesc" - flex="1" - onclick="document.getElementById('tos').focus(); - document.getElementById('tos').click()"> - &setup.tosAgree1.label; - <label class="text-link inline-link" - onclick="event.stopPropagation();gSyncUtils.openToS();"> - &setup.tosLink.label; - </label> - &setup.tosAgree2.label; - <label class="text-link inline-link" - onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"> - &setup.ppLink.label; - </label> - &setup.tosAgree3.label; - </description> - </hbox> - </row> - </rows> - </grid> - <spacer flex="1"/> - <vbox flex="1" align="center"> - <browser height="150" - width="500" - id="captcha" - type="content" - disablehistory="true"/> - <spacer flex="1"/> - <hbox id="captchaFeedback"> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </vbox> - </wizardpage> - - <wizardpage id="addDevice" - label="&pairDevice.title.label;" - onextra1="gSyncSetup.onSyncOptions()" - onpageshow="gSyncSetup.onPageShow()"> - <description> - &pairDevice.setup.description.label; - <label class="text-link" - value="&addDevice.showMeHow.label;" - href="http://www.palemoon.org/sync/help/easy-setup.shtml"/> - </description> - <label value="&addDevice.setup.enterCode.label;" - control="easySetupPIN1"/> - <spacer flex="1"/> - <vbox align="center" flex="1"> - <textbox id="easySetupPIN1" - class="pin" - value="" - readonly="true" - /> - <textbox id="easySetupPIN2" - class="pin" - value="" - readonly="true" - /> - <textbox id="easySetupPIN3" - class="pin" - value="" - readonly="true" - /> - </vbox> - <spacer flex="3"/> - <label class="text-link" - value="&addDevice.dontHaveDevice.label;" - onclick="gSyncSetup.manualSetup();"/> - </wizardpage> - - <wizardpage id="existingAccount" - label="&setup.signInPage.title.label;" - onextra1="gSyncSetup.onSyncOptions()" - onpageshow="gSyncSetup.onPageShow()"> - <grid> - <columns> - <column/> - <column class="inputColumn" flex="1"/> - </columns> - <rows> - <row id="existingAccountRow" align="center"> - <label id="existingAccountLabel" - value="&signIn.account2.label;" - accesskey="&signIn.account2.accesskey;" - control="existingAccount"/> - <textbox id="existingAccountName" - oninput="gSyncSetup.checkFields(event)" - onchange="gSyncSetup.checkFields(event)"/> - </row> - <row id="existingPasswordRow" align="center"> - <label id="existingPasswordLabel" - value="&signIn.password.label;" - accesskey="&signIn.password.accesskey;" - control="existingPassword"/> - <textbox id="existingPassword" - type="password" - onkeyup="gSyncSetup.checkFields(event)" - onchange="gSyncSetup.checkFields(event)"/> - </row> - <row id="existingPasswordFeedbackRow" align="center" hidden="true"> - <spacer/> - <hbox> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </row> - <row align="center"> - <spacer/> - <label class="text-link" - value="&resetPassword.label;" - onclick="gSyncUtils.resetPassword(); return false;"/> - </row> - <row align="center"> - <label control="existingServer" - value="&server.label;"/> - <menulist id="existingServer" - oncommand="gSyncSetup.onExistingServerCommand()" - oninput="gSyncSetup.onExistingServerInput()"> - <menupopup> - <menuitem label="&serverType.default.label;" - value="main"/> - <menuitem label="&serverType.custom2.label;" - value="custom"/> - </menupopup> - </menulist> - </row> - <row id="existingServerFeedbackRow" align="center" hidden="true"> - <spacer/> - <hbox> - <image class="statusIcon"/> - <vbox> - <label class="status" value=" "/> - </vbox> - </hbox> - </row> - </rows> - </grid> - - <groupbox> - <label id="existingPassphraseLabel" - value="&signIn.recoveryKey.label;" - accesskey="&signIn.recoveryKey.accesskey;" - control="existingPassphrase"/> - <textbox id="existingPassphrase" - oninput="gSyncSetup.checkFields()"/> - <hbox id="login-throbber" hidden="true"> - <image/> - <label value="&verifying.label;"/> - </hbox> - <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true"> - <hbox> - <image class="statusIcon"/> - <label class="status" value=" "/> - </hbox> - </vbox> - </groupbox> - - <vbox id="passphraseHelpBox"> - <description> - &existingRecoveryKey.description; - <label class="text-link" - href="http://www.palemoon.org/sync/help/recoverykey.shtml"> - &addDevice.showMeHow.label; - </label> - <spacer id="passphraseHelpSpacer"/> - <label class="text-link" - onclick="gSyncSetup.resetPassphrase(); return false;"> - &resetSyncKey.label; - </label> - </description> - </vbox> - </wizardpage> - - <wizardpage id="syncOptionsPage" - label="&setup.optionsPage.title;" - onpageshow="gSyncSetup.onPageShow()"> - <groupbox id="syncOptions"> - <grid> - <columns> - <column/> - <column flex="1" style="-moz-margin-end: 2px"/> - </columns> - <rows> - <row align="center"> - <label value="&syncDeviceName.label;" - accesskey="&syncDeviceName.accesskey;" - control="syncComputerName"/> - <textbox id="syncComputerName" flex="1" - onchange="gSyncUtils.changeName(this)"/> - </row> - <row> - <label value="&syncMy.label;" /> - <vbox> - <checkbox label="&engine.addons.label;" - accesskey="&engine.addons.accesskey;" - id="engine.addons" - checked="false" - hidden="true"/> - <checkbox label="&engine.bookmarks.label;" - accesskey="&engine.bookmarks.accesskey;" - id="engine.bookmarks" - checked="true"/> - <checkbox label="&engine.passwords.label;" - accesskey="&engine.passwords.accesskey;" - id="engine.passwords" - checked="true"/> - <checkbox label="&engine.prefs.label;" - accesskey="&engine.prefs.accesskey;" - id="engine.prefs" - checked="true"/> - <checkbox label="&engine.history.label;" - accesskey="&engine.history.accesskey;" - id="engine.history" - checked="true"/> - <checkbox label="&engine.tabs.label;" - accesskey="&engine.tabs.accesskey;" - id="engine.tabs" - checked="true"/> - </vbox> - </row> - </rows> - </grid> - </groupbox> - - <groupbox id="mergeOptions"> - <radiogroup id="mergeChoiceRadio" pack="start"> - <grid> - <columns> - <column/> - <column flex="1"/> - </columns> - <rows flex="1"> - <row align="center"> - <radio id="resetClient" - class="mergeChoiceButton" - aria-labelledby="resetClientLabel"/> - <label id="resetClientLabel" control="resetClient"> - <html:strong>&choice2.merge.recommended.label;</html:strong> - &choice2a.merge.main.label; - </label> - </row> - <row align="center"> - <radio id="wipeClient" - class="mergeChoiceButton" - aria-labelledby="wipeClientLabel"/> - <label id="wipeClientLabel" - control="wipeClient"> - &choice2a.client.main.label; - </label> - </row> - <row align="center"> - <radio id="wipeRemote" - class="mergeChoiceButton" - aria-labelledby="wipeRemoteLabel"/> - <label id="wipeRemoteLabel" - control="wipeRemote"> - &choice2a.server.main.label; - </label> - </row> - </rows> - </grid> - </radiogroup> - </groupbox> - </wizardpage> - - <wizardpage id="syncOptionsConfirm" - label="&setup.optionsConfirmPage.title;" - onpageshow="gSyncSetup.onPageShow()"> - <deck id="chosenActionDeck"> - <vbox id="chosenActionMerge" class="confirm"> - <description class="normal"> - &confirm.merge2.label; - </description> - </vbox> - <vbox id="chosenActionWipeClient" class="confirm"> - <description class="normal"> - &confirm.client3.label; - </description> - <separator class="thin"/> - <vbox id="dataList"> - <label class="data indent" id="bookmarkCount"/> - <label class="data indent" id="historyCount"/> - <label class="data indent" id="passwordCount"/> - <label class="data indent" id="addonCount"/> - <label class="data indent" id="prefsWipe" - value="&engine.prefs.label;"/> - </vbox> - <separator class="thin"/> - <description class="normal"> - &confirm.client2.moreinfo.label; - </description> - </vbox> - <vbox id="chosenActionWipeServer" class="confirm"> - <description class="normal"> - &confirm.server2.label; - </description> - <separator class="thin"/> - <vbox id="clientList"> - </vbox> - </vbox> - </deck> - </wizardpage> - <!-- In terms of the wizard flow shown to the user, the 'syncOptionsConfirm' - page above is not the last wizard page. To prevent the wizard binding from - assuming that it is, we're inserting this dummy page here. This also means - that the wizard needs to always be closed manually via wizardFinish(). --> - <wizardpage> - </wizardpage> -</wizard> - diff --git a/components/weave/content/utils.js b/components/weave/content/utils.js deleted file mode 100644 index fa5d6a5ac..000000000 --- a/components/weave/content/utils.js +++ /dev/null @@ -1,218 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Equivalent to 0o600 permissions; used for saved Sync Recovery Key. -// This constant can be replaced when the equivalent values are available to -// chrome JS; see Bug 433295 and Bug 757351. -const PERMISSIONS_RWUSR = 0x180; - -// Weave should always exist before before this file gets included. -var gSyncUtils = { - get bundle() { - delete this.bundle; - return this.bundle = Services.strings.createBundle("chrome://weave/locale/syncSetup.properties"); - }, - - // 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, - openerDocEl = window.opener && window.opener.document.documentElement; - if (thisDocEl.id == "accountSetup" && window.opener && - openerDocEl.id == "BrowserPreferences" && !openerDocEl.instantApply) - openUILinkIn(url, "window"); - else if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply) - openUILinkIn(url, "window"); - else if (document.documentElement.id == "change-dialog") - Services.wm.getMostRecentWindow("navigator:browser") - .openUILinkIn(url, "tab"); - else - openUILinkIn(url, "tab"); - }, - - changeName: function(input) { - // Make sure to update to a modified name, e.g., empty-string -> default - Weave.Service.clientsEngine.localName = input.value; - input.value = Weave.Service.clientsEngine.localName; - }, - - openChange: function(type, duringSetup) { - // Just re-show the dialog if it's already open - let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type); - if (openedDialog != null) { - openedDialog.focus(); - return; - } - - // Open up the change dialog - let changeXUL = "chrome://weave/content/genericChange.xul"; - let changeOpt = "centerscreen,chrome,resizable=no"; - Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt, - type, duringSetup); - }, - - changePassword: function() { - if (Weave.Utils.ensureMPUnlocked()) - this.openChange("ChangePassword"); - }, - - resetPassphrase: function(duringSetup) { - if (Weave.Utils.ensureMPUnlocked()) - this.openChange("ResetPassphrase", duringSetup); - }, - - updatePassphrase: function() { - if (Weave.Utils.ensureMPUnlocked()) - this.openChange("UpdatePassphrase"); - }, - - resetPassword: function() { - this._openLink(Weave.Service.pwResetURL); - }, - - openToS: function() { - this._openLink(Weave.Svc.Prefs.get("termsURL")); - }, - - openPrivacyPolicy: function() { - this._openLink(Weave.Svc.Prefs.get("privacyURL")); - }, - - openFirstSyncProgressPage: function() { - this._openLink("about:sync-progress"); - }, - - /** - * Prepare an invisible iframe with the passphrase backup document. - * Used by both the print and saving methods. - * - * @param elid : ID of the form element containing the passphrase. - * @param callback : Function called once the iframe has loaded. - */ - _preparePPiframe: function(elid, callback) { - let pp = document.getElementById(elid).value; - - // Create an invisible iframe whose contents we can print. - let iframe = document.createElement("iframe"); - iframe.setAttribute("src", "chrome://weave/content/key.xhtml"); - iframe.collapsed = true; - document.documentElement.appendChild(iframe); - iframe.contentWindow.addEventListener("load", function() { - iframe.contentWindow.removeEventListener("load", arguments.callee, false); - - // Insert the Sync Key into the page. - let el = iframe.contentDocument.getElementById("synckey"); - el.firstChild.nodeValue = pp; - - // Insert the TOS and Privacy Policy URLs into the page. - let termsURL = Weave.Svc.Prefs.get("termsURL"); - el = iframe.contentDocument.getElementById("tosLink"); - el.setAttribute("href", termsURL); - el.firstChild.nodeValue = termsURL; - - let privacyURL = Weave.Svc.Prefs.get("privacyURL"); - el = iframe.contentDocument.getElementById("ppLink"); - el.setAttribute("href", privacyURL); - el.firstChild.nodeValue = privacyURL; - - callback(iframe); - }, false); - }, - - /** - * Print passphrase backup document. - * - * @param elid : ID of the form element containing the passphrase. - */ - passphrasePrint: function(elid) { - this._preparePPiframe(elid, function(iframe) { - let webBrowserPrint = iframe.contentWindow - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebBrowserPrint); - let printSettings = PrintUtils.getPrintSettings(); - - // Display no header/footer decoration except for the date. - printSettings.headerStrLeft - = printSettings.headerStrCenter - = printSettings.headerStrRight - = printSettings.footerStrLeft - = printSettings.footerStrCenter = ""; - printSettings.footerStrRight = "&D"; - - try { - webBrowserPrint.print(printSettings, null); - } catch (ex) { - // print()'s return codes are expressed as exceptions. Ignore. - } - }); - }, - - /** - * Save passphrase backup document to disk as HTML file. - * - * @param elid : ID of the form element containing the passphrase. - */ - passphraseSave: function(elid) { - let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title"); - let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename"); - this._preparePPiframe(elid, function(iframe) { - let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - let fpCallback = function fpCallback_done(aResult) { - if (aResult == Ci.nsIFilePicker.returnOK || - aResult == Ci.nsIFilePicker.returnReplace) { - let stream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - stream.init(fp.file, -1, PERMISSIONS_RWUSR, 0); - - let serializer = new XMLSerializer(); - let output = serializer.serializeToString(iframe.contentDocument); - output = output.replace(/<!DOCTYPE (.|\n)*?]>/, - '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' + - '"DTD/xhtml1-strict.dtd">'); - output = Weave.Utils.encodeUTF8(output); - stream.write(output, output.length); - } - }; - - fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave); - fp.appendFilters(Ci.nsIFilePicker.filterHTML); - fp.defaultString = defaultSaveName; - fp.open(fpCallback); - return false; - }); - }, - - /** - * validatePassword - * - * @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) { - let valid = false; - let val1 = el1.value; - let val2 = el2 ? el2.value : ""; - let error = ""; - - if (!el2) - valid = val1.length >= Weave.MIN_PASS_LENGTH; - else if (val1 && val1 == Weave.Service.identity.username) - error = "change.password.pwSameAsUsername"; - else if (val1 && val1 == Weave.Service.identity.account) - error = "change.password.pwSameAsEmail"; - else if (val1 && val1 == Weave.Service.identity.basicPassword) - error = "change.password.pwSameAsPassword"; - else if (val1 && val2) { - if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH) - valid = true; - else if (val1.length < Weave.MIN_PASS_LENGTH) - error = "change.password.tooShort"; - else if (val1 != val2) - error = "change.password.mismatch"; - } - let errorString = error ? Weave.Utils.getErrorString(error) : ""; - return [valid, errorString]; - } -}; diff --git a/components/weave/jar.mn b/components/weave/jar.mn deleted file mode 100644 index fee5c0889..000000000 --- a/components/weave/jar.mn +++ /dev/null @@ -1,37 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -toolkit.jar: -% content weave %content/weave/ contentaccessible=yes - content/weave/aboutSyncTabs.xul (content/aboutSyncTabs.xul) - content/weave/aboutSyncTabs.js (content/aboutSyncTabs.js) - content/weave/aboutSyncTabs.css (content/aboutSyncTabs.css) - content/weave/aboutSyncTabs-bindings.xml (content/aboutSyncTabs-bindings.xml) - content/weave/setup.xul (content/setup.xul) - content/weave/addDevice.js (content/addDevice.js) - content/weave/addDevice.xul (content/addDevice.xul) - content/weave/setup.js (content/setup.js) - content/weave/genericChange.xul (content/genericChange.xul) - content/weave/genericChange.js (content/genericChange.js) - content/weave/key.xhtml (content/key.xhtml) - content/weave/notification.xml (content/notification.xml) - content/weave/quota.xul (content/quota.xul) - content/weave/quota.js (content/quota.js) - content/weave/utils.js (content/utils.js) - content/weave/progress.js (content/progress.js) - content/weave/progress.xhtml (content/progress.xhtml) - -classic.jar: -% skin weave classic/1.0 %skin/classic/weave/ - skin/classic/weave/aboutSyncTabs.css (skin/aboutSyncTabs.css) - skin/classic/weave/sync-16.png (skin/sync-16.png) - skin/classic/weave/sync-32.png (skin/sync-32.png) - skin/classic/weave/sync-128.png (skin/sync-128.png) - skin/classic/weave/sync-bg.png (skin/sync-bg.png) - skin/classic/weave/sync-desktopIcon.png (skin/sync-desktopIcon.png) - skin/classic/weave/sync-mobileIcon.png (skin/sync-mobileIcon.png) -* skin/classic/weave/syncSetup.css (skin/syncSetup.css) -* skin/classic/weave/syncCommon.css (skin/syncCommon.css) - skin/classic/weave/syncQuota.css (skin/syncQuota.css) - skin/classic/weave/syncProgress.css (skin/syncProgress.css)
\ No newline at end of file diff --git a/components/weave/locales/en-US/aboutSyncTabs.dtd b/components/weave/locales/en-US/aboutSyncTabs.dtd deleted file mode 100644 index 5865c1231..000000000 --- a/components/weave/locales/en-US/aboutSyncTabs.dtd +++ /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/. --> - -<!-- LOCALIZATION NOTE (tabs.otherDevices.label): Keep this in sync with syncTabsMenu2.label from browser.dtd --> -<!ENTITY tabs.otherDevices.label "Tabs From Other Devices"> - -<!ENTITY tabs.searchText.label "Type here to find tabs…"> - -<!-- LOCALIZATION NOTE (tabs.context.openTab.accesskey, tabs.context.openMultipleTabs.accesskey): - Only one of these will show at a time (based on selection), so reusing accesskey is ok. --> -<!ENTITY tabs.context.openTab.label "Open This Tab"> -<!ENTITY tabs.context.openTab.accesskey "O"> -<!ENTITY tabs.context.openMultipleTabs.label "Open Selected Tabs"> -<!ENTITY tabs.context.openMultipleTabs.accesskey "O"> -<!ENTITY tabs.context.bookmarkSingleTab.label "Bookmark This Tab…"> -<!ENTITY tabs.context.bookmarkSingleTab.accesskey "B"> -<!ENTITY tabs.context.bookmarkMultipleTabs.label "Bookmark Selected Tabs…"> -<!ENTITY tabs.context.bookmarkMultipleTabs.accesskey "B"> -<!ENTITY tabs.context.refreshList.label "Refresh List"> -<!ENTITY tabs.context.refreshList.accesskey "R"> diff --git a/components/weave/locales/en-US/errors.properties b/components/weave/locales/en-US/errors.properties deleted file mode 100644 index e51eb422c..000000000 --- a/components/weave/locales/en-US/errors.properties +++ /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/. - -error.login.reason.network = Failed to connect to the server -error.login.reason.recoverykey = Wrong Recovery Key -error.login.reason.account = Incorrect account name or password -error.login.reason.no_username = Missing account name -error.login.reason.no_password2 = Missing password -error.login.reason.no_recoverykey= No saved Recovery Key to use -error.login.reason.server = Server incorrectly configured - -error.sync.failed_partial = One or more data types could not be synced -# LOCALIZATION NOTE (error.sync.reason.serverMaintenance): We removed the extraneous period from this string -error.sync.reason.serverMaintenance = Sync server maintenance is underway; syncing will resume automatically - -invalid-captcha = Incorrect words, try again -weak-password = Use a stronger password - -# this is the fallback, if we hit an error we didn't bother to localize -error.reason.unknown = Unknown error - -change.password.pwSameAsPassword = Password can't match current password -change.password.pwSameAsUsername = Password can't match your user name -change.password.pwSameAsEmail = Password can't match your email address -change.password.mismatch = The passwords entered do not match -change.password.tooShort = The password entered is too short diff --git a/components/weave/locales/en-US/sync.properties b/components/weave/locales/en-US/sync.properties deleted file mode 100644 index af40e125a..000000000 --- a/components/weave/locales/en-US/sync.properties +++ /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/. - -# %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android) -client.name2 = %1$S's %2$S on %3$S - -# %S is the date and time at which the last sync successfully completed -lastSync2.label = Last sync: %S - -# signInToSync.description is the tooltip for the Sync buttons when Sync is -# not configured. -signInToSync.description = Sign In To Sync -mobile.label = Mobile Bookmarks - -remote.pending.label = Remote tabs are being synced… -remote.missing2.label = Sync your other devices again to access their tabs -remote.opened.label = All remote tabs are already open -remote.notification.label = Recent desktop tabs will be available once they sync - -error.login.title = Error While Signing In -error.login.description = Sync encountered an error while connecting: %1$S. Please try again. -error.login.prefs.label = Preferences… -error.login.prefs.accesskey = P -# should decide if we're going to show this -error.logout.title = Error While Signing Out -error.logout.description = Sync encountered an error while connecting. It's probably ok, and you don't have to do anything about it. -error.sync.title = Error While Syncing -error.sync.description = Sync encountered an error while syncing: %1$S. Sync will automatically retry this action. -error.sync.prolonged_failure = Sync has not been able to complete during the last %1$S days. Please check your network settings. -error.sync.serverStatusButton.label = Server Status -error.sync.serverStatusButton.accesskey = V -error.sync.needUpdate.description = You need to update Sync to continue syncing your data. -error.sync.needUpdate.label = Update Sync -error.sync.needUpdate.accesskey = U -error.sync.tryAgainButton.label = Sync Now -error.sync.tryAgainButton.accesskey = S -warning.sync.quota.label = Approaching Server Quota -warning.sync.quota.description = You are approaching the server quota. Please review which data to sync. -error.sync.quota.label = Server Quota Exceeded -error.sync.quota.description = Sync failed because it exceeded the server quota. Please review which data to sync. -error.sync.viewQuotaButton.label = View Quota -error.sync.viewQuotaButton.accesskey = V -warning.sync.eol.label = Service Shutting Down -# %1: the app name (Basilisk) -warning.sync.eol.description = Your Sync service is shutting down soon. Upgrade %1$S to keep syncing. -error.sync.eol.label = Service Unavailable -# %1: the app name (Basilisk) -error.sync.eol.description = Your Sync service is no longer available. You need to upgrade %1$S to keep syncing. -sync.eol.learnMore.label = Learn more -sync.eol.learnMore.accesskey = L - -syncnow.label = Sync Now -syncing2.label = Syncing… -setupsync.label = Set Up Sync diff --git a/components/weave/locales/en-US/syncGenericChange.properties b/components/weave/locales/en-US/syncGenericChange.properties deleted file mode 100644 index aea86ad0e..000000000 --- a/components/weave/locales/en-US/syncGenericChange.properties +++ /dev/null @@ -1,37 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -#LOCALIZATION NOTE (change.password.title): This (and associated change.password/passphrase) are used when the user elects to change their password. -change.password.title = Change your Password -change.password.acceptButton = Change Password -change.password.status.active = Changing your password… -change.password.status.success = Your password has been changed. -change.password.status.error = There was an error changing your password. - -change.password3.introText = Your password must be at least 8 characters long. It cannot be the same as either your user name or your Recovery Key. -change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password. - -change.recoverykey.title = My Recovery Key -change.recoverykey.acceptButton = Change Recovery Key -change.recoverykey.label = Changing Recovery Key and uploading local data, please wait… -change.recoverykey.error = There was an error while changing your Recovery Key! -change.recoverykey.success = Your Recovery Key was successfully changed! - -change.synckey.introText2 = To ensure your total privacy, all of your data is encrypted prior to being uploaded. The key to decrypt your data is not uploaded. -# LOCALIZATION NOTE (change.recoverykey.warningText) "Sync" should match &syncBrand.shortName.label; from syncBrand.dtd -change.recoverykey.warningText = Note: Changing this will erase all data stored on the Sync server and upload new data secured by this Recovery Key. Your other devices will not sync until the new Recovery Key is entered for that device. - -new.recoverykey.label = Your Recovery Key - -# LOCALIZATION NOTE (new.password.title): This (and associated new.password/passphrase) are used on a second computer when it detects that your password or passphrase has been changed on a different device. -new.password.title = Update Password -new.password.introText = Your password was rejected by the server, please update your password. -new.password.label = Enter your new password -new.password.confirm = Confirm your new password -new.password.acceptButton = Update Password -new.password.status.incorrect = Password incorrect, please try again. - -new.recoverykey.title = Update Recovery Key -new.recoverykey.introText = Your Recovery Key was changed using another device, please enter your updated Recovery Key. -new.recoverykey.acceptButton = Update Recovery Key diff --git a/components/weave/locales/en-US/syncKey.dtd b/components/weave/locales/en-US/syncKey.dtd deleted file mode 100644 index f37f2c92e..000000000 --- a/components/weave/locales/en-US/syncKey.dtd +++ /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/. --> - -<!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.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.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 "> -<!ENTITY syncKey.footer2.label ". The Privacy Policy is available at "> -<!ENTITY syncKey.footer3.label "."> diff --git a/components/weave/locales/en-US/syncProgress.dtd b/components/weave/locales/en-US/syncProgress.dtd deleted file mode 100644 index db45cb935..000000000 --- a/components/weave/locales/en-US/syncProgress.dtd +++ /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/. --> - -<!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/components/weave/locales/en-US/syncQuota.dtd b/components/weave/locales/en-US/syncQuota.dtd deleted file mode 100644 index 71174f087..000000000 --- a/components/weave/locales/en-US/syncQuota.dtd +++ /dev/null @@ -1,8 +0,0 @@ -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.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 quota.dialogTitle.label "Server Quota"> -<!ENTITY quota.retrievingInfo.label "Retrieving quota information…"> -<!ENTITY quota.typeColumn.label "Type"> -<!ENTITY quota.sizeColumn.label "Size"> diff --git a/components/weave/locales/en-US/syncQuota.properties b/components/weave/locales/en-US/syncQuota.properties deleted file mode 100644 index 0e1b857ca..000000000 --- a/components/weave/locales/en-US/syncQuota.properties +++ /dev/null @@ -1,42 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -collection.addons.label = Add-ons -collection.bookmarks.label = Bookmarks -collection.history.label = History -collection.passwords.label = Passwords -collection.prefs.label = Preferences -collection.tabs.label = Tabs - -# LOCALIZATION NOTE (quota.usageNoQuota.label): %1$S and %2$S are numeric value -# and unit (as defined in the download manager) of the amount of space occupied -# on the server -quota.usageNoQuota.label = You are currently using %1$S %2$S. -# LOCALIZATION NOTE (quota.usagePercentage.label): -# %1$S is the percentage of space used, -# %2$S and %3$S numeric value and unit (as defined in the download manager) -# of the amount of space used, -# %3$S and %4$S numeric value and unit (as defined in the download manager) -# of the total space available. -quota.usagePercentage.label = You are using %1$S%% (%2$S %3$S) of your allowed %4$S %5$S. -quota.usageError.label = Could not retrieve quota information. -quota.retrieving.label = Retrieving… -# LOCALIZATION NOTE (quota.sizeValueUnit.label): %1$S is the amount of space -# occupied by the engine, %2$K the corresponding unit (e.g. kB) as defined in -# the download manager. -quota.sizeValueUnit.label = %1$S %2$S -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 = 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 -# LOCALIZATION NOTE (quota.freeup.label): %1$S and %2$S are numeric value -# and unit (as defined in the download manager) of the amount of space freed -# up by disabling the unchecked engines. If displayed this string is -# concatenated directly to quota.removal.label and may need to start off with -# whitespace. -quota.freeup.label = \u0020This will free up %1$S %2$S. diff --git a/components/weave/locales/en-US/syncSetup.dtd b/components/weave/locales/en-US/syncSetup.dtd deleted file mode 100644 index 7ee938e5d..000000000 --- a/components/weave/locales/en-US/syncSetup.dtd +++ /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/. --> - -<!ENTITY accountSetupTitle.label "&syncBrand.fullName.label; Setup"> - -<!-- 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 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"> - -<!-- New Account AND Existing Account --> -<!ENTITY server.label "Server"> -<!ENTITY serverType.default.label "Default: &syncBrand.fullName.label; server"> -<!ENTITY serverType.custom2.label "Use a custom server…"> -<!ENTITY signIn.account2.label "Account"> -<!ENTITY signIn.account2.accesskey "A"> -<!ENTITY signIn.password.label "Password"> -<!ENTITY signIn.password.accesskey "P"> -<!ENTITY signIn.recoveryKey.label "Recovery Key"> -<!ENTITY signIn.recoveryKey.accesskey "K"> - -<!-- New Account Page 1: Basic Account Info --> -<!ENTITY setup.newAccountDetailsPage.title.label "Account Details"> -<!ENTITY setup.emailAddress.label "Email Address"> -<!ENTITY setup.emailAddress.accesskey "E"> -<!ENTITY setup.choosePassword.label "Choose a Password"> -<!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 --> -<!ENTITY setup.tosAgree1.label "I agree to the"> -<!ENTITY setup.tosAgree1.accesskey "a"> -<!ENTITY setup.tosLink.label "Terms of Service"> -<!ENTITY setup.tosAgree2.label "and the"> -<!ENTITY setup.ppLink.label "Privacy Policy"> -<!ENTITY setup.tosAgree3.label ""> -<!ENTITY setup.tosAgree2.accesskey ""> - -<!-- My Recovery Key dialog --> -<!ENTITY setup.newRecoveryKeyPage.title.label "&brandShortName; Cares About Your Privacy"> -<!ENTITY setup.newRecoveryKeyPage.description.label "To ensure your total privacy, all of your data is encrypted prior to being uploaded. The Recovery Key which is necessary to decrypt your data is not uploaded."> -<!ENTITY recoveryKeyEntry.label "Your Recovery Key"> -<!ENTITY recoveryKeyEntry.accesskey "K"> -<!ENTITY syncGenerateNewKey.label "Generate a new key"> -<!ENTITY recoveryKeyBackup.description "Your Recovery Key is required to access &syncBrand.fullName.label; on other machines. Please create a backup copy. We cannot help you recover your Recovery Key."> - -<!ENTITY button.syncKeyBackup.print.label "Print…"> -<!ENTITY button.syncKeyBackup.print.accesskey "P"> -<!ENTITY button.syncKeyBackup.save.label "Save…"> -<!ENTITY button.syncKeyBackup.save.accesskey "S"> - -<!-- 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 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."> -<!ENTITY addDevice.dialog.enterCode.label "Enter the code that the device provides:"> -<!ENTITY addDevice.dialog.tryAgain.label "Please try again."> -<!ENTITY addDevice.dialog.successful.label "The device has been successfully added. The initial synchronization can take several minutes and will finish in the background."> -<!ENTITY addDevice.dialog.recoveryKey.label "To activate your device you will need to enter your Recovery Key. Please print or save this key and take it with you."> -<!ENTITY addDevice.dialog.connected.label "Device Connected"> - -<!-- 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; 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."> - -<!-- Sync Options --> -<!ENTITY setup.optionsPage.title "Sync Options"> -<!ENTITY syncDeviceName.label "Device Name:"> -<!ENTITY syncDeviceName.accesskey "c"> - -<!ENTITY syncMy.label "Sync My"> -<!ENTITY engine.bookmarks.label "Bookmarks"> -<!ENTITY engine.bookmarks.accesskey "m"> -<!ENTITY engine.tabs.label "Tabs"> -<!ENTITY engine.tabs.accesskey "T"> -<!ENTITY engine.history.label "History"> -<!ENTITY engine.history.accesskey "r"> -<!ENTITY engine.passwords.label "Passwords"> -<!ENTITY engine.passwords.accesskey "P"> -<!ENTITY engine.prefs.label "Preferences"> -<!ENTITY engine.prefs.accesskey "S"> -<!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 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"> - -<!-- 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.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:"> - -<!-- New & Existing Account: Setup Complete --> -<!ENTITY setup.successPage.title "Setup Complete"> -<!ENTITY changeOptions.label "You can change this preference by selecting Sync Options below."> -<!ENTITY continueUsing.label "You may now continue using &brandShortName;."> diff --git a/components/weave/locales/en-US/syncSetup.properties b/components/weave/locales/en-US/syncSetup.properties deleted file mode 100644 index 8a5170adb..000000000 --- a/components/weave/locales/en-US/syncSetup.properties +++ /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/. - -button.syncOptions.label = Sync Options -button.syncOptionsDone.label = Done -button.syncOptionsCancel.label = Cancel - -invalidEmail.label = Invalid email address -serverInvalid.label = Please enter a valid server URL -usernameNotAvailable.label = Already in use - -verifying.label = Verifying… - -# LOCALIZATION NOTE (additionalClientCount.label): -# Semicolon-separated list of plural forms. See: -# http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 is the number of additional clients (was %S for a short while, use #1 instead, even if both work) -additionalClientCount.label = and #1 additional device;and #1 additional devices -# LOCALIZATION NOTE (bookmarksCount.label): -# Semicolon-separated list of plural forms. See: -# http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 is the number of bookmarks (was %S for a short while, use #1 instead, even if both work) -bookmarksCount.label = #1 bookmark;#1 bookmarks -# LOCALIZATION NOTE (historyDaysCount.label): -# Semicolon-separated list of plural forms. See: -# http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 is the number of days (was %S for a short while, use #1 instead, even if both work) -historyDaysCount.label = #1 day of history;#1 days of history -# LOCALIZATION NOTE (passwordsCount.label): -# Semicolon-separated list of plural forms. See: -# http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 is the number of passwords (was %S for a short while, use #1 instead, even if both work) -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 addon;#1 addons - -save.recoverykey.title = Save Recovery Key -save.recoverykey.defaultfilename = Pale Moon Recovery Key.html - -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 = 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 diff --git a/components/weave/locales/jar.mn b/components/weave/locales/jar.mn deleted file mode 100644 index aafcf4af6..000000000 --- a/components/weave/locales/jar.mn +++ /dev/null @@ -1,18 +0,0 @@ -#filter substitution -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - - -@AB_CD@.jar: -% locale weave @AB_CD@ %locale/@AB_CD@/weave/ - locale/@AB_CD@/weave/errors.properties (%errors.properties) - locale/@AB_CD@/weave/sync.properties (%sync.properties) - locale/@AB_CD@/weave/syncProgress.dtd (%syncProgress.dtd) - locale/@AB_CD@/weave/aboutSyncTabs.dtd (%aboutSyncTabs.dtd) - locale/@AB_CD@/weave/syncSetup.dtd (%syncSetup.dtd) - locale/@AB_CD@/weave/syncSetup.properties (%syncSetup.properties) - locale/@AB_CD@/weave/syncGenericChange.properties (%syncGenericChange.properties) - locale/@AB_CD@/weave/syncKey.dtd (%syncKey.dtd) - locale/@AB_CD@/weave/syncQuota.dtd (%syncQuota.dtd) - locale/@AB_CD@/weave/syncQuota.properties (%syncQuota.properties)
\ No newline at end of file diff --git a/components/weave/locales/l10n.ini b/components/weave/locales/l10n.ini deleted file mode 100644 index d9c1ef945..000000000 --- a/components/weave/locales/l10n.ini +++ /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/. - -[general] -depth = ../../.. - -[compare] -dirs = services/sync diff --git a/components/weave/locales/moz.build b/components/weave/locales/moz.build deleted file mode 100644 index e3d80cf11..000000000 --- a/components/weave/locales/moz.build +++ /dev/null @@ -1,6 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.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'] diff --git a/components/weave/moz.build b/components/weave/moz.build deleted file mode 100644 index 35c98f16c..000000000 --- a/components/weave/moz.build +++ /dev/null @@ -1,77 +0,0 @@ -# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.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 += ['locales'] - -DEFINES['weave_id'] = "{340c2bbc-ce74-4362-90b5-7c26312808ef}" -DEFINES['weave_version'] = "1.45.0" - -XPIDL_SOURCES += ['public/nsISyncJPAKE.idl'] - -SOURCES += ['src/nsSyncJPAKE.cpp'] - -EXTRA_COMPONENTS += [ - 'src/WeaveService.js', - 'WeaveComponents.manifest', -] - -EXTRA_JS_MODULES['services-common'] += [ - 'src/common/hawkclient.js', - 'src/common/hawkrequest.js', - 'src/common/logmanager.js', - 'src/common/observers.js', - 'src/common/rest.js', - 'src/common/stringbundle.js', - 'src/common/tokenserverclient.js', -] - -EXTRA_JS_MODULES['services-crypto'] += [ - 'src/crypto/utils.js', - 'src/crypto/WeaveCrypto.js', -] - -EXTRA_JS_MODULES['services-sync'] += [ - 'src/sync/addonsreconciler.js', - 'src/sync/addonutils.js', - 'src/sync/engines.js', - 'src/sync/identity.js', - 'src/sync/jpakeclient.js', - 'src/sync/keys.js', - 'src/sync/main.js', - 'src/sync/notifications.js', - 'src/sync/policies.js', - 'src/sync/record.js', - 'src/sync/resource.js', - 'src/sync/rest.js', - 'src/sync/service.js', - 'src/sync/status.js', - 'src/sync/userapi.js', - 'src/sync/util.js', -] - -EXTRA_JS_MODULES['services-sync'].engines += [ - 'src/engines/addons.js', - 'src/engines/bookmarks.js', - 'src/engines/clients.js', - 'src/engines/forms.js', - 'src/engines/history.js', - 'src/engines/passwords.js', - 'src/engines/prefs.js', - 'src/engines/tabs.js', -] - -EXTRA_JS_MODULES['services-sync'].stages += [ - 'src/stages/cluster.js', - 'src/stages/declined.js', - 'src/stages/enginesync.js', -] - -FINAL_TARGET_PP_FILES['modules/services-sync'] += ['src/sync/constants.js'] - -JS_PREFERENCE_FILES += ['weave-prefs.js'] -XPIDL_MODULE = 'weave' -FINAL_LIBRARY = 'xul' -JAR_MANIFESTS += ['jar.mn'] - diff --git a/components/weave/public/nsISyncJPAKE.idl b/components/weave/public/nsISyncJPAKE.idl deleted file mode 100644 index 864057235..000000000 --- a/components/weave/public/nsISyncJPAKE.idl +++ /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/. */ - -#include "nsISupports.idl" - -[scriptable, uuid(5ab02a98-5122-4b90-93cd-f259c4b42e3a)] -interface nsISyncJPAKE : nsISupports -{ - /** - * Perform first round of the JPAKE exchange. - * - * @param aSignerID - * String identifying the signer. - * @param aGX1 - * Schnorr signature value g^x1, in hex representation. - * @param aGV1 - * Schnorr signature value g^v1 (v1 is a random value), in hex - * representation. - * @param aR1 - * Schnorr signature value r1 = v1 - x1 * h, in hex representation. - * @param aGX2 - * Schnorr signature value g^x2, in hex representation. - * @param aGV2 - * Schnorr signature value g^v2 (v2 is a random value), in hex - * representation. - * @param aR2 - * Schnorr signature value r2 = v2 - x2 * h, in hex representation. - */ - void round1(in ACString aSignerID, - out ACString aGX1, - out ACString aGV1, - out ACString aR1, - out ACString aGX2, - out ACString aGV2, - out ACString aR2); - - /** - * Perform second round of the JPAKE exchange. - * - * @param aPeerID - * String identifying the peer. - * @param aPIN - * String containing the weak secret (PIN). - * @param aGX3 - * Schnorr signature value g^x3, in hex representation. - * @param aGV3 - * Schnorr signature value g^v3 (v3 is a random value), in hex - * representation. - * @param aR3 - * Schnorr signature value r3 = v3 - x3 * h, in hex representation. - * @param aGX4 - * Schnorr signature value g^x4, in hex representation. - * @param aGV4 - * Schnorr signature value g^v4 (v4 is a random value), in hex - * representation. - * @param aR4 - * Schnorr signature value r4 = v4 - x4 * h, in hex representation. - * @param aA - * Schnorr signature value A, in hex representation. - * @param aGVA - * Schnorr signature value g^va (va is a random value), in hex - * representation. - * @param aRA - * Schnorr signature value ra = va - xa * h, in hex representation. - */ - void round2(in ACString aPeerID, - in ACString aPIN, - in ACString aGX3, - in ACString aGV3, - in ACString aR3, - in ACString aGX4, - in ACString aGV4, - in ACString aR4, - out ACString aA, - out ACString aGVA, - out ACString aRA); - - /** - * Perform the final step of the JPAKE exchange. This will compute - * the key and expand the key to two keys, an AES256 encryption key - * and a 256 bit HMAC key. It returns a key confirmation value - * (SHA256d of the key) and the encryption and HMAC keys. - * - * @param aB - * Schnorr signature value B, in hex representation. - * @param aGVB - * Schnorr signature value g^vb (vb is a random value), in hex - * representation. - * @param aRB - * Schnorr signature value rb = vb - xb * h, in hex representation. - * @param aAES256Key - * The AES 256 encryption key, in base64 representation. - * @param aHMAC256Key - * The 256 bit HMAC key, in base64 representation. - */ - void final(in ACString aB, - in ACString aGVB, - in ACString aRB, - in ACString aHkdfInfo, - out ACString aAES256Key, - out ACString aHMAC256Key); -}; diff --git a/components/weave/skin/aboutSyncTabs.css b/components/weave/skin/aboutSyncTabs.css deleted file mode 100644 index 4796f7bcc..000000000 --- a/components/weave/skin/aboutSyncTabs.css +++ /dev/null @@ -1,101 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#tabs-display, -#tabsList { - background-color: transparent; - -moz-appearance: none; - margin: 0; -} - -#tabsList { - width: 100%; -} - -#tabs-display { - background: #fff url(chrome://weave/skin/sync-bg.png) repeat-x center -80px; -} - -#headers { - background: url(chrome://weave/skin/sync-32.png) no-repeat; - margin-top: 4px; - width: 45em; - height: 32px; - margin-inline-start: 2em; - margin-inline-end: 2em; -} - -#tabsListHeading { - font-size: 140%; - font-weight: bold; - margin-inline-start: 40px; -} - -richlistitem { - margin-inline-end: 2em; -} - -richlistitem[selected="true"], -richlistitem:focus { - outline-style: none; -} - -richlistitem[type="tab"] { - min-height: 3em; - border: #999999 1px solid !important; - padding: 2px 5px; - margin-bottom: 4px; - margin-inline-start: 4em; - border-radius: 6px; - background-color: menu; - width: 44em; - opacity: 0.9; - box-shadow: - inset rgba(255, 255, 255, 0.5) 0 1px 0px, - inset rgba(0, 0, 0, 0.1) 0 -2px 0px, - rgba(0, 0, 0, 0.1) 0px 1px 0px; -} - -richlistitem[type="tab"][selected="true"] { - background-color: -moz-MenuHover; -} - -richlistitem[type="client"] { - min-height: 2em; - color: #000000; - margin-inline-start: 2em; - margin-top: 2px; - margin-bottom: 3px; - width: 42em; - border-radius: 6px; - background-color: transparent; - -moz-user-focus: ignore !important; -} -richlistitem.mobile[type="client"] { - list-style-image: url("chrome://weave/skin/sync-mobileIcon.png"); -} -richlistitem.desktop[type="client"] { - list-style-image: url("chrome://weave/skin/sync-desktopIcon.png"); -} - -.title, -.clientName { - color: #000000; - font-size: 1.1em; -} - -.title[selected="true"], -.url[selected="true"] { - color: inherit; -} - -.url { - color: -moz-nativehyperlinktext; - font-size: 0.95em; -} - -.tabIcon { - padding-inline-start: 2px; - padding-top: 2px; -} diff --git a/components/weave/skin/sync-128.png b/components/weave/skin/sync-128.png Binary files differdeleted file mode 100644 index 1ea34818c..000000000 --- a/components/weave/skin/sync-128.png +++ /dev/null diff --git a/components/weave/skin/sync-16.png b/components/weave/skin/sync-16.png Binary files differdeleted file mode 100644 index 0afb1c719..000000000 --- a/components/weave/skin/sync-16.png +++ /dev/null diff --git a/components/weave/skin/sync-32.png b/components/weave/skin/sync-32.png Binary files differdeleted file mode 100644 index 7a762cb98..000000000 --- a/components/weave/skin/sync-32.png +++ /dev/null diff --git a/components/weave/skin/sync-bg.png b/components/weave/skin/sync-bg.png Binary files differdeleted file mode 100644 index 893a27d76..000000000 --- a/components/weave/skin/sync-bg.png +++ /dev/null diff --git a/components/weave/skin/sync-desktopIcon.png b/components/weave/skin/sync-desktopIcon.png Binary files differdeleted file mode 100644 index d3d1e27c3..000000000 --- a/components/weave/skin/sync-desktopIcon.png +++ /dev/null diff --git a/components/weave/skin/sync-mobileIcon.png b/components/weave/skin/sync-mobileIcon.png Binary files differdeleted file mode 100644 index a3bda5751..000000000 --- a/components/weave/skin/sync-mobileIcon.png +++ /dev/null diff --git a/components/weave/skin/syncCommon.css b/components/weave/skin/syncCommon.css deleted file mode 100644 index 05467c5c1..000000000 --- a/components/weave/skin/syncCommon.css +++ /dev/null @@ -1,53 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* The following are used by both sync/setup.xul and sync/genericChange.xul */ -.status { - color: -moz-dialogtext; -} - -.statusIcon { - margin-inline-start: 4px; - max-height: 16px; - max-width: 16px; -} - -.statusIcon[status="active"] { - list-style-image: url("chrome://global/skin/icons/loading_16.png"); -} - -.statusIcon[status="error"] { -%ifdef XP_WIN - list-style-image: url("chrome://global/skin/icons/error-16.png"); -%else - list-style-image: url("moz-icon://stock/gtk-dialog-error?size=menu"); -%endif -} - -.statusIcon[status="success"] { - list-style-image: url("moz-icon://stock/gtk-dialog-info?size=menu"); -} - -/* .data is only used by sync/genericChange.xul, but it seems unnecessary to have - a separate stylesheet for it. */ -.data { - font-size: 90%; - font-weight: bold; -} - -dialog#change-dialog { - width: 40em; -} - -image#syncIcon { - list-style-image: url("chrome://weave/skin/sync-32.png"); -} - -#introText { - margin-top: 2px; -} - -#feedback { - height: 2em; -} diff --git a/components/weave/skin/syncProgress.css b/components/weave/skin/syncProgress.css deleted file mode 100644 index d7aa59976..000000000 --- a/components/weave/skin/syncProgress.css +++ /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/. */ -@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/components/weave/skin/syncQuota.css b/components/weave/skin/syncQuota.css deleted file mode 100644 index 1577de8a3..000000000 --- a/components/weave/skin/syncQuota.css +++ /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/. */ - -#quotaDialog { - width: 33em; - height: 25em; -} - -treechildren::-moz-tree-checkbox { - list-style-image: none; -} -treechildren::-moz-tree-checkbox(checked) { - list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif"); -} -treechildren::-moz-tree-checkbox(disabled) { - list-style-image: url("chrome://global/skin/checkbox/cbox-check-dis.gif"); -} - -#treeCaption { - height: 4em; -} - -.captionWarning { - font-weight: bold; -} diff --git a/components/weave/skin/syncSetup.css b/components/weave/skin/syncSetup.css deleted file mode 100644 index b423eca29..000000000 --- a/components/weave/skin/syncSetup.css +++ /dev/null @@ -1,134 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -wizard { - -moz-appearance: none; - width: 55em; - height: 45em; - padding: 0; - background-color: Window; -} - -.wizard-page-box { - -moz-appearance: none; - padding-left: 0; - padding-right: 0; - margin: 0; -} - -wizardpage { - -moz-box-pack: center; - -moz-box-align: center; - margin: 0; - padding: 0 6em; - background-color: Window; -} - -.wizard-header { - -moz-appearance: none; - border: none; - padding: 2em 0 1em 0; - text-align: center; -} -.wizard-header-label { - font-size: 24pt; - font-weight: normal; -} - -.wizard-buttons { - background-color: rgba(0,0,0,0.1); - padding: 1em; -} - -.wizard-buttons-separator { - visibility: collapse; -} - -.wizard-header-icon { - visibility: collapse; -} - -.accountChoiceButton { - font: menu; -} - -.confirm { - border: 1px solid black; - padding: 1em; - border-radius: 5px; -} - -/* Override the text-link style from global.css */ -description > .text-link, -description > .text-link:focus { - margin: 0px; - padding: 0px; - border: 0px; -} - - -.success, -.error { - padding: 2px; - border-radius: 2px; -} - -.error { - background-color: #FF0000 !important; - color: #FFFFFF !important; -} - -.success { - background-color: #00FF00 !important; -} - -.warning { - font-weight: bold; - font-size: 100%; - color: red; -} - -.mainDesc { - font-weight: bold; - font-size: 100%; -} - -.normal { - font-size: 100%; -} - -.inputColumn { - margin-inline-end: 2px -} - -.pin { - font-size: 18pt; - width: 4em; - text-align: center; -} - -#passphraseHelpSpacer { - width: 0.5em; -} - -#pairDeviceThrobber > image, -#login-throbber > image { - list-style-image: url("chrome://global/skin/icons/loading_16.png"); -} - -#captchaFeedback { - visibility: hidden; -} - -#successPageIcon { - /* TODO replace this with a 128px version (bug 591122) */ - list-style-image: url("chrome://weave/skin/sync-32.png"); -} - -%ifdef XP_WIN -#tosDesc { - margin-left: -7px; - margin-bottom: 3px; -} -%endif diff --git a/components/weave/src/WeaveService.js b/components/weave/src/WeaveService.js deleted file mode 100644 index 5bafa07ad..000000000 --- a/components/weave/src/WeaveService.js +++ /dev/null @@ -1,177 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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/FileUtils.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-sync/util.js"); - -const SYNC_PREFS_BRANCH = "services.sync."; - - -/** - * Sync's XPCOM service. - * - * It is named "Weave" for historical reasons. - * - * It's worth noting how Sync is lazily loaded. We register a timer that - * loads Sync a few seconds after app startup. This is so Sync does not - * adversely affect application start time. - * - * If Sync is not configured, no extra Sync code is loaded. If an - * external component (say the UI) needs to interact with Sync, it - * should use the promise-base function whenLoaded() - something like the - * following: - * - * // 1. Grab a handle to the Sync XPCOM service. - * let service = Cc["@mozilla.org/weave/service;1"] - * .getService(Components.interfaces.nsISupports) - * .wrappedJSObject; - * - * // 2. Use the .then method of the promise. - * service.whenLoaded().then(() => { - * // You are free to interact with "Weave." objects. - * return; - * }); - * - * And that's it! However, if you really want to avoid promises and do it - * old-school, then - * - * // 1. Get a reference to the service as done in (1) above. - * - * // 2. Check if the service has been initialized. - * if (service.ready) { - * // You are free to interact with "Weave." objects. - * return; - * } - * - * // 3. Install "ready" listener. - * Services.obs.addObserver(function onReady() { - * Services.obs.removeObserver(onReady, "weave:service:ready"); - * - * // You are free to interact with "Weave." objects. - * }, "weave:service:ready", false); - * - * // 4. Trigger loading of Sync. - * service.ensureLoaded(); - */ -function WeaveService() { - this.wrappedJSObject = this; - this.ready = false; -} -WeaveService.prototype = { - classID: Components.ID("{74b89fb0-f200-4ae8-a3ec-dd164117f6de}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - ensureLoaded: function () { - Components.utils.import("resource://services-sync/main.js"); - - // Side-effect of accessing the service is that it is instantiated. - Weave.Service; - }, - - whenLoaded: function() { - if (this.ready) { - return Promise.resolve(); - } - let deferred = Promise.defer(); - - Services.obs.addObserver(function onReady() { - Services.obs.removeObserver(onReady, "weave:service:ready"); - deferred.resolve(); - }, "weave:service:ready", false); - this.ensureLoaded(); - return deferred.promise; - }, - - /** - * Whether Sync appears to be enabled. - * - * This returns true if all the Sync preferences for storing account - * and server configuration are populated. - * - * It does *not* perform a robust check to see if the client is working. - * For that, you'll want to check Weave.Status.checkSetup(). - */ - get enabled() { - let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH); - return prefs.prefHasUserValue("username") && - prefs.prefHasUserValue("clusterURL"); - }, - - observe: function (subject, topic, data) { - switch (topic) { - case "app-startup": - let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - os.addObserver(this, "final-ui-startup", true); - break; - - case "final-ui-startup": - // Force Weave service to load if it hasn't triggered from overlays - this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.timer.initWithCallback({ - notify: function() { - let isConfigured = false; - // We only load more if it looks like Sync is configured. - let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH); - if (prefs.prefHasUserValue("username")) { - // We have a username. So, do a more thorough check. This will - // import a number of modules and thus increase memory - // accordingly. We could potentially copy code performed by - // this check into this file if our above code is yielding too - // many false positives. - Components.utils.import("resource://services-sync/main.js"); - isConfigured = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED; - } - if (isConfigured) { - this.ensureLoaded(); - } - }.bind(this) - }, 10000, Ci.nsITimer.TYPE_ONE_SHOT); - break; - } - } -}; - -function AboutWeaveLog() {} -AboutWeaveLog.prototype = { - classID: Components.ID("{d28f8a0b-95da-48f4-b712-caf37097be41}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule, - Ci.nsISupportsWeakReference]), - - getURIFlags: function(aURI) { - return 0; - }, - - newChannel: function(aURI, aLoadInfo) { - let dir = FileUtils.getDir("ProfD", ["weave", "logs"], true); - let uri = Services.io.newFileURI(dir); - let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); - - channel.originalURI = aURI; - - // Ensure that the about page has the same privileges as a regular directory - // view. That way links to files can be opened. make sure we use the correct - // origin attributes when creating the principal for accessing the - // about:sync-log data. - let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] - .getService(Ci.nsIScriptSecurityManager); - let principal = ssm.createCodebasePrincipal(uri, aLoadInfo.originAttributes); - - channel.owner = principal; - return channel; - } -}; - -const components = [WeaveService, AboutWeaveLog]; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/components/weave/src/common/hawkclient.js b/components/weave/src/common/hawkclient.js deleted file mode 100644 index 88e9c2f2d..000000000 --- a/components/weave/src/common/hawkclient.js +++ /dev/null @@ -1,346 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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"; - -/* - * HAWK is an HTTP authentication scheme using a message authentication code - * (MAC) algorithm to provide partial HTTP request cryptographic verification. - * - * For details, see: https://github.com/hueniverse/hawk - * - * With HAWK, it is essential that the clocks on clients and server not have an - * absolute delta of greater than one minute, as the HAWK protocol uses - * timestamps to reduce the possibility of replay attacks. However, it is - * likely that some clients' clocks will be more than a little off, especially - * in mobile devices, which would break HAWK-based services (like sync and - * firefox accounts) for those clients. - * - * This library provides a stateful HAWK client that calculates (roughly) the - * clock delta on the client vs the server. The library provides an interface - * for deriving HAWK credentials and making HAWK-authenticated REST requests to - * a single remote server. Therefore, callers who want to interact with - * multiple HAWK services should instantiate one HawkClient per service. - */ - -this.EXPORTED_SYMBOLS = ["HawkClient"]; - -var {interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-common/hawkrequest.js"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -// log.appender.dump should be one of "Fatal", "Error", "Warn", "Info", "Config", -// "Debug", "Trace" or "All". If none is specified, "Error" will be used by -// default. -// Note however that Sync will also add this log to *its* DumpAppender, so -// in a Sync context it shouldn't be necessary to adjust this - however, that -// also means error logs are likely to be dump'd twice but that's OK. -const PREF_LOG_LEVEL = "services.common.hawk.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 = "services.common.hawk.log.sensitive"; - -XPCOMUtils.defineLazyGetter(this, "log", function() { - let log = Log.repository.getLogger("Hawk"); - // We set the log itself to "debug" and set the level from the preference to - // the appender. This allows other things to send the logs to different - // appenders, while still allowing the pref to control what is seen via dump() - log.level = Log.Level.Debug; - let appender = new Log.DumpAppender(); - log.addAppender(appender); - appender.level = Log.Level.Error; - try { - let level = - Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL); - 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(this, 'logPII', function() { - try { - return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS); - } catch (_) { - return false; - } -}); - -/* - * A general purpose client for making HAWK authenticated requests to a single - * host. Keeps track of the clock offset between the client and the host for - * computation of the timestamp in the HAWK Authorization header. - * - * Clients should create one HawkClient object per each server they wish to - * interact with. - * - * @param host - * The url of the host - */ -this.HawkClient = function(host) { - this.host = host; - - // Clock offset in milliseconds between our client's clock and the date - // reported in responses from our host. - this._localtimeOffsetMsec = 0; -} - -this.HawkClient.prototype = { - - /* - * A boolean for feature detection. - */ - willUTF8EncodeRequests: HAWKAuthenticatedRESTRequest.prototype.willUTF8EncodeObjectRequests, - - /* - * Construct an error message for a response. Private. - * - * @param restResponse - * A RESTResponse object from a RESTRequest - * - * @param error - * A string or object describing the error - */ - _constructError: function(restResponse, error) { - let errorObj = { - error: error, - // This object is likely to be JSON.stringify'd, but neither Error() - // objects nor Components.Exception objects do the right thing there, - // so we add a new element which is simply the .toString() version of - // the error object, so it does appear in JSON'd values. - errorString: error.toString(), - message: restResponse.statusText, - code: restResponse.status, - errno: restResponse.status, - toString() { - return this.code + ": " + this.message; - }, - }; - let retryAfter = restResponse.headers && restResponse.headers["retry-after"]; - retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter; - if (retryAfter) { - errorObj.retryAfter = retryAfter; - // and notify observers of the retry interval - if (this.observerPrefix) { - Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter); - } - } - return errorObj; - }, - - /* - * - * Update clock offset by determining difference from date gives in the (RFC - * 1123) Date header of a server response. Because HAWK tolerates a window - * of one minute of clock skew (so two minutes total since the skew can be - * positive or negative), the simple method of calculating offset here is - * probably good enough. We keep the value in milliseconds to make life - * easier, even though the value will not have millisecond accuracy. - * - * @param dateString - * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT") - * - * For HAWK clock skew and replay protection, see - * https://github.com/hueniverse/hawk#replay-protection - */ - _updateClockOffset: function(dateString) { - try { - let serverDateMsec = Date.parse(dateString); - this._localtimeOffsetMsec = serverDateMsec - this.now(); - log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec); - } catch(err) { - log.warn("Bad date header in server response: " + dateString); - } - }, - - /* - * Get the current clock offset in milliseconds. - * - * 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._localtimeOffsetMsec; - }, - - /* - * return current time in milliseconds - */ - now: function() { - return Date.now(); - }, - - /* A general method for sending raw RESTRequest calls authorized using HAWK - * - * @param path - * API endpoint path - * @param method - * The HTTP request method - * @param credentials - * Hawk credentials - * @param payloadObj - * An object that can be encodable as JSON as the payload of the - * request - * @param extraHeaders - * An object with header/value pairs to send with the request. - * @return Promise - * Returns a promise that resolves to the response of the API call, - * or is rejected with an error. If the server response can be parsed - * as JSON and contains an 'error' property, the promise will be - * rejected with this JSON-parsed response. - */ - request: function(path, method, credentials=null, payloadObj={}, extraHeaders = {}, - retryOK=true) { - method = method.toLowerCase(); - - let deferred = Promise.defer(); - let uri = this.host + path; - let self = this; - - function _onComplete(error) { - // |error| can be either a normal caught error or an explicitly created - // Components.Exception() error. Log it now as it might not end up - // correctly in the logs by the time it's passed through _constructError. - if (error) { - log.warn("hawk request error", error); - } - // If there's no response there's nothing else to do. - if (!this.response) { - deferred.reject(error); - return; - } - let restResponse = this.response; - let status = restResponse.status; - - log.debug("(Response) " + path + ": code: " + status + - " - Status text: " + restResponse.statusText); - if (logPII) { - log.debug("Response text: " + restResponse.body); - } - - // All responses may have backoff headers, which are a server-side safety - // valve to allow slowing down clients without hurting performance. - self._maybeNotifyBackoff(restResponse, "x-weave-backoff"); - self._maybeNotifyBackoff(restResponse, "x-backoff"); - - if (error) { - // When things really blow up, reconstruct an error object that follows - // the general format of the server on error responses. - return deferred.reject(self._constructError(restResponse, error)); - } - - self._updateClockOffset(restResponse.headers["date"]); - - if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) { - // Retry once if we were rejected due to a bad timestamp. - // Clock offset is adjusted already in the top of this function. - log.debug("Received 401 for " + path + ": retrying"); - return deferred.resolve( - self.request(path, method, credentials, payloadObj, extraHeaders, false)); - } - - // If the server returned a json error message, use it in the rejection - // of the promise. - // - // In the case of a 401, in which we are probably being rejected for a - // bad timestamp, retry exactly once, during which time clock offset will - // be adjusted. - - let jsonResponse = {}; - try { - jsonResponse = JSON.parse(restResponse.body); - } catch(notJSON) {} - - let okResponse = (200 <= status && status < 300); - if (!okResponse || jsonResponse.error) { - if (jsonResponse.error) { - return deferred.reject(jsonResponse); - } - return deferred.reject(self._constructError(restResponse, "Request failed")); - } - // It's up to the caller to know how to decode the response. - // We just return the whole response. - deferred.resolve(this.response); - }; - - function onComplete(error) { - try { - // |this| is the RESTRequest object and we need to ensure _onComplete - // gets the same one. - _onComplete.call(this, error); - } catch (ex) { - log.error("Unhandled exception processing response", ex); - deferred.reject(ex); - } - } - - let extra = { - now: this.now(), - localtimeOffsetMsec: this.localtimeOffsetMsec, - headers: extraHeaders - }; - - let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); - try { - if (method == "post" || method == "put" || method == "patch") { - request[method](payloadObj, onComplete); - } else { - request[method](onComplete); - } - } catch (ex) { - log.error("Failed to make hawk request", ex); - deferred.reject(ex); - } - - return deferred.promise; - }, - - /* - * The prefix used for all notifications sent by this module. This - * allows the handler of notifications to be sure they are handling - * notifications for the service they expect. - * - * If not set, no notifications will be sent. - */ - observerPrefix: null, - - // Given an optional header value, notify that a backoff has been requested. - _maybeNotifyBackoff: function (response, headerName) { - if (!this.observerPrefix || !response.headers) { - return; - } - let headerVal = response.headers[headerName]; - if (!headerVal) { - return; - } - let backoffInterval; - try { - backoffInterval = parseInt(headerVal, 10); - } catch (ex) { - log.error("hawkclient response had invalid backoff value in '" + - headerName + "' header: " + headerVal); - return; - } - Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); - }, - - // override points for testing. - newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) { - return new HAWKAuthenticatedRESTRequest(uri, credentials, extra); - }, - -} diff --git a/components/weave/src/common/hawkrequest.js b/components/weave/src/common/hawkrequest.js deleted file mode 100644 index ecedb0147..000000000 --- a/components/weave/src/common/hawkrequest.js +++ /dev/null @@ -1,198 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "HAWKAuthenticatedRESTRequest", - "deriveHawkCredentials" -]; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://gre/modules/Credentials.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", - "resource://services-crypto/utils.js"); - -const Prefs = new Preferences("services.common.rest."); - -/** - * Single-use HAWK-authenticated HTTP requests to RESTish resources. - * - * @param uri - * (String) URI for the RESTRequest constructor - * - * @param credentials - * (Object) Optional credentials for computing HAWK authentication - * header. - * - * @param payloadObj - * (Object) Optional object to be converted to JSON payload - * - * @param extra - * (Object) Optional extra params for HAWK header computation. - * Valid properties are: - * - * now: <current time in milliseconds>, - * localtimeOffsetMsec: <local clock offset vs server>, - * headers: <An object with header/value pairs to be sent - * as headers on the request> - * - * extra.localtimeOffsetMsec is the value in milliseconds that must be added to - * the local clock to make it agree with the server's clock. For instance, if - * the local clock is two minutes ahead of the server, the time offset in - * milliseconds will be -120000. - */ - -this.HAWKAuthenticatedRESTRequest = - function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) { - RESTRequest.call(this, uri); - - this.credentials = credentials; - this.now = extra.now || Date.now(); - this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; - this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); - this.extraHeaders = extra.headers || {}; - - // Expose for testing - this._intl = getIntl(); -}; -HAWKAuthenticatedRESTRequest.prototype = { - __proto__: RESTRequest.prototype, - - dispatch: function dispatch(method, data, onComplete, onProgress) { - let contentType = "text/plain"; - if (method == "POST" || method == "PUT" || method == "PATCH") { - contentType = "application/json"; - } - if (this.credentials) { - let options = { - now: this.now, - localtimeOffsetMsec: this.localtimeOffsetMsec, - credentials: this.credentials, - payload: data && JSON.stringify(data) || "", - contentType: contentType, - }; - let header = CryptoUtils.computeHAWK(this.uri, method, options); - this.setHeader("Authorization", header.field); - this._log.trace("hawk auth header: " + header.field); - } - - for (let header in this.extraHeaders) { - this.setHeader(header, this.extraHeaders[header]); - } - - this.setHeader("Content-Type", contentType); - - this.setHeader("Accept-Language", this._intl.accept_languages); - - return RESTRequest.prototype.dispatch.call( - this, method, data, onComplete, onProgress - ); - } -}; - - -/** - * Generic function to derive Hawk credentials. - * - * Hawk credentials are derived using shared secrets, which depend on the token - * in use. - * - * @param tokenHex - * The current session token encoded in hex - * @param context - * A context for the credentials. A protocol version will be prepended - * to the context, see Credentials.keyWord for more information. - * @param size - * The size in bytes of the expected derived buffer, - * defaults to 3 * 32. - * @return credentials - * Returns an object: - * { - * algorithm: sha256 - * id: the Hawk id (from the first 32 bytes derived) - * key: the Hawk key (from bytes 32 to 64) - * extra: size - 64 extra bytes (if size > 64) - * } - */ -this.deriveHawkCredentials = function deriveHawkCredentials(tokenHex, - context, - size = 96, - hexKey = false) { - let token = CommonUtils.hexToBytes(tokenHex); - let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size); - - let result = { - algorithm: "sha256", - key: hexKey ? CommonUtils.bytesAsHex(out.slice(32, 64)) : out.slice(32, 64), - id: CommonUtils.bytesAsHex(out.slice(0, 32)) - }; - if (size > 64) { - result.extra = out.slice(64); - } - - return result; -} - -// With hawk request, we send the user's accepted-languages with each request. -// To keep the number of times we read this pref at a minimum, maintain the -// preference in a stateful object that notices and updates itself when the -// pref is changed. -this.Intl = function Intl() { - // We won't actually query the pref until the first time we need it - this._accepted = ""; - this._everRead = false; - this._log = Log.repository.getLogger("Services.common.RESTRequest"); - this._log.level = Log.Level[Prefs.get("log.logger.rest.request")]; - this.init(); -}; - -this.Intl.prototype = { - init: function() { - Services.prefs.addObserver("intl.accept_languages", this, false); - }, - - uninit: function() { - Services.prefs.removeObserver("intl.accept_languages", this); - }, - - observe: function(subject, topic, data) { - this.readPref(); - }, - - readPref: function() { - this._everRead = true; - try { - this._accepted = Services.prefs.getComplexValue( - "intl.accept_languages", Ci.nsIPrefLocalizedString).data; - } catch (err) { - this._log.error("Error reading intl.accept_languages pref", err); - } - }, - - get accept_languages() { - if (!this._everRead) { - this.readPref(); - } - return this._accepted; - }, -}; - -// Singleton getter for Intl, creating an instance only when we first need it. -var intl = null; -function getIntl() { - if (!intl) { - intl = new Intl(); - } - return intl; -} - diff --git a/components/weave/src/common/logmanager.js b/components/weave/src/common/logmanager.js deleted file mode 100644 index c501229a9..000000000 --- a/components/weave/src/common/logmanager.js +++ /dev/null @@ -1,331 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Log", - "resource://gre/modules/Log.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", - "resource://gre/CommonUtils.jsm"); - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - -this.EXPORTED_SYMBOLS = [ - "LogManager", -]; - -const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days - -// "shared" logs (ie, where the same log name is used by multiple LogManager -// instances) are a fact of life here - eg, FirefoxAccounts logs are used by -// both Sync and Reading List. -// However, different instances have different pref branches, so we need to -// handle when one pref branch says "Debug" and the other says "Error" -// So we (a) keep singleton console and dump appenders and (b) keep track -// of the minimum (ie, most verbose) level and use that. -// This avoids (a) the most recent setter winning (as that is indeterminate) -// and (b) multiple dump/console appenders being added to the same log multiple -// times, which would cause messages to appear twice. - -// Singletons used by each instance. -var formatter; -var dumpAppender; -var consoleAppender; - -// A set of all preference roots used by all instances. -var allBranches = new Set(); - -// A storage appender that is flushable to a file on disk. Policies for -// when to flush, to what file, log rotation etc are up to the consumer -// (although it does maintain a .sawError property to help the consumer decide -// based on its policies) -function FlushableStorageAppender(formatter) { - Log.StorageStreamAppender.call(this, formatter); - this.sawError = false; -} - -FlushableStorageAppender.prototype = { - __proto__: Log.StorageStreamAppender.prototype, - - append(message) { - if (message.level >= Log.Level.Error) { - this.sawError = true; - } - Log.StorageStreamAppender.prototype.append.call(this, message); - }, - - reset() { - Log.StorageStreamAppender.prototype.reset.call(this); - this.sawError = false; - }, - - // Flush the current stream to a file. Somewhat counter-intuitively, you - // must pass a log which will be written to with details of the operation. - flushToFile: Task.async(function* (subdirArray, filename, log) { - let inStream = this.getInputStream(); - this.reset(); - if (!inStream) { - log.debug("Failed to flush log to a file - no input stream"); - return; - } - log.debug("Flushing file log"); - log.trace("Beginning stream copy to " + filename + ": " + Date.now()); - try { - yield this._copyStreamToFile(inStream, subdirArray, filename, log); - log.trace("onCopyComplete", Date.now()); - } catch (ex) { - log.error("Failed to copy log stream to file", ex); - } - }), - - /** - * Copy an input stream to the named file, doing everything off the main - * thread. - * subDirArray is an array of path components, relative to the profile - * directory, where the file will be created. - * outputFileName is the filename to create. - * Returns a promise that is resolved on completion or rejected with an error. - */ - _copyStreamToFile: Task.async(function* (inputStream, subdirArray, outputFileName, log) { - // The log data could be large, so we don't want to pass it all in a single - // message, so use BUFFER_SIZE chunks. - const BUFFER_SIZE = 8192; - - // get a binary stream - let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); - binaryStream.setInputStream(inputStream); - - let outputDirectory = OS.Path.join(OS.Constants.Path.profileDir, ...subdirArray); - yield OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir }); - let fullOutputFileName = OS.Path.join(outputDirectory, outputFileName); - let output = yield OS.File.open(fullOutputFileName, { write: true} ); - try { - while (true) { - let available = binaryStream.available(); - if (!available) { - break; - } - let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE)); - yield output.write(new Uint8Array(chunk)); - } - } finally { - try { - binaryStream.close(); // inputStream is closed by the binaryStream - yield output.close(); - } catch (ex) { - log.error("Failed to close the input stream", ex); - } - } - log.trace("finished copy to", fullOutputFileName); - }), -} - -// The public LogManager object. -function LogManager(prefRoot, logNames, logFilePrefix) { - this._prefObservers = []; - this.init(prefRoot, logNames, logFilePrefix); -} - -LogManager.prototype = { - _cleaningUpFileLogs: false, - - init(prefRoot, logNames, logFilePrefix) { - if (prefRoot instanceof Preferences) { - this._prefs = prefRoot; - } else { - this._prefs = new Preferences(prefRoot); - } - - this.logFilePrefix = logFilePrefix; - if (!formatter) { - // Create a formatter and various appenders to attach to the logs. - formatter = new Log.BasicFormatter(); - consoleAppender = new Log.ConsoleAppender(formatter); - dumpAppender = new Log.DumpAppender(formatter); - } - - allBranches.add(this._prefs._branchStr); - // We create a preference observer for all our prefs so they are magically - // reflected if the pref changes after creation. - let setupAppender = (appender, prefName, defaultLevel, findSmallest = false) => { - let observer = newVal => { - let level = Log.Level[newVal] || defaultLevel; - if (findSmallest) { - // As some of our appenders have global impact (ie, there is only one - // place 'dump' goes to), we need to find the smallest value from all - // prefs controlling this appender. - // For example, if consumerA has dump=Debug then consumerB sets - // dump=Error, we need to keep dump=Debug so consumerA is respected. - for (let branch of allBranches) { - let lookPrefBranch = new Preferences(branch); - let lookVal = Log.Level[lookPrefBranch.get(prefName)]; - if (lookVal && lookVal < level) { - level = lookVal; - } - } - } - appender.level = level; - } - this._prefs.observe(prefName, observer, this); - this._prefObservers.push([prefName, observer]); - // and call the observer now with the current pref value. - observer(this._prefs.get(prefName)); - return observer; - } - - this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Fatal, true); - this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true); - - // The file appender doesn't get the special singleton behaviour. - let fapp = this._fileAppender = new FlushableStorageAppender(formatter); - // the stream gets a default of Debug as the user must go out of their way - // to see the stuff spewed to it. - this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug); - - // now attach the appenders to all our logs. - for (let logName of logNames) { - let log = Log.repository.getLogger(logName); - for (let appender of [fapp, dumpAppender, consoleAppender]) { - log.addAppender(appender); - } - } - // and use the first specified log as a "root" for our log. - this._log = Log.repository.getLogger(logNames[0] + ".LogManager"); - }, - - /** - * Cleanup this instance - */ - finalize() { - for (let [name, pref] of this._prefObservers) { - this._prefs.ignore(name, pref, this); - } - this._prefObservers = []; - try { - allBranches.delete(this._prefs._branchStr); - } catch (e) {} - this._prefs = null; - }, - - get _logFileSubDirectoryEntries() { - // At this point we don't allow a custom directory for the logs, nor allow - // it to be outside the profile directory. - // This returns an array of the the relative directory entries below the - // profile dir, and is the directory about:sync-log uses. - return ["weave", "logs"]; - }, - - get sawError() { - return this._fileAppender.sawError; - }, - - // Result values for resetFileLog. - SUCCESS_LOG_WRITTEN: "success-log-written", - ERROR_LOG_WRITTEN: "error-log-written", - - /** - * Possibly generate a log file for all accumulated log messages and refresh - * the input & output streams. - * Whether a "success" or "error" log is written is determined based on - * whether an "Error" log entry was written to any of the logs. - * Returns a promise that resolves on completion with either null (for no - * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was - * written, or ERROR_LOG_WRITTEN if an "error" log was written. - */ - resetFileLog: Task.async(function* () { - try { - let flushToFile; - let reasonPrefix; - let reason; - if (this._fileAppender.sawError) { - reason = this.ERROR_LOG_WRITTEN; - flushToFile = this._prefs.get("log.appender.file.logOnError", true); - reasonPrefix = "error"; - } else { - reason = this.SUCCESS_LOG_WRITTEN; - flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false); - reasonPrefix = "success"; - } - - // might as well avoid creating an input stream if we aren't going to use it. - if (!flushToFile) { - this._fileAppender.reset(); - return null; - } - - // We have reasonPrefix at the start of the filename so all "error" - // logs are grouped in about:sync-log. - let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt"; - yield this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log); - - // It's not completely clear to markh why we only do log cleanups - // for errors, but for now the Sync semantics have been copied... - // (one theory is that only cleaning up on error makes it less - // likely old error logs would be removed, but that's not true if - // there are occasional errors - let's address this later!) - if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) { - this._log.trace("Scheduling cleanup."); - // Note we don't return/yield or otherwise wait on this promise - it - // continues in the background - this.cleanupLogs().catch(err => { - this._log.error("Failed to cleanup logs", err); - }); - } - return reason; - } catch (ex) { - this._log.error("Failed to resetFileLog", ex); - return null; - } - }), - - /** - * Finds all logs older than maxErrorAge and deletes them using async I/O. - */ - cleanupLogs: Task.async(function* () { - this._cleaningUpFileLogs = true; - let logDir = FileUtils.getDir("ProfD", this._logFileSubDirectoryEntries); - let iterator = new OS.File.DirectoryIterator(logDir.path); - let maxAge = this._prefs.get("log.appender.file.maxErrorAge", DEFAULT_MAX_ERROR_AGE); - let threshold = Date.now() - 1000 * maxAge; - - this._log.debug("Log cleanup threshold time: " + threshold); - yield iterator.forEach(Task.async(function* (entry) { - // Note that we don't check this.logFilePrefix is in the name - we cleanup - // all files in this directory regardless of that prefix so old logfiles - // for prefixes no longer in use are still cleaned up. See bug 1279145. - if (!entry.name.startsWith("error-") && - !entry.name.startsWith("success-")) { - return; - } - try { - // need to call .stat() as the enumerator doesn't give that to us on *nix. - let info = yield OS.File.stat(entry.path); - if (info.lastModificationDate.getTime() >= threshold) { - return; - } - this._log.trace(" > Cleanup removing " + entry.name + - " (" + info.lastModificationDate.getTime() + ")"); - yield OS.File.remove(entry.path); - this._log.trace("Deleted " + entry.name); - } catch (ex) { - this._log.debug("Encountered error trying to clean up old log file " - + entry.name, ex); - } - }.bind(this))); - iterator.close(); - this._cleaningUpFileLogs = false; - this._log.debug("Done deleting files."); - // This notification is used only for tests. - Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs", null); - }), -} diff --git a/components/weave/src/common/observers.js b/components/weave/src/common/observers.js deleted file mode 100644 index c0b771048..000000000 --- a/components/weave/src/common/observers.js +++ /dev/null @@ -1,150 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Observers"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -/** - * A service for adding, removing and notifying observers of notifications. - * Wraps the nsIObserverService interface. - * - * @version 0.2 - */ -this.Observers = { - /** - * Register the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic to observe - * - * @param callback {Object} - * the callback; an Object that implements nsIObserver or a Function - * that gets called when the notification occurs - * - * @param thisObject {Object} [optional] - * the object to use as |this| when calling a Function callback - * - * @returns the observer - */ - add: function(topic, callback, thisObject) { - let observer = new Observer(topic, callback, thisObject); - this._cache.push(observer); - this._service.addObserver(observer, topic, true); - - return observer; - }, - - /** - * Unregister the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic being observed - * - * @param callback {Object} - * the callback doing the observing - * - * @param thisObject {Object} [optional] - * the object being used as |this| when calling a Function callback - */ - remove: function(topic, callback, thisObject) { - // This seems fairly inefficient, but I'm not sure how much better - // we can make it. We could index by topic, but we can't index by callback - // or thisObject, as far as I know, since the keys to JavaScript hashes - // (a.k.a. objects) can apparently only be primitive values. - let [observer] = this._cache.filter(v => v.topic == topic && - v.callback == callback && - v.thisObject == thisObject); - if (observer) { - this._service.removeObserver(observer, topic); - this._cache.splice(this._cache.indexOf(observer), 1); - } - }, - - /** - * Notify observers about something. - * - * @param topic {String} - * the topic to notify observers about - * - * @param subject {Object} [optional] - * some information about the topic; can be any JS object or primitive - * - * @param data {String} [optional] [deprecated] - * some more information about the topic; deprecated as the subject - * is sufficient to pass all needed information to the JS observers - * that this module targets; if you have multiple values to pass to - * the observer, wrap them in an object and pass them via the subject - * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) - */ - notify: function(topic, subject, data) { - subject = (typeof subject == "undefined") ? null : new Subject(subject); - data = (typeof data == "undefined") ? null : data; - this._service.notifyObservers(subject, topic, data); - }, - - _service: Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService), - - /** - * A cache of observers that have been added. - * - * We use this to remove observers when a caller calls |remove|. - * - * XXX This might result in reference cycles, causing memory leaks, - * if we hold a reference to an observer that holds a reference to us. - * Could we fix that by making this an independent top-level object - * rather than a property of this object? - */ - _cache: [] -}; - - -function Observer(topic, callback, thisObject) { - this.topic = topic; - this.callback = callback; - this.thisObject = thisObject; -} - -Observer.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), - observe: function(subject, topic, data) { - // Extract the wrapped object for subjects that are one of our wrappers - // around a JS object. This way we support both wrapped subjects created - // using this module and those that are real XPCOM components. - if (subject && typeof subject == "object" && - ("wrappedJSObject" in subject) && - ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) - subject = subject.wrappedJSObject.object; - - if (typeof this.callback == "function") { - if (this.thisObject) - this.callback.call(this.thisObject, subject, data); - else - this.callback(subject, data); - } - else // typeof this.callback == "object" (nsIObserver) - this.callback.observe(subject, topic, data); - } -} - - -function Subject(object) { - // Double-wrap the object and set a property identifying the wrappedJSObject - // as one of our wrappers to distinguish between subjects that are one of our - // wrappers (which we should unwrap when notifying our observers) and those - // that are real JS XPCOM components (which we should pass through unaltered). - this.wrappedJSObject = { observersModuleSubjectWrapper: true, object: object }; -} - -Subject.prototype = { - QueryInterface: XPCOMUtils.generateQI([]), - getScriptableHelper: function() {}, - getInterfaces: function() {} -}; diff --git a/components/weave/src/common/rest.js b/components/weave/src/common/rest.js deleted file mode 100644 index 22b2ebbba..000000000 --- a/components/weave/src/common/rest.js +++ /dev/null @@ -1,764 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "RESTRequest", - "RESTResponse", - "TokenAuthenticatedRESTRequest", -]; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/CommonUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", - "resource://services-crypto/utils.js"); - -const Prefs = new Preferences("services.common."); - -/** - * Single use HTTP requests to RESTish resources. - * - * @param uri - * URI for the request. This can be an nsIURI object or a string - * that can be used to create one. An exception will be thrown if - * the string is not a valid URI. - * - * Examples: - * - * (1) Quick GET request: - * - * new RESTRequest("http://server/rest/resource").get(function (error) { - * if (error) { - * // Deal with a network error. - * processNetworkErrorCode(error.result); - * return; - * } - * if (!this.response.success) { - * // Bail out if we're not getting an HTTP 2xx code. - * processHTTPError(this.response.status); - * return; - * } - * processData(this.response.body); - * }); - * - * (2) Quick PUT request (non-string data is automatically JSONified) - * - * new RESTRequest("http://server/rest/resource").put(data, function (error) { - * ... - * }); - * - * (3) Streaming GET - * - * let request = new RESTRequest("http://server/rest/resource"); - * request.setHeader("Accept", "application/newlines"); - * request.onComplete = function (error) { - * if (error) { - * // Deal with a network error. - * processNetworkErrorCode(error.result); - * return; - * } - * callbackAfterRequestHasCompleted() - * }); - * request.onProgress = function () { - * if (!this.response.success) { - * // Bail out if we're not getting an HTTP 2xx code. - * return; - * } - * // Process body data and reset it so we don't process the same data twice. - * processIncrementalData(this.response.body); - * this.response.body = ""; - * }); - * request.get(); - */ -this.RESTRequest = function RESTRequest(uri) { - this.status = this.NOT_SENT; - - // If we don't have an nsIURI object yet, make one. This will throw if - // 'uri' isn't a valid URI string. - if (!(uri instanceof Ci.nsIURI)) { - uri = Services.io.newURI(uri, null, null); - } - this.uri = uri; - - this._headers = {}; - this._log = Log.repository.getLogger(this._logName); - this._log.level = - Log.Level[Prefs.get("log.logger.rest.request")]; -} -RESTRequest.prototype = { - - _logName: "Services.Common.RESTRequest", - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIBadCertListener2, - Ci.nsIInterfaceRequestor, - Ci.nsIChannelEventSink - ]), - - /*** Public API: ***/ - - /** - * A constant boolean that indicates whether this object will automatically - * utf-8 encode request bodies passed as an object. Used for feature detection - * so, eg, loop can use the same source code for old and new Firefox versions. - */ - willUTF8EncodeObjectRequests: true, - - /** - * URI for the request (an nsIURI object). - */ - uri: null, - - /** - * HTTP method (e.g. "GET") - */ - method: null, - - /** - * RESTResponse object - */ - response: null, - - /** - * nsIRequest load flags. Don't do any caching by default. Don't send user - * cookies and such over the wire (Bug 644734). - */ - loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS, - - /** - * nsIHttpChannel - */ - channel: null, - - /** - * Flag to indicate the status of the request. - * - * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. - */ - status: null, - - NOT_SENT: 0, - SENT: 1, - IN_PROGRESS: 2, - COMPLETED: 4, - ABORTED: 8, - - /** - * HTTP status text of response - */ - statusText: null, - - /** - * Request timeout (in seconds, though decimal values can be used for - * up to millisecond granularity.) - * - * 0 for no timeout. - */ - timeout: null, - - /** - * The encoding with which the response to this request must be treated. - * If a charset parameter is available in the HTTP Content-Type header for - * this response, that will always be used, and this value is ignored. We - * default to UTF-8 because that is a reasonable default. - */ - charset: "utf-8", - - /** - * Called when the request has been completed, including failures and - * timeouts. - * - * @param error - * Error that occurred while making the request, null if there - * was no error. - */ - onComplete: function onComplete(error) { - }, - - /** - * Called whenever data is being received on the channel. If this throws an - * exception, the request is aborted and the exception is passed as the - * error to onComplete(). - */ - onProgress: function onProgress() { - }, - - /** - * Set a request header. - */ - setHeader: function setHeader(name, value) { - this._headers[name.toLowerCase()] = value; - }, - - /** - * Perform an HTTP GET. - * - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - get: function get(onComplete, onProgress) { - return this.dispatch("GET", null, onComplete, onProgress); - }, - - /** - * Perform an HTTP PATCH. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - patch: function patch(data, onComplete, onProgress) { - return this.dispatch("PATCH", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP PUT. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - put: function put(data, onComplete, onProgress) { - return this.dispatch("PUT", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP POST. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - post: function post(data, onComplete, onProgress) { - return this.dispatch("POST", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP DELETE. - * - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - delete: function delete_(onComplete, onProgress) { - return this.dispatch("DELETE", null, onComplete, onProgress); - }, - - /** - * Abort an active request. - */ - abort: function abort() { - if (this.status != this.SENT && this.status != this.IN_PROGRESS) { - throw "Can only abort a request that has been sent."; - } - - this.status = this.ABORTED; - this.channel.cancel(Cr.NS_BINDING_ABORTED); - - if (this.timeoutTimer) { - // Clear the abort timer now that the channel is done. - this.timeoutTimer.clear(); - } - }, - - /*** Implementation stuff ***/ - - dispatch: function dispatch(method, data, onComplete, onProgress) { - if (this.status != this.NOT_SENT) { - throw "Request has already been sent!"; - } - - this.method = method; - if (onComplete) { - this.onComplete = onComplete; - } - if (onProgress) { - this.onProgress = onProgress; - } - - // Create and initialize HTTP channel. - let channel = NetUtil.newChannel({uri: this.uri, loadUsingSystemPrincipal: true}) - .QueryInterface(Ci.nsIRequest) - .QueryInterface(Ci.nsIHttpChannel); - this.channel = channel; - channel.loadFlags |= this.loadFlags; - channel.notificationCallbacks = this; - - this._log.debug(`${method} request to ${this.uri.spec}`); - // Set request headers. - let headers = this._headers; - for (let key in headers) { - if (key == 'authorization') { - this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); - } else { - this._log.trace("HTTP Header " + key + ": " + headers[key]); - } - channel.setRequestHeader(key, headers[key], false); - } - - // Set HTTP request body. - if (method == "PUT" || method == "POST" || method == "PATCH") { - // Convert non-string bodies into JSON with utf-8 encoding. If a string - // is passed we assume they've already encoded it. - let contentType = headers["content-type"]; - if (typeof data != "string") { - data = JSON.stringify(data); - if (!contentType) { - contentType = "application/json"; - } - if (!contentType.includes("charset")) { - data = CommonUtils.encodeUTF8(data); - contentType += "; charset=utf-8"; - } else { - // If someone handed us an object but also a custom content-type - // it's probably confused. We could go to even further lengths to - // respect it, but this shouldn't happen in practice. - Cu.reportError("rest.js found an object to JSON.stringify but also a " + - "content-type header with a charset specification. " + - "This probably isn't going to do what you expect"); - } - } - if (!contentType) { - contentType = "text/plain"; - } - - this._log.debug(method + " Length: " + data.length); - if (this._log.level <= Log.Level.Trace) { - this._log.trace(method + " Body: " + data); - } - - let stream = Cc["@mozilla.org/io/string-input-stream;1"] - .createInstance(Ci.nsIStringInputStream); - stream.setData(data, data.length); - - channel.QueryInterface(Ci.nsIUploadChannel); - channel.setUploadStream(stream, contentType, data.length); - } - // We must set this after setting the upload stream, otherwise it - // will always be 'PUT'. Yeah, I know. - channel.requestMethod = method; - - // Before opening the channel, set the charset that serves as a hint - // as to what the response might be encoded as. - channel.contentCharset = this.charset; - - // Blast off! - try { - channel.asyncOpen2(this); - } catch (ex) { - // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. - this._log.warn("Caught an error in asyncOpen", ex); - CommonUtils.nextTick(onComplete.bind(this, ex)); - } - this.status = this.SENT; - this.delayTimeout(); - return this; - }, - - /** - * Create or push back the abort timer that kills this request. - */ - delayTimeout: function delayTimeout() { - if (this.timeout) { - CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this, - "timeoutTimer"); - } - }, - - /** - * Abort the request based on a timeout. - */ - abortTimeout: function abortTimeout() { - this.abort(); - let error = Components.Exception("Aborting due to channel inactivity.", - Cr.NS_ERROR_NET_TIMEOUT); - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "abortTimeout."); - return; - } - this.onComplete(error); - }, - - /*** nsIStreamListener ***/ - - onStartRequest: function onStartRequest(channel) { - if (this.status == this.ABORTED) { - this._log.trace("Not proceeding with onStartRequest, request was aborted."); - return; - } - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); - this.status = this.ABORTED; - channel.cancel(Cr.NS_BINDING_ABORTED); - return; - } - - this.status = this.IN_PROGRESS; - - this._log.trace("onStartRequest: " + channel.requestMethod + " " + - channel.URI.spec); - - // Create a response object and fill it with some data. - let response = this.response = new RESTResponse(); - response.request = this; - response.body = ""; - - this.delayTimeout(); - }, - - onStopRequest: function onStopRequest(channel, context, statusCode) { - if (this.timeoutTimer) { - // Clear the abort timer now that the channel is done. - this.timeoutTimer.clear(); - } - - // We don't want to do anything for a request that's already been aborted. - if (this.status == this.ABORTED) { - this._log.trace("Not proceeding with onStopRequest, request was aborted."); - return; - } - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - this.status = this.ABORTED; - return; - } - this.status = this.COMPLETED; - - let statusSuccess = Components.isSuccessCode(statusCode); - let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; - this._log.trace("Channel for " + channel.requestMethod + " " + uri + - " returned status code " + statusCode); - - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "abortRequest."); - this.onProgress = null; - return; - } - - // Throw the failure code and stop execution. Use Components.Exception() - // instead of Error() so the exception is QI-able and can be passed across - // XPCOM borders while preserving the status code. - if (!statusSuccess) { - let message = Components.Exception("", statusCode).name; - let error = Components.Exception(message, statusCode); - this._log.debug(this.method + " " + uri + " failed: " + statusCode + " - " + message); - this.onComplete(error); - this.onComplete = this.onProgress = null; - return; - } - - this._log.debug(this.method + " " + uri + " " + this.response.status); - - // Additionally give the full response body when Trace logging. - if (this._log.level <= Log.Level.Trace) { - this._log.trace(this.method + " body: " + this.response.body); - } - - delete this._inputStream; - - this.onComplete(null); - this.onComplete = this.onProgress = null; - }, - - onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) { - // We get an nsIRequest, which doesn't have contentCharset. - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - this.abort(); - - if (this.onComplete) { - this.onComplete(ex); - } - - this.onComplete = this.onProgress = null; - return; - } - - if (channel.contentCharset) { - this.response.charset = channel.contentCharset; - - if (!this._converterStream) { - this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Ci.nsIConverterInputStream); - } - - this._converterStream.init(stream, channel.contentCharset, 0, - this._converterStream.DEFAULT_REPLACEMENT_CHARACTER); - - try { - let str = {}; - let num = this._converterStream.readString(count, str); - if (num != 0) { - this.response.body += str.value; - } - } catch (ex) { - this._log.warn("Exception thrown reading " + count + " bytes from " + - "the channel", ex); - throw ex; - } - } else { - this.response.charset = null; - - if (!this._inputStream) { - this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"] - .createInstance(Ci.nsIScriptableInputStream); - } - - this._inputStream.init(stream); - - this.response.body += this._inputStream.read(count); - } - - try { - this.onProgress(); - } catch (ex) { - this._log.warn("Got exception calling onProgress handler, aborting " + - this.method + " " + channel.URI.spec, ex); - this.abort(); - - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "onDataAvailable."); - this.onProgress = null; - return; - } - - this.onComplete(ex); - this.onComplete = this.onProgress = null; - return; - } - - this.delayTimeout(); - }, - - /*** nsIInterfaceRequestor ***/ - - getInterface: function(aIID) { - return this.QueryInterface(aIID); - }, - - /*** nsIBadCertListener2 ***/ - - notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) { - this._log.warn("Invalid HTTPS certificate encountered!"); - // Suppress invalid HTTPS certificate warnings in the UI. - // (The request will still fail.) - return true; - }, - - /** - * Returns true if headers from the old channel should be - * copied to the new channel. Invoked when a channel redirect - * is in progress. - */ - shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) { - let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); - let isSameURI = newChannel.URI.equals(oldChannel.URI); - this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " + - newChannel.URI.spec + ", internal = " + isInternal); - return isInternal && isSameURI; - }, - - /*** nsIChannelEventSink ***/ - asyncOnChannelRedirect: - function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { - - let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>"; - let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>"; - this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags); - - try { - newChannel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); - return; - } - - // For internal redirects, copy the headers that our caller set. - try { - if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { - this._log.trace("Copying headers for safe internal redirect."); - for (let key in this._headers) { - newChannel.setRequestHeader(key, this._headers[key], false); - } - } - } catch (ex) { - this._log.error("Error copying headers", ex); - } - - this.channel = newChannel; - - // We let all redirects proceed. - callback.onRedirectVerifyCallback(Cr.NS_OK); - } -}; - -/** - * Response object for a RESTRequest. This will be created automatically by - * the RESTRequest. - */ -this.RESTResponse = function RESTResponse() { - this._log = Log.repository.getLogger(this._logName); - this._log.level = - Log.Level[Prefs.get("log.logger.rest.response")]; -} -RESTResponse.prototype = { - - _logName: "Services.Common.RESTResponse", - - /** - * Corresponding REST request - */ - request: null, - - /** - * HTTP status code - */ - get status() { - let status; - try { - status = this.request.channel.responseStatus; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP status code", ex); - return null; - } - Object.defineProperty(this, "status", {value: status}); - return status; - }, - - /** - * HTTP status text - */ - get statusText() { - let statusText; - try { - statusText = this.request.channel.responseStatusText; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP status text", ex); - return null; - } - Object.defineProperty(this, "statusText", {value: statusText}); - return statusText; - }, - - /** - * Boolean flag that indicates whether the HTTP status code is 2xx or not. - */ - get success() { - let success; - try { - success = this.request.channel.requestSucceeded; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP success flag", ex); - return null; - } - Object.defineProperty(this, "success", {value: success}); - return success; - }, - - /** - * Object containing HTTP headers (keyed as lower case) - */ - get headers() { - let headers = {}; - try { - this._log.trace("Processing response headers."); - let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); - channel.visitResponseHeaders(function (header, value) { - headers[header.toLowerCase()] = value; - }); - } catch (ex) { - this._log.debug("Caught exception processing response headers", ex); - return null; - } - - Object.defineProperty(this, "headers", {value: headers}); - return headers; - }, - - /** - * HTTP body (string) - */ - body: null - -}; - -/** - * Single use MAC authenticated HTTP requests to RESTish resources. - * - * @param uri - * URI going to the RESTRequest constructor. - * @param authToken - * (Object) An auth token of the form {id: (string), key: (string)} - * from which the MAC Authentication header for this request will be - * derived. A token as obtained from - * TokenServerClient.getTokenFromBrowserIDAssertion is accepted. - * @param extra - * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, - * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on - * the purpose of these values. - */ -this.TokenAuthenticatedRESTRequest = - function TokenAuthenticatedRESTRequest(uri, authToken, extra) { - RESTRequest.call(this, uri); - this.authToken = authToken; - this.extra = extra || {}; -} -TokenAuthenticatedRESTRequest.prototype = { - __proto__: RESTRequest.prototype, - - dispatch: function dispatch(method, data, onComplete, onProgress) { - let sig = CryptoUtils.computeHTTPMACSHA1( - this.authToken.id, this.authToken.key, method, this.uri, this.extra - ); - - this.setHeader("Authorization", sig.getHeader()); - - return RESTRequest.prototype.dispatch.call( - this, method, data, onComplete, onProgress - ); - }, -}; diff --git a/components/weave/src/common/services-common.js b/components/weave/src/common/services-common.js deleted file mode 100644 index bc37d4028..000000000 --- a/components/weave/src/common/services-common.js +++ /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/. */ - -// This file contains default preference values for components in -// services-common. - -pref("services.common.log.logger.rest.request", "Debug"); -pref("services.common.log.logger.rest.response", "Debug"); - -pref("services.common.log.logger.tokenserverclient", "Debug"); diff --git a/components/weave/src/common/stringbundle.js b/components/weave/src/common/stringbundle.js deleted file mode 100644 index a07fa4831..000000000 --- a/components/weave/src/common/stringbundle.js +++ /dev/null @@ -1,203 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["StringBundle"]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -/** - * A string bundle. - * - * This object presents two APIs: a deprecated one that is equivalent to the API - * for the stringbundle XBL binding, to make it easy to switch from that binding - * to this module, and a new one that is simpler and easier to use. - * - * The benefit of this module over the XBL binding is that it can also be used - * in JavaScript modules and components, not only in chrome JS. - * - * To use this module, import it, create a new instance of StringBundle, - * and then use the instance's |get| and |getAll| methods to retrieve strings - * (you can get both plain and formatted strings with |get|): - * - * let strings = - * new StringBundle("chrome://example/locale/strings.properties"); - * let foo = strings.get("foo"); - * let barFormatted = strings.get("bar", [arg1, arg2]); - * for (let string of strings.getAll()) - * dump (string.key + " = " + string.value + "\n"); - * - * @param url {String} - * the URL of the string bundle - */ -this.StringBundle = function StringBundle(url) { - this.url = url; -} - -StringBundle.prototype = { - /** - * the locale associated with the application - * @type nsILocale - * @private - */ - get _appLocale() { - try { - return Cc["@mozilla.org/intl/nslocaleservice;1"]. - getService(Ci.nsILocaleService). - getApplicationLocale(); - } - catch(ex) { - return null; - } - }, - - /** - * the wrapped nsIStringBundle - * @type nsIStringBundle - * @private - */ - get _stringBundle() { - let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. - getService(Ci.nsIStringBundleService). - createBundle(this.url, this._appLocale); - this.__defineGetter__("_stringBundle", () => stringBundle); - return this._stringBundle; - }, - - - // the new API - - /** - * the URL of the string bundle - * @type String - */ - _url: null, - get url() { - return this._url; - }, - set url(newVal) { - this._url = newVal; - delete this._stringBundle; - }, - - /** - * Get a string from the bundle. - * - * @param key {String} - * the identifier of the string to get - * @param args {array} [optional] - * an array of arguments that replace occurrences of %S in the string - * - * @returns {String} the value of the string - */ - get: function(key, args) { - if (args) - return this.stringBundle.formatStringFromName(key, args, args.length); - else - return this.stringBundle.GetStringFromName(key); - }, - - /** - * Get all the strings in the bundle. - * - * @returns {Array} - * an array of objects with key and value properties - */ - getAll: function() { - let strings = []; - - // FIXME: for performance, return an enumerable array that wraps the string - // bundle's nsISimpleEnumerator (does JavaScript already support this?). - - let enumerator = this.stringBundle.getSimpleEnumeration(); - - while (enumerator.hasMoreElements()) { - // We could simply return the nsIPropertyElement objects, but I think - // it's better to return standard JS objects that behave as consumers - // expect JS objects to behave (f.e. you can modify them dynamically). - let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - strings.push({ key: string.key, value: string.value }); - } - - return strings; - }, - - - // the deprecated XBL binding-compatible API - - /** - * the URL of the string bundle - * @deprecated because its name doesn't make sense outside of an XBL binding - * @type String - */ - get src() { - return this.url; - }, - set src(newVal) { - this.url = newVal; - }, - - /** - * the locale associated with the application - * @deprecated because it has never been used outside the XBL binding itself, - * and consumers should obtain it directly from the locale service anyway. - * @type nsILocale - */ - get appLocale() { - return this._appLocale; - }, - - /** - * the wrapped nsIStringBundle - * @deprecated because this module should provide all necessary functionality - * @type nsIStringBundle - * - * If you do ever need to use this, let the authors of this module know why - * so they can surface functionality for your use case in the module itself - * and you don't have to access this underlying XPCOM component. - */ - get stringBundle() { - return this._stringBundle; - }, - - /** - * Get a string from the bundle. - * @deprecated use |get| instead - * - * @param key {String} - * the identifier of the string to get - * - * @returns {String} - * the value of the string - */ - getString: function(key) { - return this.get(key); - }, - - /** - * Get a formatted string from the bundle. - * @deprecated use |get| instead - * - * @param key {string} - * the identifier of the string to get - * @param args {array} - * an array of arguments that replace occurrences of %S in the string - * - * @returns {String} - * the formatted value of the string - */ - getFormattedString: function(key, args) { - return this.get(key, args); - }, - - /** - * Get an enumeration of the strings in the bundle. - * @deprecated use |getAll| instead - * - * @returns {nsISimpleEnumerator} - * a enumeration of the strings in the bundle - */ - get strings() { - return this.stringBundle.getSimpleEnumeration(); - } -} diff --git a/components/weave/src/common/tokenserverclient.js b/components/weave/src/common/tokenserverclient.js deleted file mode 100644 index ca40f7d93..000000000 --- a/components/weave/src/common/tokenserverclient.js +++ /dev/null @@ -1,459 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "TokenServerClient", - "TokenServerClientError", - "TokenServerClientNetworkError", - "TokenServerClientServerError", -]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-common/observers.js"); - -const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient"; - -/** - * Represents a TokenServerClient error that occurred on the client. - * - * This is the base type for all errors raised by client operations. - * - * @param message - * (string) Error message. - */ -this.TokenServerClientError = function TokenServerClientError(message) { - this.name = "TokenServerClientError"; - this.message = message || "Client error."; - // Without explicitly setting .stack, all stacks from these errors will point - // to the "new Error()" call a few lines down, which isn't helpful. - this.stack = Error().stack; -} -TokenServerClientError.prototype = new Error(); -TokenServerClientError.prototype.constructor = TokenServerClientError; -TokenServerClientError.prototype._toStringFields = function() { - return {message: this.message}; -} -TokenServerClientError.prototype.toString = function() { - return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; -} -TokenServerClientError.prototype.toJSON = function() { - let result = this._toStringFields(); - result["name"] = this.name; - return result; -} - -/** - * Represents a TokenServerClient error that occurred in the network layer. - * - * @param error - * The underlying error thrown by the network layer. - */ -this.TokenServerClientNetworkError = - function TokenServerClientNetworkError(error) { - this.name = "TokenServerClientNetworkError"; - this.error = error; - this.stack = Error().stack; -} -TokenServerClientNetworkError.prototype = new TokenServerClientError(); -TokenServerClientNetworkError.prototype.constructor = - TokenServerClientNetworkError; -TokenServerClientNetworkError.prototype._toStringFields = function() { - return {error: this.error}; -} - -/** - * Represents a TokenServerClient error that occurred on the server. - * - * This type will be encountered for all non-200 response codes from the - * server. The type of error is strongly enumerated and is stored in the - * `cause` property. This property can have the following string values: - * - * conditions-required -- The server is requesting that the client - * agree to service conditions before it can obtain a token. The - * conditions that must be presented to the user and agreed to are in - * the `urls` mapping on the instance. Keys of this mapping are - * identifiers. Values are string URLs. - * - * invalid-credentials -- A token could not be obtained because - * the credentials presented by the client were invalid. - * - * unknown-service -- The requested service was not found. - * - * malformed-request -- The server rejected the request because it - * was invalid. If you see this, code in this file is likely wrong. - * - * malformed-response -- The response from the server was not what was - * expected. - * - * general -- A general server error has occurred. Clients should - * interpret this as an opaque failure. - * - * @param message - * (string) Error message. - */ -this.TokenServerClientServerError = - function TokenServerClientServerError(message, cause="general") { - this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues. - this.name = "TokenServerClientServerError"; - this.message = message || "Server error."; - this.cause = cause; - this.stack = Error().stack; -} -TokenServerClientServerError.prototype = new TokenServerClientError(); -TokenServerClientServerError.prototype.constructor = - TokenServerClientServerError; - -TokenServerClientServerError.prototype._toStringFields = function() { - let fields = { - now: this.now, - message: this.message, - cause: this.cause, - }; - if (this.response) { - fields.response_body = this.response.body; - fields.response_headers = this.response.headers; - fields.response_status = this.response.status; - } - return fields; -}; - -/** - * Represents a client to the Token Server. - * - * http://docs.services.mozilla.com/token/index.html - * - * The Token Server supports obtaining tokens for arbitrary apps by - * constructing URI paths of the form <app>/<app_version>. However, the service - * discovery mechanism emphasizes the use of full URIs and tries to not force - * the client to manipulate URIs. This client currently enforces this practice - * by not implementing an API which would perform URI manipulation. - * - * If you are tempted to implement this API in the future, consider this your - * warning that you may be doing it wrong and that you should store full URIs - * instead. - * - * Areas to Improve: - * - * - The server sends a JSON response on error. The client does not currently - * parse this. It might be convenient if it did. - * - Currently most non-200 status codes are rolled into one error type. It - * might be helpful if callers had a richer API that communicated who was - * at fault (e.g. differentiating a 503 from a 401). - */ -this.TokenServerClient = function TokenServerClient() { - this._log = Log.repository.getLogger("Common.TokenServerClient"); - let level = Services.prefs.getCharPref(PREF_LOG_LEVEL, "Debug"); - this._log.level = Log.Level[level]; -} -TokenServerClient.prototype = { - /** - * Logger instance. - */ - _log: null, - - /** - * Obtain a token from a BrowserID assertion against a specific URL. - * - * This asynchronously obtains the token. The callback receives 2 arguments: - * - * (TokenServerClientError | null) If no token could be obtained, this - * will be a TokenServerClientError instance describing why. The - * type seen defines the type of error encountered. If an HTTP response - * was seen, a RESTResponse instance will be stored in the `response` - * property of this object. If there was no error and a token is - * available, this will be null. - * - * (map | null) On success, this will be a map containing the results from - * the server. If there was an error, this will be null. The map has the - * following properties: - * - * id (string) HTTP MAC public key identifier. - * key (string) HTTP MAC shared symmetric key. - * endpoint (string) URL where service can be connected to. - * uid (string) user ID for requested service. - * duration (string) the validity duration of the issued token. - * - * Terms of Service Acceptance - * --------------------------- - * - * Some services require users to accept terms of service before they can - * obtain a token. If a service requires ToS acceptance, the error passed - * to the callback will be a `TokenServerClientServerError` with the - * `cause` property set to "conditions-required". The `urls` property of that - * instance will be a map of string keys to string URL values. The user-agent - * should prompt the user to accept the content at these URLs. - * - * Clients signify acceptance of the terms of service by sending a token - * request with additional metadata. This is controlled by the - * `conditionsAccepted` argument to this function. Clients only need to set - * this flag once per service and the server remembers acceptance. If - * the conditions for the service change, the server may request - * clients agree to terms again. Therefore, clients should always be - * prepared to handle a conditions required response. - * - * Clients should not blindly send acceptance to conditions. Instead, clients - * should set `conditionsAccepted` if and only if the server asks for - * acceptance, the conditions are displayed to the user, and the user agrees - * to them. - * - * Example Usage - * ------------- - * - * let client = new TokenServerClient(); - * let assertion = getBrowserIDAssertionFromSomewhere(); - * let url = "https://token.services.mozilla.com/1.0/sync/2.0"; - * - * client.getTokenFromBrowserIDAssertion(url, assertion, - * function onResponse(error, result) { - * if (error) { - * if (error.cause == "conditions-required") { - * promptConditionsAcceptance(error.urls, function onAccept() { - * client.getTokenFromBrowserIDAssertion(url, assertion, - * onResponse, true); - * } - * return; - * } - * - * // Do other error handling. - * return; - * } - * - * let { - * id: id, key: key, uid: uid, endpoint: endpoint, duration: duration - * } = result; - * // Do stuff with data and carry on. - * }); - * - * @param url - * (string) URL to fetch token from. - * @param assertion - * (string) BrowserID assertion to exchange token for. - * @param cb - * (function) Callback to be invoked with result of operation. - * @param conditionsAccepted - * (bool) Whether to send acceptance to service conditions. - */ - getTokenFromBrowserIDAssertion: - function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders={}) { - if (!url) { - throw new TokenServerClientError("url argument is not valid."); - } - - if (!assertion) { - throw new TokenServerClientError("assertion argument is not valid."); - } - - if (!cb) { - throw new TokenServerClientError("cb argument is not valid."); - } - - this._log.debug("Beginning BID assertion exchange: " + url); - - let req = this.newRESTRequest(url); - req.setHeader("Accept", "application/json"); - req.setHeader("Authorization", "BrowserID " + assertion); - - for (let header in addHeaders) { - req.setHeader(header, addHeaders[header]); - } - - let client = this; - req.get(function onResponse(error) { - if (error) { - cb(new TokenServerClientNetworkError(error), null); - return; - } - - let self = this; - function callCallback(error, result) { - if (!cb) { - self._log.warn("Callback already called! Did it throw?"); - return; - } - - try { - cb(error, result); - } catch (ex) { - self._log.warn("Exception when calling user-supplied callback", ex); - } - - cb = null; - } - - try { - client._processTokenResponse(this.response, callCallback); - } catch (ex) { - this._log.warn("Error processing token server response", ex); - - let error = new TokenServerClientError(ex); - error.response = this.response; - callCallback(error, null); - } - }); - }, - - /** - * Handler to process token request responses. - * - * @param response - * RESTResponse from token HTTP request. - * @param cb - * The original callback passed to the public API. - */ - _processTokenResponse: function processTokenResponse(response, cb) { - this._log.debug("Got token response: " + response.status); - - // 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. - let ct = response.headers["content-type"] || ""; - if (ct != "application/json" && !ct.startsWith("application/json;")) { - this._log.warn("Did not receive JSON response. Misconfigured server?"); - this._log.debug("Content-Type: " + ct); - this._log.debug("Body: " + response.body); - - let error = new TokenServerClientServerError("Non-JSON response.", - "malformed-response"); - error.response = response; - cb(error, null); - return; - } - - let result; - try { - result = JSON.parse(response.body); - } catch (ex) { - this._log.warn("Invalid JSON returned by server: " + response.body); - let error = new TokenServerClientServerError("Malformed JSON.", - "malformed-response"); - error.response = response; - cb(error, null); - return; - } - - // Any response status can have X-Backoff or X-Weave-Backoff headers. - this._maybeNotifyBackoff(response, "x-weave-backoff"); - this._maybeNotifyBackoff(response, "x-backoff"); - - // The service shouldn't have any 3xx, so we don't need to handle those. - if (response.status != 200) { - // We /should/ have a Cornice error report in the JSON. We log that to - // help with debugging. - if ("errors" in result) { - // This could throw, but this entire function is wrapped in a try. If - // the server is sending something not an array of objects, it has - // failed to keep its contract with us and there is little we can do. - for (let error of result.errors) { - this._log.info("Server-reported error: " + JSON.stringify(error)); - } - } - - let error = new TokenServerClientServerError(); - error.response = response; - - if (response.status == 400) { - error.message = "Malformed request."; - error.cause = "malformed-request"; - } else if (response.status == 401) { - // Cause can be invalid-credentials, invalid-timestamp, or - // invalid-generation. - error.message = "Authentication failed."; - error.cause = result.status; - } - - // 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. - else if (response.status == 403) { - if (!("urls" in result)) { - this._log.warn("403 response without proper fields!"); - this._log.warn("Response body: " + response.body); - - error.message = "Missing JSON fields."; - error.cause = "malformed-response"; - } else if (typeof(result.urls) != "object") { - error.message = "urls field is not a map."; - error.cause = "malformed-response"; - } else { - error.message = "Conditions must be accepted."; - error.cause = "conditions-required"; - error.urls = result.urls; - } - } else if (response.status == 404) { - error.message = "Unknown service."; - error.cause = "unknown-service"; - } - - // A Retry-After header should theoretically only appear on a 503, but - // we'll look for it on any error response. - this._maybeNotifyBackoff(response, "retry-after"); - - cb(error, null); - return; - } - - for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) { - if (!(k in result)) { - let error = new TokenServerClientServerError("Expected key not " + - " present in result: " + - k); - error.cause = "malformed-response"; - error.response = response; - cb(error, null); - return; - } - } - - this._log.debug("Successful token response"); - cb(null, { - id: result.id, - key: result.key, - endpoint: result.api_endpoint, - uid: result.uid, - duration: result.duration, - hashed_fxa_uid: result.hashed_fxa_uid, - }); - }, - - /* - * The prefix used for all notifications sent by this module. This - * allows the handler of notifications to be sure they are handling - * notifications for the service they expect. - * - * If not set, no notifications will be sent. - */ - observerPrefix: null, - - // Given an optional header value, notify that a backoff has been requested. - _maybeNotifyBackoff: function (response, headerName) { - if (!this.observerPrefix) { - return; - } - let headerVal = response.headers[headerName]; - if (!headerVal) { - return; - } - let backoffInterval; - try { - backoffInterval = parseInt(headerVal, 10); - } catch (ex) { - this._log.error("TokenServer response had invalid backoff value in '" + - headerName + "' header: " + headerVal); - return; - } - Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); - }, - - // override points for testing. - newRESTRequest: function(url) { - return new RESTRequest(url); - } -}; diff --git a/components/weave/src/crypto/WeaveCrypto.js b/components/weave/src/crypto/WeaveCrypto.js deleted file mode 100644 index aa82e4621..000000000 --- a/components/weave/src/crypto/WeaveCrypto.js +++ /dev/null @@ -1,262 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["WeaveCrypto"]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Async.jsm"); - -Cu.importGlobalProperties(['crypto']); - -const CRYPT_ALGO = "AES-CBC"; -const CRYPT_ALGO_LENGTH = 256; -const AES_CBC_IV_SIZE = 16; -const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 }; -const UTF_LABEL = "utf-8"; - -const KEY_DERIVATION_ALGO = "PBKDF2"; -const KEY_DERIVATION_HASHING_ALGO = "SHA-1"; -const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000. -const DERIVED_KEY_ALGO = CRYPT_ALGO; - -this.WeaveCrypto = function WeaveCrypto() { - this.init(); -}; - -WeaveCrypto.prototype = { - prefBranch : null, - debug : true, // services.sync.log.cryptoDebug - - observer : { - _self : null, - - QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - observe(subject, topic, data) { - let self = this._self; - self.log("Observed " + topic + " topic."); - if (topic == "nsPref:changed") { - self.debug = self.prefBranch.getBoolPref("cryptoDebug"); - } - } - }, - - init() { - // Preferences. Add observer so we get notified of changes. - this.prefBranch = Services.prefs.getBranch("services.sync.log."); - this.prefBranch.addObserver("cryptoDebug", this.observer, false); - this.observer._self = this; - this.debug = this.prefBranch.getBoolPref("cryptoDebug", false); - XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL)); - XPCOMUtils.defineLazyGetter(this, 'decoder', () => new TextDecoder(UTF_LABEL, { fatal: true })); - }, - - log(message) { - if (!this.debug) { - return; - } - dump("WeaveCrypto: " + message + "\n"); - Services.console.logStringMessage("WeaveCrypto: " + message); - }, - - // /!\ Only use this for tests! /!\ - _getCrypto() { - return crypto; - }, - - encrypt(clearTextUCS2, symmetricKey, iv) { - this.log("encrypt() called"); - let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer; - let encrypted = this._commonCrypt(clearTextBuffer, symmetricKey, iv, OPERATIONS.ENCRYPT); - return this.encodeBase64(encrypted); - }, - - decrypt(cipherText, symmetricKey, iv) { - this.log("decrypt() called"); - if (cipherText.length) { - cipherText = atob(cipherText); - } - let cipherTextBuffer = this.byteCompressInts(cipherText); - let decrypted = this._commonCrypt(cipherTextBuffer, symmetricKey, iv, OPERATIONS.DECRYPT); - return this.decoder.decode(decrypted); - }, - - /** - * _commonCrypt - * - * @args - * data: data to encrypt/decrypt (ArrayBuffer) - * symKeyStr: symmetric key (Base64 String) - * ivStr: initialization vector (Base64 String) - * operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT) - * @returns - * the encrypted/decrypted data (ArrayBuffer) - */ - _commonCrypt(data, symKeyStr, ivStr, operation) { - this.log("_commonCrypt() called"); - ivStr = atob(ivStr); - - if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) { - throw new Error("Unsupported operation in _commonCrypt."); - } - // We never want an IV longer than the block size, which is 16 bytes - // for AES, neither do we want one smaller; throw in both cases. - if (ivStr.length !== AES_CBC_IV_SIZE) { - throw "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes."; - } - - let iv = this.byteCompressInts(ivStr); - let symKey = this.importSymKey(symKeyStr, operation); - let cryptMethod = (operation === OPERATIONS.ENCRYPT - ? crypto.subtle.encrypt - : crypto.subtle.decrypt) - .bind(crypto.subtle); - let algo = { name: CRYPT_ALGO, iv: iv }; - - - return Async.promiseSpinningly( - cryptMethod(algo, symKey, data) - .then(keyBytes => new Uint8Array(keyBytes)) - ); - }, - - - generateRandomKey() { - this.log("generateRandomKey() called"); - let algo = { - name: CRYPT_ALGO, - length: CRYPT_ALGO_LENGTH - }; - return Async.promiseSpinningly( - crypto.subtle.generateKey(algo, true, []) - .then(key => crypto.subtle.exportKey("raw", key)) - .then(keyBytes => { - keyBytes = new Uint8Array(keyBytes); - return this.encodeBase64(keyBytes); - }) - ); - }, - - generateRandomIV() { - return this.generateRandomBytes(AES_CBC_IV_SIZE); - }, - - generateRandomBytes(byteCount) { - this.log("generateRandomBytes() called"); - - let randBytes = new Uint8Array(byteCount); - crypto.getRandomValues(randBytes); - - return this.encodeBase64(randBytes); - }, - - // - // SymKey CryptoKey memoization. - // - - // Memoize the import of symmetric keys. We do this by using the base64 - // string itself as a key. - _encryptionSymKeyMemo: {}, - _decryptionSymKeyMemo: {}, - importSymKey(encodedKeyString, operation) { - let memo; - - // We use two separate memos for thoroughness: operation is an input to - // key import. - switch (operation) { - case OPERATIONS.ENCRYPT: - memo = this._encryptionSymKeyMemo; - break; - case OPERATIONS.DECRYPT: - memo = this._decryptionSymKeyMemo; - break; - default: - throw "Unsupported operation in importSymKey."; - } - - if (encodedKeyString in memo) - return memo[encodedKeyString]; - - let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true); - let algo = { name: CRYPT_ALGO }; - let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"]; - - return Async.promiseSpinningly( - crypto.subtle.importKey("raw", symmetricKeyBuffer, algo, false, usages) - .then(symKey => { - memo[encodedKeyString] = symKey; - return symKey; - }) - ); - }, - - - // - // Utility functions - // - - /** - * Returns an Uint8Array filled with a JS string, - * which means we only keep utf-16 characters from 0x00 to 0xFF. - */ - byteCompressInts(str) { - let arrayBuffer = new Uint8Array(str.length); - for (let i = 0; i < str.length; i++) { - arrayBuffer[i] = str.charCodeAt(i) & 0xFF; - } - return arrayBuffer; - }, - - expandData(data) { - let expanded = ""; - for (let i = 0; i < data.length; i++) { - expanded += String.fromCharCode(data[i]); - } - return expanded; - }, - - encodeBase64(data) { - return btoa(this.expandData(data)); - }, - - makeUint8Array(input, isEncoded) { - if (isEncoded) { - input = atob(input); - } - return this.byteCompressInts(input); - }, - - /** - * Returns the expanded data string for the derived key. - */ - deriveKeyFromPassphrase(passphrase, saltStr, keyLength = 32) { - this.log("deriveKeyFromPassphrase() called."); - let keyData = this.makeUint8Array(passphrase, false); - let salt = this.makeUint8Array(saltStr, true); - let importAlgo = { name: KEY_DERIVATION_ALGO }; - let deriveAlgo = { - name: KEY_DERIVATION_ALGO, - salt: salt, - iterations: KEY_DERIVATION_ITERATIONS, - hash: { name: KEY_DERIVATION_HASHING_ALGO }, - }; - let derivedKeyType = { - name: DERIVED_KEY_ALGO, - length: keyLength * 8, - }; - return Async.promiseSpinningly( - crypto.subtle.importKey("raw", keyData, importAlgo, false, ["deriveKey"]) - .then(key => crypto.subtle.deriveKey(deriveAlgo, key, derivedKeyType, true, [])) - .then(derivedKey => crypto.subtle.exportKey("raw", derivedKey)) - .then(keyBytes => { - keyBytes = new Uint8Array(keyBytes); - return this.expandData(keyBytes); - }) - ); - }, -}; diff --git a/components/weave/src/crypto/utils.js b/components/weave/src/crypto/utils.js deleted file mode 100644 index 17d6155fb..000000000 --- a/components/weave/src/crypto/utils.js +++ /dev/null @@ -1,588 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -this.EXPORTED_SYMBOLS = ["CryptoUtils"]; - -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -this.CryptoUtils = { - xor: function xor(a, b) { - let bytes = []; - - if (a.length != b.length) { - throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length); - } - - for (let i = 0; i < a.length; i++) { - bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); - } - - return String.fromCharCode.apply(String, bytes); - }, - - /** - * Generate a string of random bytes. - */ - generateRandomBytes: function generateRandomBytes(length) { - let rng = Cc["@mozilla.org/security/random-generator;1"] - .createInstance(Ci.nsIRandomGenerator); - let bytes = rng.generateRandomBytes(length); - return CommonUtils.byteArrayToString(bytes); - }, - - /** - * UTF8-encode a message and hash it with the given hasher. Returns a - * string containing bytes. The hasher is reset if it's an HMAC hasher. - */ - digestUTF8: function digestUTF8(message, hasher) { - let data = this._utf8Converter.convertToByteArray(message, {}); - hasher.update(data, data.length); - let result = hasher.finish(false); - if (hasher instanceof Ci.nsICryptoHMAC) { - hasher.reset(); - } - return result; - }, - - /** - * Treat the given message as a bytes string and hash it with the given - * hasher. Returns a string containing bytes. The hasher is reset if it's - * an HMAC hasher. - */ - digestBytes: function digestBytes(message, hasher) { - // No UTF-8 encoding for you, sunshine. - let bytes = Array.prototype.slice.call(message).map(b => b.charCodeAt(0)); - hasher.update(bytes, bytes.length); - let result = hasher.finish(false); - if (hasher instanceof Ci.nsICryptoHMAC) { - hasher.reset(); - } - return result; - }, - - /** - * Encode the message into UTF-8 and feed the resulting bytes into the - * given hasher. Does not return a hash. This can be called multiple times - * with a single hasher, but eventually you must extract the result - * yourself. - */ - updateUTF8: function(message, hasher) { - let bytes = this._utf8Converter.convertToByteArray(message, {}); - hasher.update(bytes, bytes.length); - }, - - /** - * Produce an HMAC key object from a key string. - */ - makeHMACKey: function makeHMACKey(str) { - return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str); - }, - - /** - * Produce an HMAC hasher and initialize it with the given HMAC key. - */ - makeHMACHasher: function makeHMACHasher(type, key) { - let hasher = Cc["@mozilla.org/security/hmac;1"] - .createInstance(Ci.nsICryptoHMAC); - hasher.init(type, key); - return hasher; - }, - - /** - * HMAC-based Key Derivation (RFC 5869). - */ - hkdf: function hkdf(ikm, xts, info, len) { - const BLOCKSIZE = 256 / 8; - if (typeof xts === undefined) - xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0); - let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, - CryptoUtils.makeHMACKey(xts)); - let prk = CryptoUtils.digestBytes(ikm, h); - return CryptoUtils.hkdfExpand(prk, info, len); - }, - - /** - * HMAC-based Key Derivation Step 2 according to RFC 5869. - */ - hkdfExpand: function hkdfExpand(prk, info, len) { - const BLOCKSIZE = 256 / 8; - let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, - CryptoUtils.makeHMACKey(prk)); - let T = ""; - let Tn = ""; - let iterations = Math.ceil(len/BLOCKSIZE); - for (let i = 0; i < iterations; i++) { - Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h); - T += Tn; - } - return T.slice(0, len); - }, - - /** - * PBKDF2 implementation in Javascript. - * - * The arguments to this function correspond to items in - * PKCS #5, v2.0 pp. 9-10 - * - * P: the passphrase, an octet string: e.g., "secret phrase" - * S: the salt, an octet string: e.g., "DNXPzPpiwn" - * c: the number of iterations, a positive integer: e.g., 4096 - * dkLen: the length in octets of the destination - * key, a positive integer: e.g., 16 - * hmacAlg: The algorithm to use for hmac - * hmacLen: The hmac length - * - * The default value of 20 for hmacLen is appropriate for SHA1. For SHA256, - * hmacLen should be 32. - * - * The output is an octet string of length dkLen, which you - * can encode as you wish. - */ - pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen, - hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) { - - // We don't have a default in the algo itself, as NSS does. - if (!dkLen) { - throw new Error("dkLen should be defined"); - } - - function F(S, c, i, h) { - - function XOR(a, b, isA) { - if (a.length != b.length) { - return false; - } - - let val = []; - for (let i = 0; i < a.length; i++) { - if (isA) { - val[i] = a[i] ^ b[i]; - } else { - val[i] = a.charCodeAt(i) ^ b.charCodeAt(i); - } - } - - return val; - } - - let ret; - let U = []; - - /* Encode i into 4 octets: _INT */ - let I = []; - I[0] = String.fromCharCode((i >> 24) & 0xff); - I[1] = String.fromCharCode((i >> 16) & 0xff); - I[2] = String.fromCharCode((i >> 8) & 0xff); - I[3] = String.fromCharCode(i & 0xff); - - U[0] = CryptoUtils.digestBytes(S + I.join(''), h); - for (let j = 1; j < c; j++) { - U[j] = CryptoUtils.digestBytes(U[j - 1], h); - } - - ret = U[0]; - for (let j = 1; j < c; j++) { - ret = CommonUtils.byteArrayToString(XOR(ret, U[j])); - } - - return ret; - } - - let l = Math.ceil(dkLen / hmacLen); - let r = dkLen - ((l - 1) * hmacLen); - - // Reuse the key and the hasher. Remaking them 4096 times is 'spensive. - let h = CryptoUtils.makeHMACHasher(hmacAlg, - CryptoUtils.makeHMACKey(P)); - - let T = []; - for (let i = 0; i < l;) { - T[i] = F(S, c, ++i, h); - } - - let ret = ""; - for (let i = 0; i < l-1;) { - ret += T[i++]; - } - ret += T[l - 1].substr(0, r); - - return ret; - }, - - deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, - salt, - keyLength, - forceJS) { - if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) { - return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength); - } - else { - // Fall back to JS implementation. - // 4096 is hardcoded in WeaveCrypto, so do so here. - return CryptoUtils.pbkdf2Generate(passphrase, atob(salt), 4096, - keyLength); - } - }, - - /** - * Compute the HTTP MAC SHA-1 for an HTTP request. - * - * @param identifier - * (string) MAC Key Identifier. - * @param key - * (string) MAC Key. - * @param method - * (string) HTTP request method. - * @param URI - * (nsIURI) HTTP request URI. - * @param extra - * (object) Optional extra parameters. Valid keys are: - * nonce_bytes - How many bytes the nonce should be. This defaults - * to 8. Note that this many bytes are Base64 encoded, so the - * string length of the nonce will be longer than this value. - * ts - Timestamp to use. Should only be defined for testing. - * nonce - String nonce. Should only be defined for testing as this - * function will generate a cryptographically secure random one - * if not defined. - * ext - Extra string to be included in MAC. Per the HTTP MAC spec, - * the format is undefined and thus application specific. - * @returns - * (object) Contains results of operation and input arguments (for - * symmetry). The object has the following keys: - * - * identifier - (string) MAC Key Identifier (from arguments). - * key - (string) MAC Key (from arguments). - * method - (string) HTTP request method (from arguments). - * hostname - (string) HTTP hostname used (derived from arguments). - * port - (string) HTTP port number used (derived from arguments). - * mac - (string) Raw HMAC digest bytes. - * getHeader - (function) Call to obtain the string Authorization - * header value for this invocation. - * nonce - (string) Nonce value used. - * ts - (number) Integer seconds since Unix epoch that was used. - */ - computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method, - uri, extra) { - let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000); - let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8; - - // We are allowed to use more than the Base64 alphabet if we want. - let nonce = (extra && extra.nonce) - ? extra.nonce - : btoa(CryptoUtils.generateRandomBytes(nonce_bytes)); - - let host = uri.asciiHost; - let port; - let usedMethod = method.toUpperCase(); - - if (uri.port != -1) { - port = uri.port; - } else if (uri.scheme == "http") { - port = "80"; - } else if (uri.scheme == "https") { - port = "443"; - } else { - throw new Error("Unsupported URI scheme: " + uri.scheme); - } - - let ext = (extra && extra.ext) ? extra.ext : ""; - - let requestString = ts.toString(10) + "\n" + - nonce + "\n" + - usedMethod + "\n" + - uri.path + "\n" + - host + "\n" + - port + "\n" + - ext + "\n"; - - let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, - CryptoUtils.makeHMACKey(key)); - let mac = CryptoUtils.digestBytes(requestString, hasher); - - function getHeader() { - return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts, - this.nonce, this.mac, this.ext); - } - - return { - identifier: identifier, - key: key, - method: usedMethod, - hostname: host, - port: port, - mac: mac, - nonce: nonce, - ts: ts, - ext: ext, - getHeader: getHeader - }; - }, - - - /** - * Obtain the HTTP MAC Authorization header value from fields. - * - * @param identifier - * (string) MAC key identifier. - * @param ts - * (number) Integer seconds since Unix epoch. - * @param nonce - * (string) Nonce value. - * @param mac - * (string) Computed HMAC digest (raw bytes). - * @param ext - * (optional) (string) Extra string content. - * @returns - * (string) Value to put in Authorization header. - */ - getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce, - mac, ext) { - let header ='MAC id="' + identifier + '", ' + - 'ts="' + ts + '", ' + - 'nonce="' + nonce + '", ' + - 'mac="' + btoa(mac) + '"'; - - if (!ext) { - return header; - } - - return header += ', ext="' + ext +'"'; - }, - - /** - * Given an HTTP header value, strip out any attributes. - */ - - stripHeaderAttributes: function(value) { - value = value || ""; - let i = value.indexOf(";"); - return value.substring(0, (i >= 0) ? i : undefined).trim().toLowerCase(); - }, - - /** - * Compute the HAWK client values (mostly the header) for an HTTP request. - * - * @param URI - * (nsIURI) HTTP request URI. - * @param method - * (string) HTTP request method. - * @param options - * (object) extra parameters (all but "credentials" are optional): - * credentials - (object, mandatory) HAWK credentials object. - * All three keys are required: - * id - (string) key identifier - * key - (string) raw key bytes - * algorithm - (string) which hash to use: "sha1" or "sha256" - * ext - (string) application-specific data, included in MAC - * localtimeOffsetMsec - (number) local clock offset (vs server) - * payload - (string) payload to include in hash, containing the - * HTTP request body. If not provided, the HAWK hash - * will not cover the request body, and the server - * should not check it either. This will be UTF-8 - * encoded into bytes before hashing. This function - * cannot handle arbitrary binary data, sorry (the - * UTF-8 encoding process will corrupt any codepoints - * between U+0080 and U+00FF). Callers must be careful - * to use an HTTP client function which encodes the - * payload exactly the same way, otherwise the hash - * will not match. - * contentType - (string) payload Content-Type. This is included - * (without any attributes like "charset=") in the - * HAWK hash. It does *not* affect interpretation - * of the "payload" property. - * hash - (base64 string) pre-calculated payload hash. If - * provided, "payload" is ignored. - * ts - (number) pre-calculated timestamp, secs since epoch - * now - (number) current time, ms-since-epoch, for tests - * nonce - (string) pre-calculated nonce. Should only be defined - * for testing as this function will generate a - * cryptographically secure random one if not defined. - * @returns - * (object) Contains results of operation. The object has the - * following keys: - * field - (string) HAWK header, to use in Authorization: header - * artifacts - (object) other generated values: - * ts - (number) timestamp, in seconds since epoch - * nonce - (string) - * method - (string) - * resource - (string) path plus querystring - * host - (string) - * port - (number) - * hash - (string) payload hash (base64) - * ext - (string) app-specific data - * MAC - (string) request MAC (base64) - */ - computeHAWK: function(uri, method, options) { - let credentials = options.credentials; - let ts = options.ts || Math.floor(((options.now || Date.now()) + - (options.localtimeOffsetMsec || 0)) - / 1000); - - let hash_algo, hmac_algo; - if (credentials.algorithm == "sha1") { - hash_algo = Ci.nsICryptoHash.SHA1; - hmac_algo = Ci.nsICryptoHMAC.SHA1; - } else if (credentials.algorithm == "sha256") { - hash_algo = Ci.nsICryptoHash.SHA256; - hmac_algo = Ci.nsICryptoHMAC.SHA256; - } else { - throw new Error("Unsupported algorithm: " + credentials.algorithm); - } - - let port; - if (uri.port != -1) { - port = uri.port; - } else if (uri.scheme == "http") { - port = 80; - } else if (uri.scheme == "https") { - port = 443; - } else { - throw new Error("Unsupported URI scheme: " + uri.scheme); - } - - let artifacts = { - ts: ts, - nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)), - method: method.toUpperCase(), - resource: uri.path, // This includes both path and search/queryarg. - host: uri.asciiHost.toLowerCase(), // This includes punycoding. - port: port.toString(10), - hash: options.hash, - ext: options.ext, - }; - - let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); - - if (!artifacts.hash && options.hasOwnProperty("payload") - && options.payload) { - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hash_algo); - CryptoUtils.updateUTF8("hawk.1.payload\n", hasher); - CryptoUtils.updateUTF8(contentType+"\n", hasher); - CryptoUtils.updateUTF8(options.payload, hasher); - CryptoUtils.updateUTF8("\n", hasher); - let hash = hasher.finish(false); - // HAWK specifies this .hash to use +/ (not _-) and include the - // trailing "==" padding. - let hash_b64 = btoa(hash); - artifacts.hash = hash_b64; - } - - let requestString = ("hawk.1.header" + "\n" + - artifacts.ts.toString(10) + "\n" + - artifacts.nonce + "\n" + - artifacts.method + "\n" + - artifacts.resource + "\n" + - artifacts.host + "\n" + - artifacts.port + "\n" + - (artifacts.hash || "") + "\n"); - if (artifacts.ext) { - requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); - } - requestString += "\n"; - - let hasher = CryptoUtils.makeHMACHasher(hmac_algo, - CryptoUtils.makeHMACKey(credentials.key)); - artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher)); - // The output MAC uses "+" and "/", and padded== . - - function escape(attribute) { - // This is used for "x=y" attributes inside HTTP headers. - return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); - } - let header = ('Hawk id="' + credentials.id + '", ' + - 'ts="' + artifacts.ts + '", ' + - 'nonce="' + artifacts.nonce + '", ' + - (artifacts.hash ? ('hash="' + artifacts.hash + '", ') : "") + - (artifacts.ext ? ('ext="' + escape(artifacts.ext) + '", ') : "") + - 'mac="' + artifacts.mac + '"'); - return { - artifacts: artifacts, - field: header, - }; - }, - -}; - -/** - * Hashing Algorithms SHA-X. - * These values map directly onto the values defined - * in netwerk/base/nsICryptoHash.idl. - */ -let shaX = ["1", "256", "384", "512", "224"]; - -for (let shaIdx = 0, shaIdxLen = shaX.length; shaIdx < shaIdxLen; shaIdx++) { - let shaXIdx = shaX[shaIdx]; - - /** - * UTF-8 encode a message and perform a SHA-X over it. - * - * @param message - * (string) Buffer to perform operation on. Should be a JS string. - * It is possible to pass in a string representing an array - * of bytes. But, you probably don't want to UTF-8 encode - * such data and thus should not be using this function. - * - * @return string - * Raw bytes constituting SHA-X hash. Value is a JS string. - * Each character is the byte value for that offset. - */ - CryptoUtils["UTF8AndSHA" + shaXIdx] = function (message) { - let hasher = Cc["@mozilla.org/security/hash;1"] - .createInstance(Ci.nsICryptoHash); - hasher.init(hasher["SHA" + shaXIdx]); - - return CryptoUtils.digestUTF8(message, hasher); - }; - - CryptoUtils["sha" + shaXIdx] = function (message) { - return CommonUtils.bytesAsHex( - CryptoUtils["UTF8AndSHA" + shaXIdx](message)); - }; - - CryptoUtils["sha" + shaXIdx + "Base32"] = function (message) { - return CommonUtils.encodeBase32( - CryptoUtils["UTF8AndSHA" + shaXIdx](message)); - }; -} - -XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() { - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - - return converter; -}); - -var Svc = {}; - -XPCOMUtils.defineLazyServiceGetter(Svc, - "KeyFactory", - "@mozilla.org/security/keyobjectfactory;1", - "nsIKeyObjectFactory"); - -Svc.__defineGetter__("Crypto", function() { - let ns = {}; - Cu.import("resource://services-crypto/WeaveCrypto.js", ns); - - let wc = new ns.WeaveCrypto(); - delete Svc.Crypto; - return Svc.Crypto = wc; -}); - -Observers.add("xpcom-shutdown", function unloadServices() { - Observers.remove("xpcom-shutdown", unloadServices); - - for (let k in Svc) { - delete Svc[k]; - } -}); diff --git a/components/weave/src/engines/addons.js b/components/weave/src/engines/addons.js deleted file mode 100644 index 86f94e80b..000000000 --- a/components/weave/src/engines/addons.js +++ /dev/null @@ -1,706 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 file defines the add-on sync functionality. - * - * There are currently a number of known limitations: - * - We only sync XPI extensions and themes available from addons.mozilla.org. - * We hope to expand support for other add-ons eventually. - * - We only attempt syncing of add-ons between applications of the same type. - * This means add-ons will not synchronize between Firefox desktop and - * Firefox mobile, for example. This is because of significant add-on - * incompatibility between application types. - * - * Add-on records exist for each known {add-on, app-id} pair in the Sync client - * set. Each record has a randomly chosen GUID. The records then contain - * basic metadata about the add-on. - * - * We currently synchronize: - * - * - Installations - * - Uninstallations - * - User enabling and disabling - * - * Synchronization is influenced by the following preferences: - * - * - services.sync.addons.ignoreRepositoryChecking - * - services.sync.addons.ignoreUserEnabledChanges - * - services.sync.addons.trustedSourceHostnames - * - * See the documentation in services-sync.js for the behavior of these prefs. - */ -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-sync/addonutils.js"); -Cu.import("resource://services-sync/addonsreconciler.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Async.jsm"); - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", - "resource://gre/modules/addons/AddonRepository.jsm"); - -this.EXPORTED_SYMBOLS = ["AddonsEngine"]; - -// 7 days in milliseconds. -const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000; - -/** - * AddonRecord represents the state of an add-on in an application. - * - * Each add-on has its own record for each application ID it is installed - * on. - * - * The ID of add-on records is a randomly-generated GUID. It is random instead - * of deterministic so the URIs of the records cannot be guessed and so - * compromised server credentials won't result in disclosure of the specific - * add-ons present in a Sync account. - * - * The record contains the following fields: - * - * addonID - * ID of the add-on. This correlates to the "id" property on an Addon type. - * - * applicationID - * The application ID this record is associated with. - * - * enabled - * Boolean stating whether add-on is enabled or disabled by the user. - * - * source - * String indicating where an add-on is from. Currently, we only support - * the value "amo" which indicates that the add-on came from the official - * add-ons repository, addons.mozilla.org. In the future, we may support - * installing add-ons from other sources. This provides a future-compatible - * mechanism for clients to only apply records they know how to handle. - */ -function AddonRecord(collection, id) { - CryptoWrapper.call(this, collection, id); -} -AddonRecord.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Record.Addon" -}; - -Utils.deferGetSet(AddonRecord, "cleartext", ["addonID", - "applicationID", - "enabled", - "source"]); - -/** - * The AddonsEngine handles synchronization of add-ons between clients. - * - * The engine maintains an instance of an AddonsReconciler, which is the entity - * maintaining state for add-ons. It provides the history and tracking APIs - * that AddonManager doesn't. - * - * The engine instance overrides a handful of functions on the base class. The - * rationale for each is documented by that function. - */ -this.AddonsEngine = function AddonsEngine(service) { - SyncEngine.call(this, "Addons", service); - - this._reconciler = new AddonsReconciler(); -} -AddonsEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: AddonsStore, - _trackerObj: AddonsTracker, - _recordObj: AddonRecord, - version: 1, - - syncPriority: 5, - - _reconciler: null, - - /** - * Override parent method to find add-ons by their public ID, not Sync GUID. - */ - _findDupe: function _findDupe(item) { - let id = item.addonID; - - // The reconciler should have been updated at the top of the sync, so we - // can assume it is up to date when this function is called. - let addons = this._reconciler.addons; - if (!(id in addons)) { - return null; - } - - let addon = addons[id]; - if (addon.guid != item.id) { - return addon.guid; - } - - return null; - }, - - /** - * Override getChangedIDs to pull in tracker changes plus changes from the - * reconciler log. - */ - getChangedIDs: function getChangedIDs() { - let changes = {}; - for (let [id, modified] of Object.entries(this._tracker.changedIDs)) { - changes[id] = modified; - } - - let lastSyncDate = new Date(this.lastSync * 1000); - - // The reconciler should have been refreshed at the beginning of a sync and - // we assume this function is only called from within a sync. - let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate); - let addons = this._reconciler.addons; - for (let change of reconcilerChanges) { - let changeTime = change[0]; - let id = change[2]; - - if (!(id in addons)) { - continue; - } - - // Keep newest modified time. - if (id in changes && changeTime < changes[id]) { - continue; - } - - if (!this._store.isAddonSyncable(addons[id])) { - continue; - } - - this._log.debug("Adding changed add-on from changes log: " + id); - let addon = addons[id]; - changes[addon.guid] = changeTime.getTime() / 1000; - } - - return changes; - }, - - /** - * Override start of sync function to refresh reconciler. - * - * Many functions in this class assume the reconciler is refreshed at the - * top of a sync. If this ever changes, those functions should be revisited. - * - * Technically speaking, we don't need to refresh the reconciler on every - * sync since it is installed as an AddonManager listener. However, add-ons - * are complicated and we force a full refresh, just in case the listeners - * missed something. - */ - _syncStartup: function _syncStartup() { - // We refresh state before calling parent because syncStartup in the parent - // looks for changed IDs, which is dependent on add-on state being up to - // date. - this._refreshReconcilerState(); - - SyncEngine.prototype._syncStartup.call(this); - }, - - /** - * Override end of sync to perform a little housekeeping on the reconciler. - * - * We prune changes to prevent the reconciler state from growing without - * bound. Even if it grows unbounded, there would have to be many add-on - * changes (thousands) for it to slow things down significantly. This is - * highly unlikely to occur. Still, we exercise defense just in case. - */ - _syncCleanup: function _syncCleanup() { - let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD; - this._reconciler.pruneChangesBeforeDate(new Date(ms)); - - SyncEngine.prototype._syncCleanup.call(this); - }, - - /** - * Helper function to ensure reconciler is up to date. - * - * This will synchronously load the reconciler's state from the file - * system (if needed) and refresh the state of the reconciler. - */ - _refreshReconcilerState: function _refreshReconcilerState() { - this._log.debug("Refreshing reconciler state"); - let cb = Async.makeSpinningCallback(); - this._reconciler.refreshGlobalState(cb); - cb.wait(); - } -}; - -/** - * This is the primary interface between Sync and the Addons Manager. - * - * In addition to the core store APIs, we provide convenience functions to wrap - * Add-on Manager APIs with Sync-specific semantics. - */ -function AddonsStore(name, engine) { - Store.call(this, name, engine); -} -AddonsStore.prototype = { - __proto__: Store.prototype, - - // Define the add-on types (.type) that we support. - _syncableTypes: ["extension", "theme"], - - _extensionsPrefs: new Preferences("extensions."), - - get reconciler() { - return this.engine._reconciler; - }, - - /** - * Override applyIncoming to filter out records we can't handle. - */ - applyIncoming: function applyIncoming(record) { - // The fields we look at aren't present when the record is deleted. - if (!record.deleted) { - // Ignore records not belonging to our application ID because that is the - // current policy. - if (record.applicationID != Services.appinfo.ID) { - this._log.info("Ignoring incoming record from other App ID: " + - record.id); - return; - } - - // Ignore records that aren't from the official add-on repository, as that - // is our current policy. - if (record.source != "amo") { - this._log.info("Ignoring unknown add-on source (" + record.source + ")" + - " for " + record.id); - return; - } - } - - Store.prototype.applyIncoming.call(this, record); - }, - - - /** - * Provides core Store API to create/install an add-on from a record. - */ - create: function create(record) { - let cb = Async.makeSpinningCallback(); - AddonUtils.installAddons([{ - id: record.addonID, - syncGUID: record.id, - enabled: record.enabled, - requireSecureURI: this._extensionsPrefs.get("install.requireSecureOrigin", true), - }], cb); - - // This will throw if there was an error. This will get caught by the sync - // engine and the record will try to be applied later. - let results = cb.wait(); - - let addon; - for (let a of results.addons) { - if (a.id == record.addonID) { - addon = a; - break; - } - } - - // This should never happen, but is present as a fail-safe. - if (!addon) { - throw new Error("Add-on not found after install: " + record.addonID); - } - - this._log.info("Add-on installed: " + record.addonID); - }, - - /** - * Provides core Store API to remove/uninstall an add-on from a record. - */ - remove: function remove(record) { - // If this is called, the payload is empty, so we have to find by GUID. - let addon = this.getAddonByGUID(record.id); - if (!addon) { - // We don't throw because if the add-on could not be found then we assume - // it has already been uninstalled and there is nothing for this function - // to do. - return; - } - - this._log.info("Uninstalling add-on: " + addon.id); - let cb = Async.makeSpinningCallback(); - AddonUtils.uninstallAddon(addon, cb); - cb.wait(); - }, - - /** - * Provides core Store API to update an add-on from a record. - */ - update: function update(record) { - let addon = this.getAddonByID(record.addonID); - - // update() is called if !this.itemExists. And, since itemExists consults - // the reconciler only, we need to take care of some corner cases. - // - // First, the reconciler could know about an add-on that was uninstalled - // and no longer present in the add-ons manager. - if (!addon) { - this.create(record); - return; - } - - // It's also possible that the add-on is non-restartless and has pending - // install/uninstall activity. - // - // We wouldn't get here if the incoming record was for a deletion. So, - // check for pending uninstall and cancel if necessary. - if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) { - addon.cancelUninstall(); - - // We continue with processing because there could be state or ID change. - } - - let cb = Async.makeSpinningCallback(); - this.updateUserDisabled(addon, !record.enabled, cb); - cb.wait(); - }, - - /** - * Provide core Store API to determine if a record exists. - */ - itemExists: function itemExists(guid) { - let addon = this.reconciler.getAddonStateFromSyncGUID(guid); - - return !!addon; - }, - - /** - * Create an add-on record from its GUID. - * - * @param guid - * Add-on GUID (from extensions DB) - * @param collection - * Collection to add record to. - * - * @return AddonRecord instance - */ - createRecord: function createRecord(guid, collection) { - let record = new AddonRecord(collection, guid); - record.applicationID = Services.appinfo.ID; - - let addon = this.reconciler.getAddonStateFromSyncGUID(guid); - - // If we don't know about this GUID or if it has been uninstalled, we mark - // the record as deleted. - if (!addon || !addon.installed) { - record.deleted = true; - return record; - } - - record.modified = addon.modified.getTime() / 1000; - - record.addonID = addon.id; - record.enabled = addon.enabled; - - // This needs to be dynamic when add-ons don't come from AddonRepository. - record.source = "amo"; - - return record; - }, - - /** - * Changes the id of an add-on. - * - * This implements a core API of the store. - */ - changeItemID: function changeItemID(oldID, newID) { - // We always update the GUID in the reconciler because it will be - // referenced later in the sync process. - let state = this.reconciler.getAddonStateFromSyncGUID(oldID); - if (state) { - state.guid = newID; - let cb = Async.makeSpinningCallback(); - this.reconciler.saveState(null, cb); - cb.wait(); - } - - let addon = this.getAddonByGUID(oldID); - if (!addon) { - this._log.debug("Cannot change item ID (" + oldID + ") in Add-on " + - "Manager because old add-on not present: " + oldID); - return; - } - - addon.syncGUID = newID; - }, - - /** - * Obtain the set of all syncable add-on Sync GUIDs. - * - * This implements a core Store API. - */ - getAllIDs: function getAllIDs() { - let ids = {}; - - let addons = this.reconciler.addons; - for (let id in addons) { - let addon = addons[id]; - if (this.isAddonSyncable(addon)) { - ids[addon.guid] = true; - } - } - - return ids; - }, - - /** - * Wipe engine data. - * - * This uninstalls all syncable addons from the application. In case of - * error, it logs the error and keeps trying with other add-ons. - */ - wipe: function wipe() { - this._log.info("Processing wipe."); - - this.engine._refreshReconcilerState(); - - // We only wipe syncable add-ons. Wipe is a Sync feature not a security - // feature. - for (let guid in this.getAllIDs()) { - let addon = this.getAddonByGUID(guid); - if (!addon) { - this._log.debug("Ignoring add-on because it couldn't be obtained: " + - guid); - continue; - } - - this._log.info("Uninstalling add-on as part of wipe: " + addon.id); - Utils.catch(addon.uninstall)(); - } - }, - - /*************************************************************************** - * Functions below are unique to this store and not part of the Store API * - ***************************************************************************/ - - /** - * Synchronously obtain an add-on from its public ID. - * - * @param id - * Add-on ID - * @return Addon or undefined if not found - */ - getAddonByID: function getAddonByID(id) { - let cb = Async.makeSyncCallback(); - AddonManager.getAddonByID(id, cb); - return Async.waitForSyncCallback(cb); - }, - - /** - * Synchronously obtain an add-on from its Sync GUID. - * - * @param guid - * Add-on Sync GUID - * @return DBAddonInternal or null - */ - getAddonByGUID: function getAddonByGUID(guid) { - let cb = Async.makeSyncCallback(); - AddonManager.getAddonBySyncGUID(guid, cb); - return Async.waitForSyncCallback(cb); - }, - - /** - * Determines whether an add-on is suitable for Sync. - * - * @param addon - * Addon instance - * @return Boolean indicating whether it is appropriate for Sync - */ - isAddonSyncable: function isAddonSyncable(addon) { - // Currently, we limit syncable add-ons to those that are: - // 1) In a well-defined set of types - // 2) Installed in the current profile - // 3) Not installed by a foreign entity (i.e. installed by the app) - // since they act like global extensions. - // 4) Is not a hotfix. - // 5) Are installed from AMO - - // We could represent the test as a complex boolean expression. We go the - // verbose route so the failure reason is logged. - if (!addon) { - this._log.debug("Null object passed to isAddonSyncable."); - return false; - } - - if (this._syncableTypes.indexOf(addon.type) == -1) { - this._log.debug(addon.id + " not syncable: type not in whitelist: " + - addon.type); - return false; - } - - if (!(addon.scope & AddonManager.SCOPE_PROFILE)) { - this._log.debug(addon.id + " not syncable: not installed in profile."); - return false; - } - - // This may be too aggressive. If an add-on is downloaded from AMO and - // manually placed in the profile directory, foreignInstall will be set. - // Arguably, that add-on should be syncable. - // TODO Address the edge case and come up with more robust heuristics. - if (addon.foreignInstall) { - this._log.debug(addon.id + " not syncable: is foreign install."); - return false; - } - - // Ignore hotfix extensions (bug 741670). The pref may not be defined. - if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) { - this._log.debug(addon.id + " not syncable: is a hotfix."); - return false; - } - - // We provide a back door to skip the repository checking of an add-on. - // This is utilized by the tests to make testing easier. Users could enable - // this, but it would sacrifice security. - if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) { - return true; - } - - let cb = Async.makeSyncCallback(); - AddonRepository.getCachedAddonByID(addon.id, cb); - let result = Async.waitForSyncCallback(cb); - - if (!result) { - this._log.debug(addon.id + " not syncable: add-on not found in add-on " + - "repository."); - return false; - } - - return this.isSourceURITrusted(result.sourceURI); - }, - - /** - * Determine whether an add-on's sourceURI field is trusted and the add-on - * can be installed. - * - * This function should only ever be called from isAddonSyncable(). It is - * exposed as a separate function to make testing easier. - * - * @param uri - * nsIURI instance to validate - * @return bool - */ - isSourceURITrusted: function isSourceURITrusted(uri) { - // For security reasons, we currently limit synced add-ons to those - // installed from trusted hostname(s). We additionally require TLS with - // the add-ons site to help prevent forgeries. - let trustedHostnames = Svc.Prefs.get("addons.trustedSourceHostnames", "") - .split(","); - - if (!uri) { - this._log.debug("Undefined argument to isSourceURITrusted()."); - return false; - } - - // Scheme is validated before the hostname because uri.host may not be - // populated for certain schemes. It appears to always be populated for - // https, so we avoid the potential NS_ERROR_FAILURE on field access. - if (uri.scheme != "https") { - this._log.debug("Source URI not HTTPS: " + uri.spec); - return false; - } - - if (trustedHostnames.indexOf(uri.host) == -1) { - this._log.debug("Source hostname not trusted: " + uri.host); - return false; - } - - return true; - }, - - /** - * Update the userDisabled flag on an add-on. - * - * This will enable or disable an add-on and call the supplied callback when - * the action is complete. If no action is needed, the callback gets called - * immediately. - * - * @param addon - * Addon instance to manipulate. - * @param value - * Boolean to which to set userDisabled on the passed Addon. - * @param callback - * Function to be called when action is complete. Will receive 2 - * arguments, a truthy value that signifies error, and the Addon - * instance passed to this function. - */ - updateUserDisabled: function updateUserDisabled(addon, value, callback) { - if (addon.userDisabled == value) { - callback(null, addon); - return; - } - - // A pref allows changes to the enabled flag to be ignored. - if (Svc.Prefs.get("addons.ignoreUserEnabledChanges", false)) { - this._log.info("Ignoring enabled state change due to preference: " + - addon.id); - callback(null, addon); - return; - } - - AddonUtils.updateUserDisabled(addon, value, callback); - }, -}; - -/** - * The add-ons tracker keeps track of real-time changes to add-ons. - * - * It hooks up to the reconciler and receives notifications directly from it. - */ -function AddonsTracker(name, engine) { - Tracker.call(this, name, engine); -} -AddonsTracker.prototype = { - __proto__: Tracker.prototype, - - get reconciler() { - return this.engine._reconciler; - }, - - get store() { - return this.engine._store; - }, - - /** - * This callback is executed whenever the AddonsReconciler sends out a change - * notification. See AddonsReconciler.addChangeListener(). - */ - changeListener: function changeHandler(date, change, addon) { - this._log.debug("changeListener invoked: " + change + " " + addon.id); - // Ignore changes that occur during sync. - if (this.ignoreAll) { - return; - } - - if (!this.store.isAddonSyncable(addon)) { - this._log.debug("Ignoring change because add-on isn't syncable: " + - addon.id); - return; - } - - this.addChangedID(addon.guid, date.getTime() / 1000); - this.score += SCORE_INCREMENT_XLARGE; - }, - - startTracking: function() { - if (this.engine.enabled) { - this.reconciler.startListening(); - } - - this.reconciler.addChangeListener(this); - }, - - stopTracking: function() { - this.reconciler.removeChangeListener(this); - this.reconciler.stopListening(); - }, -}; diff --git a/components/weave/src/engines/bookmarks.js b/components/weave/src/engines/bookmarks.js deleted file mode 100644 index 61e771134..000000000 --- a/components/weave/src/engines/bookmarks.js +++ /dev/null @@ -1,1542 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ['BookmarksEngine', "PlacesItem", "Bookmark", - "BookmarkFolder", "BookmarkQuery", - "Livemark", "BookmarkSeparator"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/PlacesUtils.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Async.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/PlacesBackups.jsm"); - -const ALLBOOKMARKS_ANNO = "AllBookmarks"; -const DESCRIPTION_ANNO = "bookmarkProperties/description"; -const SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar"; -const MOBILEROOT_ANNO = "mobile/bookmarksRoot"; -const MOBILE_ANNO = "MobileBookmarks"; -const EXCLUDEBACKUP_ANNO = "places/excludeFromBackup"; -const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark"; -const PARENT_ANNO = "sync/parent"; -const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery"; -const ANNOS_TO_TRACK = [DESCRIPTION_ANNO, SIDEBAR_ANNO, - PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI]; - -const SERVICE_NOT_SUPPORTED = "Service not supported on this platform"; -const FOLDER_SORTINDEX = 1000000; - -this.PlacesItem = function PlacesItem(collection, id, type) { - CryptoWrapper.call(this, collection, id); - this.type = type || "item"; -} -PlacesItem.prototype = { - decrypt: function PlacesItem_decrypt(keyBundle) { - // Do the normal CryptoWrapper decrypt, but change types before returning - let clear = CryptoWrapper.prototype.decrypt.call(this, keyBundle); - - // Convert the abstract places item to the actual object type - if (!this.deleted) - this.__proto__ = this.getTypeObject(this.type).prototype; - - return clear; - }, - - getTypeObject: function PlacesItem_getTypeObject(type) { - switch (type) { - case "bookmark": - case "microsummary": - return Bookmark; - case "query": - return BookmarkQuery; - case "folder": - return BookmarkFolder; - case "livemark": - return Livemark; - case "separator": - return BookmarkSeparator; - case "item": - return PlacesItem; - } - throw "Unknown places item object type: " + type; - }, - - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.PlacesItem", -}; - -Utils.deferGetSet(PlacesItem, - "cleartext", - ["hasDupe", "parentid", "parentName", "type"]); - -this.Bookmark = function Bookmark(collection, id, type) { - PlacesItem.call(this, collection, id, type || "bookmark"); -} -Bookmark.prototype = { - __proto__: PlacesItem.prototype, - _logName: "Sync.Record.Bookmark", -}; - -Utils.deferGetSet(Bookmark, - "cleartext", - ["title", "bmkUri", "description", - "loadInSidebar", "tags", "keyword"]); - -this.BookmarkQuery = function BookmarkQuery(collection, id) { - Bookmark.call(this, collection, id, "query"); -} -BookmarkQuery.prototype = { - __proto__: Bookmark.prototype, - _logName: "Sync.Record.BookmarkQuery", -}; - -Utils.deferGetSet(BookmarkQuery, - "cleartext", - ["folderName", "queryId"]); - -this.BookmarkFolder = function BookmarkFolder(collection, id, type) { - PlacesItem.call(this, collection, id, type || "folder"); -} -BookmarkFolder.prototype = { - __proto__: PlacesItem.prototype, - _logName: "Sync.Record.Folder", -}; - -Utils.deferGetSet(BookmarkFolder, "cleartext", ["description", "title", - "children"]); - -this.Livemark = function Livemark(collection, id) { - BookmarkFolder.call(this, collection, id, "livemark"); -} -Livemark.prototype = { - __proto__: BookmarkFolder.prototype, - _logName: "Sync.Record.Livemark", -}; - -Utils.deferGetSet(Livemark, "cleartext", ["siteUri", "feedUri"]); - -this.BookmarkSeparator = function BookmarkSeparator(collection, id) { - PlacesItem.call(this, collection, id, "separator"); -} -BookmarkSeparator.prototype = { - __proto__: PlacesItem.prototype, - _logName: "Sync.Record.Separator", -}; - -Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos"); - - -let kSpecialIds = { - - // Special IDs. Note that mobile can attempt to create a record on - // dereference; special accessors are provided to prevent recursion within - // observers. - guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"], - - // Create the special mobile folder to store mobile bookmarks. - createMobileRoot: function createMobileRoot() { - let root = PlacesUtils.placesRootId; - let mRoot = PlacesUtils.bookmarks.createFolder(root, "mobile", -1); - PlacesUtils.annotations.setItemAnnotation( - mRoot, MOBILEROOT_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); - PlacesUtils.annotations.setItemAnnotation( - mRoot, EXCLUDEBACKUP_ANNO, 1, 0, PlacesUtils.annotations.EXPIRE_NEVER); - return mRoot; - }, - - findMobileRoot: function findMobileRoot(create) { - // Use the (one) mobile root if it already exists. - let root = PlacesUtils.annotations.getItemsWithAnnotation(MOBILEROOT_ANNO, {}); - if (root.length != 0) - return root[0]; - - if (create) - return this.createMobileRoot(); - - return null; - }, - - // Accessors for IDs. - isSpecialGUID: function isSpecialGUID(g) { - return this.guids.indexOf(g) != -1; - }, - - specialIdForGUID: function specialIdForGUID(guid, create) { - if (guid == "mobile") { - return this.findMobileRoot(create); - } - return this[guid]; - }, - - // Don't bother creating mobile: if it doesn't exist, this ID can't be it! - specialGUIDForId: function specialGUIDForId(id) { - for each (let guid in this.guids) - if (this.specialIdForGUID(guid, false) == id) - return guid; - return null; - }, - - get menu() { - return PlacesUtils.bookmarksMenuFolderId; - }, - get places() { - return PlacesUtils.placesRootId; - }, - get tags() { - return PlacesUtils.tagsFolderId; - }, - get toolbar() { - return PlacesUtils.toolbarFolderId; - }, - get unfiled() { - return PlacesUtils.unfiledBookmarksFolderId; - }, - get mobile() { - return this.findMobileRoot(true); - }, -}; - -this.BookmarksEngine = function BookmarksEngine(service) { - SyncEngine.call(this, "Bookmarks", service); -} -BookmarksEngine.prototype = { - __proto__: SyncEngine.prototype, - _recordObj: PlacesItem, - _storeObj: BookmarksStore, - _trackerObj: BookmarksTracker, - version: 2, - _defaultSort: "index", - - syncPriority: 4, - - _sync: function _sync() { - let engine = this; - let batchEx = null; - - // Try running sync in batch mode - PlacesUtils.bookmarks.runInBatchMode({ - runBatched: function wrappedSync() { - try { - SyncEngine.prototype._sync.call(engine); - } - catch(ex) { - batchEx = ex; - } - } - }, null); - - // Expose the exception if something inside the batch failed - if (batchEx != null) { - throw batchEx; - } - }, - - _guidMapFailed: false, - _buildGUIDMap: function _buildGUIDMap() { - let guidMap = {}; - for (let guid in this._store.getAllIDs()) { - // Figure out with which key to store the mapping. - let key; - let id = this._store.idForGUID(guid); - switch (PlacesUtils.bookmarks.getItemType(id)) { - case PlacesUtils.bookmarks.TYPE_BOOKMARK: - - // Smart bookmarks map to their annotation value. - let queryId; - try { - queryId = PlacesUtils.annotations.getItemAnnotation( - id, SMART_BOOKMARKS_ANNO); - } catch(ex) {} - - if (queryId) - key = "q" + queryId; - else - key = "b" + PlacesUtils.bookmarks.getBookmarkURI(id).spec + ":" + - PlacesUtils.bookmarks.getItemTitle(id); - break; - case PlacesUtils.bookmarks.TYPE_FOLDER: - key = "f" + PlacesUtils.bookmarks.getItemTitle(id); - break; - case PlacesUtils.bookmarks.TYPE_SEPARATOR: - key = "s" + PlacesUtils.bookmarks.getItemIndex(id); - break; - default: - continue; - } - - // The mapping is on a per parent-folder-name basis. - let parent = PlacesUtils.bookmarks.getFolderIdForItem(id); - if (parent <= 0) - continue; - - let parentName = PlacesUtils.bookmarks.getItemTitle(parent); - if (guidMap[parentName] == null) - guidMap[parentName] = {}; - - // If the entry already exists, remember that there are explicit dupes. - let entry = new String(guid); - entry.hasDupe = guidMap[parentName][key] != null; - - // Remember this item's GUID for its parent-name/key pair. - guidMap[parentName][key] = entry; - this._log.trace("Mapped: " + [parentName, key, entry, entry.hasDupe]); - } - - return guidMap; - }, - - // Helper function to get a dupe GUID for an item. - _mapDupe: function _mapDupe(item) { - // Figure out if we have something to key with. - let key; - let altKey; - switch (item.type) { - case "query": - // Prior to Bug 610501, records didn't carry their Smart Bookmark - // anno, so we won't be able to dupe them correctly. This altKey - // hack should get them to dupe correctly. - if (item.queryId) { - key = "q" + item.queryId; - altKey = "b" + item.bmkUri + ":" + item.title; - break; - } - // No queryID? Fall through to the regular bookmark case. - case "bookmark": - case "microsummary": - key = "b" + item.bmkUri + ":" + item.title; - break; - case "folder": - case "livemark": - key = "f" + item.title; - break; - case "separator": - key = "s" + item.pos; - break; - default: - return; - } - - // Figure out if we have a map to use! - // This will throw in some circumstances. That's fine. - let guidMap = this._guidMap; - - // Give the GUID if we have the matching pair. - this._log.trace("Finding mapping: " + item.parentName + ", " + key); - let parent = guidMap[item.parentName]; - - if (!parent) { - this._log.trace("No parent => no dupe."); - return undefined; - } - - let dupe = parent[key]; - - if (dupe) { - this._log.trace("Mapped dupe: " + dupe); - return dupe; - } - - if (altKey) { - dupe = parent[altKey]; - if (dupe) { - this._log.trace("Mapped dupe using altKey " + altKey + ": " + dupe); - return dupe; - } - } - - this._log.trace("No dupe found for key " + key + "/" + altKey + "."); - return undefined; - }, - - _syncStartup: function _syncStart() { - SyncEngine.prototype._syncStartup.call(this); - - let cb = Async.makeSpinningCallback(); - Task.spawn(function() { - // For first-syncs, make a backup for the user to restore - if (this.lastSync == 0) { - this._log.debug("Bookmarks backup starting."); - yield PlacesBackups.create(null, true); - this._log.debug("Bookmarks backup done."); - } - }.bind(this)).then( - cb, ex => { - // Failure to create a backup is somewhat bad, but probably not bad - // enough to prevent syncing of bookmarks - so just log the error and - // continue. - this._log.warn("Got exception backing up bookmarks, but continuing with sync.", ex); - cb(); - } - ); - - cb.wait(); - - this.__defineGetter__("_guidMap", function() { - // Create a mapping of folder titles and separator positions to GUID. - // We do this lazily so that we don't do any work unless we reconcile - // incoming items. - let guidMap; - try { - guidMap = this._buildGUIDMap(); - } catch (ex) { - this._log.warn("Got exception building GUID map." + - " Skipping all other incoming items.", ex); - throw {code: Engine.prototype.eEngineAbortApplyIncoming, - cause: ex}; - } - delete this._guidMap; - return this._guidMap = guidMap; - }); - - this._store._childrenToOrder = {}; - }, - - _processIncoming: function (newitems) { - try { - SyncEngine.prototype._processIncoming.call(this, newitems); - } finally { - // Reorder children. - this._tracker.ignoreAll = true; - this._store._orderChildren(); - this._tracker.ignoreAll = false; - delete this._store._childrenToOrder; - } - }, - - _syncFinish: function _syncFinish() { - SyncEngine.prototype._syncFinish.call(this); - this._tracker._ensureMobileQuery(); - }, - - _syncCleanup: function _syncCleanup() { - SyncEngine.prototype._syncCleanup.call(this); - delete this._guidMap; - }, - - _createRecord: function _createRecord(id) { - // Create the record as usual, but mark it as having dupes if necessary. - let record = SyncEngine.prototype._createRecord.call(this, id); - let entry = this._mapDupe(record); - if (entry != null && entry.hasDupe) { - record.hasDupe = true; - } - return record; - }, - - _findDupe: function _findDupe(item) { - this._log.trace("Finding dupe for " + item.id + - " (already duped: " + item.hasDupe + ")."); - - // Don't bother finding a dupe if the incoming item has duplicates. - if (item.hasDupe) { - this._log.trace(item.id + " already a dupe: not finding one."); - return; - } - let mapped = this._mapDupe(item); - this._log.debug(item.id + " mapped to " + mapped); - return mapped; - } -}; - -function BookmarksStore(name, engine) { - Store.call(this, name, engine); - - // Explicitly nullify our references to our cached services so we don't leak - Svc.Obs.add("places-shutdown", function() { - for each (let [query, stmt] in Iterator(this._stmts)) { - stmt.finalize(); - } - this._stmts = {}; - }, this); -} -BookmarksStore.prototype = { - __proto__: Store.prototype, - - itemExists: function BStore_itemExists(id) { - return this.idForGUID(id, true) > 0; - }, - - /* - * If the record is a tag query, rewrite it to refer to the local tag ID. - * - * Otherwise, just return. - */ - preprocessTagQuery: function preprocessTagQuery(record) { - if (record.type != "query" || - record.bmkUri == null || - !record.folderName) - return; - - // Yes, this works without chopping off the "place:" prefix. - let uri = record.bmkUri - let queriesRef = {}; - let queryCountRef = {}; - let optionsRef = {}; - PlacesUtils.history.queryStringToQueries(uri, queriesRef, queryCountRef, - optionsRef); - - // We only process tag URIs. - if (optionsRef.value.resultType != optionsRef.value.RESULTS_AS_TAG_CONTENTS) - return; - - // Tag something to ensure that the tag exists. - let tag = record.folderName; - let dummyURI = Utils.makeURI("about:weave#BStore_preprocess"); - PlacesUtils.tagging.tagURI(dummyURI, [tag]); - - // Look for the id of the tag, which might just have been added. - let tags = this._getNode(PlacesUtils.tagsFolderId); - if (!(tags instanceof Ci.nsINavHistoryQueryResultNode)) { - this._log.debug("tags isn't an nsINavHistoryQueryResultNode; aborting."); - return; - } - - tags.containerOpen = true; - try { - for (let i = 0; i < tags.childCount; i++) { - let child = tags.getChild(i); - if (child.title == tag) { - // Found the tag, so fix up the query to use the right id. - this._log.debug("Tag query folder: " + tag + " = " + child.itemId); - - this._log.trace("Replacing folders in: " + uri); - for each (let q in queriesRef.value) - q.setFolders([child.itemId], 1); - - record.bmkUri = PlacesUtils.history.queriesToQueryString( - queriesRef.value, queryCountRef.value, optionsRef.value); - return; - } - } - } - finally { - tags.containerOpen = false; - } - }, - - applyIncoming: function BStore_applyIncoming(record) { - this._log.debug("Applying record " + record.id); - let isSpecial = record.id in kSpecialIds; - - if (record.deleted) { - if (isSpecial) { - this._log.warn("Ignoring deletion for special record " + record.id); - return; - } - - // Don't bother with pre and post-processing for deletions. - Store.prototype.applyIncoming.call(this, record); - return; - } - - // For special folders we're only interested in child ordering. - if (isSpecial && record.children) { - this._log.debug("Processing special node: " + record.id); - // Reorder children later - this._childrenToOrder[record.id] = record.children; - return; - } - - // Skip malformed records. (Bug 806460.) - if (record.type == "query" && - !record.bmkUri) { - this._log.warn("Skipping malformed query bookmark: " + record.id); - return; - } - - // Preprocess the record before doing the normal apply. - this.preprocessTagQuery(record); - - // Figure out the local id of the parent GUID if available - let parentGUID = record.parentid; - if (!parentGUID) { - throw "Record " + record.id + " has invalid parentid: " + parentGUID; - } - this._log.debug("Local parent is " + parentGUID); - - let parentId = this.idForGUID(parentGUID); - if (parentId > 0) { - // Save the parent id for modifying the bookmark later - record._parent = parentId; - record._orphan = false; - this._log.debug("Record " + record.id + " is not an orphan."); - } else { - this._log.trace("Record " + record.id + - " is an orphan: could not find parent " + parentGUID); - record._orphan = true; - } - - // Do the normal processing of incoming records - Store.prototype.applyIncoming.call(this, record); - - // Do some post-processing if we have an item - let itemId = this.idForGUID(record.id); - if (itemId > 0) { - // Move any children that are looking for this folder as a parent - if (record.type == "folder") { - this._reparentOrphans(itemId); - // Reorder children later - if (record.children) - this._childrenToOrder[record.id] = record.children; - } - - // Create an annotation to remember that it needs reparenting. - if (record._orphan) { - PlacesUtils.annotations.setItemAnnotation( - itemId, PARENT_ANNO, parentGUID, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - } - }, - - /** - * Find all ids of items that have a given value for an annotation - */ - _findAnnoItems: function BStore__findAnnoItems(anno, val) { - return PlacesUtils.annotations.getItemsWithAnnotation(anno, {}) - .filter(function(id) { - return PlacesUtils.annotations.getItemAnnotation(id, anno) == val; - }); - }, - - /** - * For the provided parent item, attach its children to it - */ - _reparentOrphans: function _reparentOrphans(parentId) { - // Find orphans and reunite with this folder parent - let parentGUID = this.GUIDForId(parentId); - let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID); - - this._log.debug("Reparenting orphans " + orphans + " to " + parentId); - orphans.forEach(function(orphan) { - // Move the orphan to the parent and drop the missing parent annotation - if (this._reparentItem(orphan, parentId)) { - PlacesUtils.annotations.removeItemAnnotation(orphan, PARENT_ANNO); - } - }, this); - }, - - _reparentItem: function _reparentItem(itemId, parentId) { - this._log.trace("Attempting to move item " + itemId + " to new parent " + - parentId); - try { - if (parentId > 0) { - PlacesUtils.bookmarks.moveItem(itemId, parentId, - PlacesUtils.bookmarks.DEFAULT_INDEX); - return true; - } - } catch(ex) { - this._log.debug("Failed to reparent item. ", ex); - } - return false; - }, - - // Turn a record's nsINavBookmarksService constant and other attributes into - // a granular type for comparison. - _recordType: function _recordType(itemId) { - let bms = PlacesUtils.bookmarks; - let type = bms.getItemType(itemId); - - switch (type) { - case bms.TYPE_FOLDER: - if (PlacesUtils.annotations - .itemHasAnnotation(itemId, PlacesUtils.LMANNO_FEEDURI)) { - return "livemark"; - } - return "folder"; - - case bms.TYPE_BOOKMARK: - let bmkUri = bms.getBookmarkURI(itemId).spec; - if (bmkUri.indexOf("place:") == 0) { - return "query"; - } - return "bookmark"; - - case bms.TYPE_SEPARATOR: - return "separator"; - - default: - return null; - } - }, - - create: function BStore_create(record) { - // Default to unfiled if we don't have the parent yet. - - // Valid parent IDs are all positive integers. Other values -- undefined, - // null, -1 -- all compare false for > 0, so this catches them all. We - // don't just use <= without the !, because undefined and null compare - // false for that, too! - if (!(record._parent > 0)) { - this._log.debug("Parent is " + record._parent + "; reparenting to unfiled."); - record._parent = kSpecialIds.unfiled; - } - - let newId; - switch (record.type) { - case "bookmark": - case "query": - case "microsummary": { - let uri = Utils.makeURI(record.bmkUri); - newId = PlacesUtils.bookmarks.insertBookmark( - record._parent, uri, PlacesUtils.bookmarks.DEFAULT_INDEX, record.title); - this._log.debug("created bookmark " + newId + " under " + record._parent - + " as " + record.title + " " + record.bmkUri); - - // Smart bookmark annotations are strings. - if (record.queryId) { - PlacesUtils.annotations.setItemAnnotation( - newId, SMART_BOOKMARKS_ANNO, record.queryId, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - - if (Array.isArray(record.tags)) { - this._tagURI(uri, record.tags); - } - PlacesUtils.bookmarks.setKeywordForBookmark(newId, record.keyword); - if (record.description) { - PlacesUtils.annotations.setItemAnnotation( - newId, DESCRIPTION_ANNO, record.description, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - - if (record.loadInSidebar) { - PlacesUtils.annotations.setItemAnnotation( - newId, SIDEBAR_ANNO, true, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - - } break; - case "folder": - newId = PlacesUtils.bookmarks.createFolder( - record._parent, record.title, PlacesUtils.bookmarks.DEFAULT_INDEX); - this._log.debug("created folder " + newId + " under " + record._parent - + " as " + record.title); - - if (record.description) { - PlacesUtils.annotations.setItemAnnotation( - newId, DESCRIPTION_ANNO, record.description, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - - // record.children will be dealt with in _orderChildren. - break; - case "livemark": - let siteURI = null; - if (!record.feedUri) { - this._log.debug("No feed URI: skipping livemark record " + record.id); - return; - } - if (PlacesUtils.annotations - .itemHasAnnotation(record._parent, PlacesUtils.LMANNO_FEEDURI)) { - this._log.debug("Invalid parent: skipping livemark record " + record.id); - return; - } - - if (record.siteUri != null) - siteURI = Utils.makeURI(record.siteUri); - - // Until this engine can handle asynchronous error reporting, we need to - // detect errors on creation synchronously. - let spinningCb = Async.makeSpinningCallback(); - - let livemarkObj = {title: record.title, - parentId: record._parent, - index: PlacesUtils.bookmarks.DEFAULT_INDEX, - feedURI: Utils.makeURI(record.feedUri), - siteURI: siteURI, - guid: record.id}; - PlacesUtils.livemarks.addLivemark(livemarkObj).then( - aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) }, - () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) } - ); - - let [status, livemark] = spinningCb.wait(); - if (!Components.isSuccessCode(status)) { - throw status; - } - - this._log.debug("Created livemark " + livemark.id + " under " + - livemark.parentId + " as " + livemark.title + - ", " + livemark.siteURI.spec + ", " + - livemark.feedURI.spec + ", GUID " + - livemark.guid); - break; - case "separator": - newId = PlacesUtils.bookmarks.insertSeparator( - record._parent, PlacesUtils.bookmarks.DEFAULT_INDEX); - this._log.debug("created separator " + newId + " under " + record._parent); - break; - case "item": - this._log.debug(" -> got a generic places item.. do nothing?"); - return; - default: - this._log.error("_create: Unknown item type: " + record.type); - return; - } - - if (newId) { - // Livemarks can set the GUID through the API, so there's no need to - // do that here. - this._log.trace("Setting GUID of new item " + newId + " to " + record.id); - this._setGUID(newId, record.id); - } - }, - - // Factored out of `remove` to avoid redundant DB queries when the Places ID - // is already known. - removeById: function removeById(itemId, guid) { - let type = PlacesUtils.bookmarks.getItemType(itemId); - - switch (type) { - case PlacesUtils.bookmarks.TYPE_BOOKMARK: - this._log.debug(" -> removing bookmark " + guid); - PlacesUtils.bookmarks.removeItem(itemId); - break; - case PlacesUtils.bookmarks.TYPE_FOLDER: - this._log.debug(" -> removing folder " + guid); - PlacesUtils.bookmarks.removeItem(itemId); - break; - case PlacesUtils.bookmarks.TYPE_SEPARATOR: - this._log.debug(" -> removing separator " + guid); - PlacesUtils.bookmarks.removeItem(itemId); - break; - default: - this._log.error("remove: Unknown item type: " + type); - break; - } - }, - - remove: function BStore_remove(record) { - if (kSpecialIds.isSpecialGUID(record.id)) { - this._log.warn("Refusing to remove special folder " + record.id); - return; - } - - let itemId = this.idForGUID(record.id); - if (itemId <= 0) { - this._log.debug("Item " + record.id + " already removed"); - return; - } - this.removeById(itemId, record.id); - }, - - _taggableTypes: ["bookmark", "microsummary", "query"], - isTaggable: function isTaggable(recordType) { - return this._taggableTypes.indexOf(recordType) != -1; - }, - - update: function BStore_update(record) { - let itemId = this.idForGUID(record.id); - - if (itemId <= 0) { - this._log.debug("Skipping update for unknown item: " + record.id); - return; - } - - // Two items are the same type if they have the same ItemType in Places, - // and also share some key characteristics (e.g., both being livemarks). - // We figure this out by examining the item to find the equivalent granular - // (string) type. - // If they're not the same type, we can't just update attributes. Delete - // then recreate the record instead. - let localItemType = this._recordType(itemId); - let remoteRecordType = record.type; - this._log.trace("Local type: " + localItemType + ". " + - "Remote type: " + remoteRecordType + "."); - - if (localItemType != remoteRecordType) { - this._log.debug("Local record and remote record differ in type. " + - "Deleting and recreating."); - this.removeById(itemId, record.id); - this.create(record); - return; - } - - this._log.trace("Updating " + record.id + " (" + itemId + ")"); - - // Move the bookmark to a new parent or new position if necessary - if (record._parent > 0 && - PlacesUtils.bookmarks.getFolderIdForItem(itemId) != record._parent) { - this._reparentItem(itemId, record._parent); - } - - for (let [key, val] in Iterator(record.cleartext)) { - switch (key) { - case "title": - PlacesUtils.bookmarks.setItemTitle(itemId, val); - break; - case "bmkUri": - PlacesUtils.bookmarks.changeBookmarkURI(itemId, Utils.makeURI(val)); - break; - case "tags": - if (Array.isArray(val)) { - if (this.isTaggable(remoteRecordType)) { - this._tagID(itemId, val); - } else { - this._log.debug("Remote record type is invalid for tags: " + remoteRecordType); - } - } - break; - case "keyword": - PlacesUtils.bookmarks.setKeywordForBookmark(itemId, val); - break; - case "description": - if (val) { - PlacesUtils.annotations.setItemAnnotation( - itemId, DESCRIPTION_ANNO, val, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } else { - PlacesUtils.annotations.removeItemAnnotation(itemId, DESCRIPTION_ANNO); - } - break; - case "loadInSidebar": - if (val) { - PlacesUtils.annotations.setItemAnnotation( - itemId, SIDEBAR_ANNO, true, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } else { - PlacesUtils.annotations.removeItemAnnotation(itemId, SIDEBAR_ANNO); - } - break; - case "queryId": - PlacesUtils.annotations.setItemAnnotation( - itemId, SMART_BOOKMARKS_ANNO, val, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - break; - } - } - }, - - _orderChildren: function _orderChildren() { - for (let [guid, children] in Iterator(this._childrenToOrder)) { - // Reorder children according to the GUID list. Gracefully deal - // with missing items, e.g. locally deleted. - let delta = 0; - let parent = null; - for (let idx = 0; idx < children.length; idx++) { - let itemid = this.idForGUID(children[idx]); - if (itemid == -1) { - delta += 1; - this._log.trace("Could not locate record " + children[idx]); - continue; - } - try { - // This code path could be optimized by caching the parent earlier. - // Doing so should take in count any edge case due to reparenting - // or parent invalidations though. - if (!parent) { - parent = PlacesUtils.bookmarks.getFolderIdForItem(itemid); - } - PlacesUtils.bookmarks.moveItem(itemid, parent, idx - delta); - } catch (ex) { - this._log.debug("Could not move item " + children[idx] + ": " + ex); - } - } - } - }, - - changeItemID: function BStore_changeItemID(oldID, newID) { - this._log.debug("Changing GUID " + oldID + " to " + newID); - - // Make sure there's an item to change GUIDs - let itemId = this.idForGUID(oldID); - if (itemId <= 0) - return; - - this._setGUID(itemId, newID); - }, - - _getNode: function BStore__getNode(folder) { - let query = PlacesUtils.history.getNewQuery(); - query.setFolders([folder], 1); - return PlacesUtils.history.executeQuery( - query, PlacesUtils.history.getNewQueryOptions()).root; - }, - - _getTags: function BStore__getTags(uri) { - try { - if (typeof(uri) == "string") - uri = Utils.makeURI(uri); - } catch(e) { - this._log.warn("Could not parse URI \"" + uri + "\": " + e); - } - return PlacesUtils.tagging.getTagsForURI(uri, {}); - }, - - _getDescription: function BStore__getDescription(id) { - try { - return PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO); - } catch (e) { - return null; - } - }, - - _isLoadInSidebar: function BStore__isLoadInSidebar(id) { - return PlacesUtils.annotations.itemHasAnnotation(id, SIDEBAR_ANNO); - }, - - get _childGUIDsStm() { - return this._getStmt( - "SELECT id AS item_id, guid " + - "FROM moz_bookmarks " + - "WHERE parent = :parent " + - "ORDER BY position"); - }, - _childGUIDsCols: ["item_id", "guid"], - - _getChildGUIDsForId: function _getChildGUIDsForId(itemid) { - let stmt = this._childGUIDsStm; - stmt.params.parent = itemid; - let rows = Async.querySpinningly(stmt, this._childGUIDsCols); - return rows.map(function (row) { - if (row.guid) { - return row.guid; - } - // A GUID hasn't been assigned to this item yet, do this now. - return this.GUIDForId(row.item_id); - }, this); - }, - - // Create a record starting from the weave id (places guid) - createRecord: function createRecord(id, collection) { - let placeId = this.idForGUID(id); - let record; - if (placeId <= 0) { // deleted item - record = new PlacesItem(collection, id); - record.deleted = true; - return record; - } - - let parent = PlacesUtils.bookmarks.getFolderIdForItem(placeId); - switch (PlacesUtils.bookmarks.getItemType(placeId)) { - case PlacesUtils.bookmarks.TYPE_BOOKMARK: - let bmkUri = PlacesUtils.bookmarks.getBookmarkURI(placeId).spec; - if (bmkUri.indexOf("place:") == 0) { - record = new BookmarkQuery(collection, id); - - // Get the actual tag name instead of the local itemId - let folder = bmkUri.match(/[:&]folder=(\d+)/); - try { - // There might not be the tag yet when creating on a new client - if (folder != null) { - folder = folder[1]; - record.folderName = PlacesUtils.bookmarks.getItemTitle(folder); - this._log.trace("query id: " + folder + " = " + record.folderName); - } - } - catch(ex) {} - - // Persist the Smart Bookmark anno, if found. - try { - let anno = PlacesUtils.annotations.getItemAnnotation(placeId, SMART_BOOKMARKS_ANNO); - if (anno != null) { - this._log.trace("query anno: " + SMART_BOOKMARKS_ANNO + - " = " + anno); - record.queryId = anno; - } - } - catch(ex) {} - } - else { - record = new Bookmark(collection, id); - } - record.title = PlacesUtils.bookmarks.getItemTitle(placeId); - - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - record.bmkUri = bmkUri; - record.tags = this._getTags(record.bmkUri); - record.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(placeId); - record.description = this._getDescription(placeId); - record.loadInSidebar = this._isLoadInSidebar(placeId); - break; - - case PlacesUtils.bookmarks.TYPE_FOLDER: - if (PlacesUtils.annotations - .itemHasAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI)) { - record = new Livemark(collection, id); - let as = PlacesUtils.annotations; - record.feedUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_FEEDURI); - try { - record.siteUri = as.getItemAnnotation(placeId, PlacesUtils.LMANNO_SITEURI); - } catch (ex) {} - } else { - record = new BookmarkFolder(collection, id); - } - - if (parent > 0) - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - record.title = PlacesUtils.bookmarks.getItemTitle(placeId); - record.description = this._getDescription(placeId); - record.children = this._getChildGUIDsForId(placeId); - break; - - case PlacesUtils.bookmarks.TYPE_SEPARATOR: - record = new BookmarkSeparator(collection, id); - if (parent > 0) - record.parentName = PlacesUtils.bookmarks.getItemTitle(parent); - // Create a positioning identifier for the separator, used by _mapDupe - record.pos = PlacesUtils.bookmarks.getItemIndex(placeId); - break; - - default: - record = new PlacesItem(collection, id); - this._log.warn("Unknown item type, cannot serialize: " + - PlacesUtils.bookmarks.getItemType(placeId)); - } - - record.parentid = this.GUIDForId(parent); - record.sortindex = this._calculateIndex(record); - - return record; - }, - - _stmts: {}, - _getStmt: function(query) { - if (query in this._stmts) { - return this._stmts[query]; - } - - this._log.trace("Creating SQL statement: " + query); - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) - .DBConnection; - return this._stmts[query] = db.createAsyncStatement(query); - }, - - get _frecencyStm() { - return this._getStmt( - "SELECT frecency " + - "FROM moz_places " + - "WHERE url = :url " + - "LIMIT 1"); - }, - _frecencyCols: ["frecency"], - - get _setGUIDStm() { - return this._getStmt( - "UPDATE moz_bookmarks " + - "SET guid = :guid " + - "WHERE id = :item_id"); - }, - - // Some helper functions to handle GUIDs - _setGUID: function _setGUID(id, guid) { - if (!guid) - guid = Utils.makeGUID(); - - let stmt = this._setGUIDStm; - stmt.params.guid = guid; - stmt.params.item_id = id; - Async.querySpinningly(stmt); - return guid; - }, - - get _guidForIdStm() { - return this._getStmt( - "SELECT guid " + - "FROM moz_bookmarks " + - "WHERE id = :item_id"); - }, - _guidForIdCols: ["guid"], - - GUIDForId: function GUIDForId(id) { - let special = kSpecialIds.specialGUIDForId(id); - if (special) - return special; - - let stmt = this._guidForIdStm; - stmt.params.item_id = id; - - // Use the existing GUID if it exists - let result = Async.querySpinningly(stmt, this._guidForIdCols)[0]; - if (result && result.guid) - return result.guid; - - // Give the uri a GUID if it doesn't have one - return this._setGUID(id); - }, - - get _idForGUIDStm() { - return this._getStmt( - "SELECT id AS item_id " + - "FROM moz_bookmarks " + - "WHERE guid = :guid"); - }, - _idForGUIDCols: ["item_id"], - - // noCreate is provided as an optional argument to prevent the creation of - // non-existent special records, such as "mobile". - idForGUID: function idForGUID(guid, noCreate) { - if (kSpecialIds.isSpecialGUID(guid)) - return kSpecialIds.specialIdForGUID(guid, !noCreate); - - let stmt = this._idForGUIDStm; - // guid might be a String object rather than a string. - stmt.params.guid = guid.toString(); - - let results = Async.querySpinningly(stmt, this._idForGUIDCols); - this._log.trace("Number of rows matching GUID " + guid + ": " - + results.length); - - // Here's the one we care about: the first. - let result = results[0]; - - if (!result) - return -1; - - return result.item_id; - }, - - _calculateIndex: function _calculateIndex(record) { - // Ensure folders have a very high sort index so they're not synced last. - if (record.type == "folder") - return FOLDER_SORTINDEX; - - // For anything directly under the toolbar, give it a boost of more than an - // unvisited bookmark - let index = 0; - if (record.parentid == "toolbar") - index += 150; - - // Add in the bookmark's frecency if we have something. - if (record.bmkUri != null) { - this._frecencyStm.params.url = record.bmkUri; - let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols); - if (result.length) - index += result[0].frecency; - } - - return index; - }, - - _getChildren: function BStore_getChildren(guid, items) { - let node = guid; // the recursion case - if (typeof(node) == "string") { // callers will give us the guid as the first arg - let nodeID = this.idForGUID(guid, true); - if (!nodeID) { - this._log.debug("No node for GUID " + guid + "; returning no children."); - return items; - } - node = this._getNode(nodeID); - } - - if (node.type == node.RESULT_TYPE_FOLDER) { - node.QueryInterface(Ci.nsINavHistoryQueryResultNode); - node.containerOpen = true; - try { - // Remember all the children GUIDs and recursively get more - for (let i = 0; i < node.childCount; i++) { - let child = node.getChild(i); - items[this.GUIDForId(child.itemId)] = true; - this._getChildren(child, items); - } - } - finally { - node.containerOpen = false; - } - } - - return items; - }, - - /** - * Associates the URI of the item with the provided ID with the - * provided array of tags. - * If the provided ID does not identify an item with a URI, - * returns immediately. - */ - _tagID: function _tagID(itemID, tags) { - if (!itemID || !tags) { - return; - } - - try { - let u = PlacesUtils.bookmarks.getBookmarkURI(itemID); - this._tagURI(u, tags); - } catch (e) { - this._log.warn("Got exception fetching URI for " + itemID + ": not tagging. ", e); - - // I guess it doesn't have a URI. Don't try to tag it. - return; - } - }, - - /** - * Associate the provided URI with the provided array of tags. - * If the provided URI is falsy, returns immediately. - */ - _tagURI: function _tagURI(bookmarkURI, tags) { - if (!bookmarkURI || !tags) { - return; - } - - // Filter out any null/undefined/empty tags. - tags = tags.filter(t => t); - - // Temporarily tag a dummy URI to preserve tag ids when untagging. - let dummyURI = Utils.makeURI("about:weave#BStore_tagURI"); - PlacesUtils.tagging.tagURI(dummyURI, tags); - PlacesUtils.tagging.untagURI(bookmarkURI, null); - PlacesUtils.tagging.tagURI(bookmarkURI, tags); - PlacesUtils.tagging.untagURI(dummyURI, null); - }, - - getAllIDs: function BStore_getAllIDs() { - let items = {"menu": true, - "toolbar": true}; - for each (let guid in kSpecialIds.guids) { - if (guid != "places" && guid != "tags") - this._getChildren(guid, items); - } - return items; - }, - - wipe: function BStore_wipe() { - let cb = Async.makeSpinningCallback(); - Task.spawn(function() { - // Save a backup before clearing out all bookmarks. - yield PlacesBackups.create(null, true); - for each (let guid in kSpecialIds.guids) - if (guid != "places") { - let id = kSpecialIds.specialIdForGUID(guid); - if (id) - PlacesUtils.bookmarks.removeFolderChildren(id); - } - cb(); - }); - cb.wait(); - } -}; - -function BookmarksTracker(name, engine) { - Tracker.call(this, name, engine); - - Svc.Obs.add("places-shutdown", this); -} -BookmarksTracker.prototype = { - __proto__: Tracker.prototype, - - startTracking: function() { - PlacesUtils.bookmarks.addObserver(this, true); - Svc.Obs.add("bookmarks-restore-begin", this); - Svc.Obs.add("bookmarks-restore-success", this); - Svc.Obs.add("bookmarks-restore-failed", this); - }, - - stopTracking: function() { - PlacesUtils.bookmarks.removeObserver(this); - Svc.Obs.remove("bookmarks-restore-begin", this); - Svc.Obs.remove("bookmarks-restore-success", this); - Svc.Obs.remove("bookmarks-restore-failed", this); - }, - - observe: function observe(subject, topic, data) { - Tracker.prototype.observe.call(this, subject, topic, data); - - switch (topic) { - case "bookmarks-restore-begin": - this._log.debug("Ignoring changes from importing bookmarks."); - this.ignoreAll = true; - break; - case "bookmarks-restore-success": - this._log.debug("Tracking all items on successful import."); - this.ignoreAll = false; - - this._log.debug("Restore succeeded: wiping server and other clients."); - this.engine.service.resetClient([this.name]); - this.engine.service.wipeServer([this.name]); - this.engine.service.clientsEngine.sendCommand("wipeEngine", [this.name]); - break; - case "bookmarks-restore-failed": - this._log.debug("Tracking all items on failed import."); - this.ignoreAll = false; - break; - } - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsINavBookmarkObserver, - Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS, - Ci.nsISupportsWeakReference - ]), - - /** - * Add a bookmark GUID to be uploaded and bump up the sync score. - * - * @param itemGuid - * GUID of the bookmark to upload. - */ - _add: function BMT__add(itemId, guid) { - guid = kSpecialIds.specialGUIDForId(itemId) || guid; - if (this.addChangedID(guid)) - this._upScore(); - }, - - /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */ - _upScore: function BMT__upScore() { - this.score += SCORE_INCREMENT_XLARGE; - }, - - /** - * Determine if a change should be ignored. - * - * @param itemId - * Item under consideration to ignore - * @param folder (optional) - * Folder of the item being changed - */ - _ignore: function BMT__ignore(itemId, folder, guid) { - // Ignore unconditionally if the engine tells us to. - if (this.ignoreAll) - return true; - - // Get the folder id if we weren't given one. - if (folder == null) { - try { - folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId); - } catch (ex) { - this._log.debug("getFolderIdForItem(" + itemId + - ") threw; calling _ensureMobileQuery."); - // I'm guessing that gFIFI can throw, and perhaps that's why - // _ensureMobileQuery is here at all. Try not to call it. - this._ensureMobileQuery(); - folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId); - } - } - - // Ignore changes to tags (folders under the tags folder). - let tags = kSpecialIds.tags; - if (folder == tags) - return true; - - // Ignore tag items (the actual instance of a tag for a bookmark). - if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags) - return true; - - // Make sure to remove items that have the exclude annotation. - if (PlacesUtils.annotations.itemHasAnnotation(itemId, EXCLUDEBACKUP_ANNO)) { - this.removeChangedID(guid); - return true; - } - - return false; - }, - - onItemAdded: function BMT_onItemAdded(itemId, folder, index, - itemType, uri, title, dateAdded, - guid, parentGuid) { - if (this._ignore(itemId, folder, guid)) - return; - - this._log.trace("onItemAdded: " + itemId); - this._add(itemId, guid); - this._add(folder, parentGuid); - }, - - onItemRemoved: function (itemId, parentId, index, type, uri, - guid, parentGuid) { - if (this._ignore(itemId, parentId, guid)) { - return; - } - - this._log.trace("onItemRemoved: " + itemId); - this._add(itemId, guid); - this._add(parentId, parentGuid); - }, - - _ensureMobileQuery: function _ensureMobileQuery() { - let find = val => - PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter( - id => PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val - ); - - // Don't continue if the Library isn't ready - let all = find(ALLBOOKMARKS_ANNO); - if (all.length == 0) - return; - - // Disable handling of notifications while changing the mobile query - this.ignoreAll = true; - - let mobile = find(MOBILE_ANNO); - let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile); - let title = Str.sync.get("mobile.label"); - - // Don't add OR remove the mobile bookmarks if there's nothing. - if (PlacesUtils.bookmarks.getIdForItemAt(kSpecialIds.mobile, 0) == -1) { - if (mobile.length != 0) - PlacesUtils.bookmarks.removeItem(mobile[0]); - } - // Add the mobile bookmarks query if it doesn't exist - else if (mobile.length == 0) { - let query = PlacesUtils.bookmarks.insertBookmark(all[0], queryURI, -1, title); - PlacesUtils.annotations.setItemAnnotation(query, ORGANIZERQUERY_ANNO, MOBILE_ANNO, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - PlacesUtils.annotations.setItemAnnotation(query, EXCLUDEBACKUP_ANNO, 1, 0, - PlacesUtils.annotations.EXPIRE_NEVER); - } - // Make sure the existing title is correct - else if (PlacesUtils.bookmarks.getItemTitle(mobile[0]) != title) { - PlacesUtils.bookmarks.setItemTitle(mobile[0], title); - } - - this.ignoreAll = false; - }, - - // This method is oddly structured, but the idea is to return as quickly as - // possible -- this handler gets called *every time* a bookmark changes, for - // *each change*. - onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value, - lastModified, itemType, parentId, - guid, parentGuid) { - // Quicker checks first. - if (this.ignoreAll) - return; - - if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1)) - // Ignore annotations except for the ones that we sync. - return; - - // Ignore favicon changes to avoid unnecessary churn. - if (property == "favicon") - return; - - if (this._ignore(itemId, parentId, guid)) - return; - - this._log.trace("onItemChanged: " + itemId + - (", " + property + (isAnno? " (anno)" : "")) + - (value ? (" = \"" + value + "\"") : "")); - this._add(itemId, guid); - }, - - onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, - newParent, newIndex, itemType, - guid, oldParentGuid, newParentGuid) { - if (this._ignore(itemId, newParent, guid)) - return; - - this._log.trace("onItemMoved: " + itemId); - this._add(oldParent, oldParentGuid); - if (oldParent != newParent) { - this._add(itemId, guid); - this._add(newParent, newParentGuid); - } - - // Remove any position annotations now that the user moved the item - PlacesUtils.annotations.removeItemAnnotation(itemId, PARENT_ANNO); - }, - - onBeginUpdateBatch: function () {}, - onEndUpdateBatch: function () {}, - onItemVisited: function () {} -}; diff --git a/components/weave/src/engines/clients.js b/components/weave/src/engines/clients.js deleted file mode 100644 index 6c8e37a7b..000000000 --- a/components/weave/src/engines/clients.js +++ /dev/null @@ -1,476 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "ClientEngine", - "ClientsRec" -]; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-common/stringbundle.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); - -const CLIENTS_TTL = 1814400; // 21 days -const CLIENTS_TTL_REFRESH = 604800; // 7 days - -const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"]; - -this.ClientsRec = function ClientsRec(collection, id) { - CryptoWrapper.call(this, collection, id); -} -ClientsRec.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.Clients", - ttl: CLIENTS_TTL -}; - -Utils.deferGetSet(ClientsRec, - "cleartext", - ["name", "type", "commands", - "version", "protocols", - "formfactor", "os", "appPackage", "application", "device"]); - - -this.ClientEngine = function ClientEngine(service) { - SyncEngine.call(this, "Clients", service); - - // Reset the client on every startup so that we fetch recent clients - this._resetClient(); -} -ClientEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: ClientStore, - _recordObj: ClientsRec, - _trackerObj: ClientsTracker, - - // Always sync client data as it controls other sync behavior - get enabled() true, - - get lastRecordUpload() { - return Svc.Prefs.get(this.name + ".lastRecordUpload", 0); - }, - set lastRecordUpload(value) { - Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value)); - }, - - // Aggregate some stats on the composition of clients on this account - get stats() { - let stats = { - hasMobile: this.localType == "mobile", - names: [this.localName], - numClients: 1, - }; - - for (let id in this._store._remoteClients) { - let {name, type, stale} = this._store._remoteClients[id]; - if (!stale) { - stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE; - stats.names.push(name); - stats.numClients++; - } - } - - return stats; - }, - - /** - * Obtain information about device types. - * - * Returns a Map of device types to integer counts. - */ - get deviceTypes() { - let counts = new Map(); - - counts.set(this.localType, 1); - - for (let id in this._store._remoteClients) { - let record = this._store._remoteClients[id]; - if (record.stale) { - continue; // pretend "stale" records don't exist. - } - let type = record.type; - if (!counts.has(type)) { - counts.set(type, 0); - } - - counts.set(type, counts.get(type) + 1); - } - - return counts; - }, - - get localID() { - // Generate a random GUID id we don't have one - let localID = Svc.Prefs.get("client.GUID", ""); - return localID == "" ? this.localID = Utils.makeGUID() : localID; - }, - set localID(value) Svc.Prefs.set("client.GUID", value), - - get brandName() { - let brand = new StringBundle("chrome://branding/locale/brand.properties"); - return brand.get("brandShortName"); - }, - - get localName() { - let localName = Svc.Prefs.get("client.name", ""); - if (localName != "") - return localName; - - return this.localName = Utils.getDefaultDeviceName(); - }, - set localName(value) Svc.Prefs.set("client.name", value), - - get localType() Svc.Prefs.get("client.type", "desktop"), - set localType(value) Svc.Prefs.set("client.type", value), - - isMobile: function isMobile(id) { - if (this._store._remoteClients[id]) - return this._store._remoteClients[id].type == "mobile"; - return false; - }, - - _syncStartup: function _syncStartup() { - // Reupload new client record periodically. - if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) { - this._tracker.addChangedID(this.localID); - this.lastRecordUpload = Date.now() / 1000; - } - SyncEngine.prototype._syncStartup.call(this); - }, - - // Always process incoming items because they might have commands - _reconcile: function _reconcile() { - return true; - }, - - // Treat reset the same as wiping for locally cached clients - _resetClient() { - this._wipeClient(); - }, - - _wipeClient: function _wipeClient() { - SyncEngine.prototype._resetClient.call(this); - this._store.wipe(); - }, - - removeClientData: function removeClientData() { - let res = this.service.resource(this.engineURL + "/" + this.localID); - res.delete(); - }, - - // Override the default behavior to delete bad records from the server. - handleHMACMismatch: function handleHMACMismatch(item, mayRetry) { - this._log.debug("Handling HMAC mismatch for " + item.id); - - let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry); - if (base != SyncEngine.kRecoveryStrategy.error) - return base; - - // It's a bad client record. Save it to be deleted at the end of the sync. - this._log.debug("Bad client record detected. Scheduling for deletion."); - this._deleteId(item.id); - - // Neither try again nor error; we're going to delete it. - return SyncEngine.kRecoveryStrategy.ignore; - }, - - /** - * A hash of valid commands that the client knows about. The key is a command - * and the value is a hash containing information about the command such as - * number of arguments and description. - */ - _commands: { - resetAll: { args: 0, desc: "Clear temporary local data for all engines" }, - resetEngine: { args: 1, desc: "Clear temporary local data for engine" }, - wipeAll: { args: 0, desc: "Delete all client data for all engines" }, - wipeEngine: { args: 1, desc: "Delete all client data for engine" }, - logout: { args: 0, desc: "Log out client" }, - displayURI: { args: 3, desc: "Instruct a client to display a URI" }, - }, - - /** - * Remove any commands for the local client and mark it for upload. - */ - clearCommands: function clearCommands() { - delete this.localCommands; - this._tracker.addChangedID(this.localID); - }, - - /** - * Sends a command+args pair to a specific client. - * - * @param command Command string - * @param args Array of arguments/data for command - * @param clientId Client to send command to - */ - _sendCommandToClient: function sendCommandToClient(command, args, clientId) { - this._log.trace("Sending " + command + " to " + clientId); - - let client = this._store._remoteClients[clientId]; - if (!client) { - throw new Error("Unknown remote client ID: '" + clientId + "'."); - } - - // notDupe compares two commands and returns if they are not equal. - let notDupe = function(other) { - return other.command != command || !Utils.deepEquals(other.args, args); - }; - - let action = { - command: command, - args: args, - }; - - if (!client.commands) { - client.commands = [action]; - } - // Add the new action if there are no duplicates. - else if (client.commands.every(notDupe)) { - client.commands.push(action); - } - // It must be a dupe. Skip. - else { - return; - } - - this._log.trace("Client " + clientId + " got a new action: " + [command, args]); - this._tracker.addChangedID(clientId); - }, - - /** - * Check if the local client has any remote commands and perform them. - * - * @return false to abort sync - */ - processIncomingCommands: function processIncomingCommands() { - return this._notify("clients:process-commands", "", function() { - let commands = this.localCommands; - - // Immediately clear out the commands as we've got them locally. - this.clearCommands(); - - // Process each command in order. - for each (let {command, args} in commands) { - this._log.debug("Processing command: " + command + "(" + args + ")"); - - let engines = [args[0]]; - switch (command) { - case "resetAll": - engines = null; - // Fallthrough - case "resetEngine": - this.service.resetClient(engines); - break; - case "wipeAll": - engines = null; - // Fallthrough - case "wipeEngine": - this.service.wipeClient(engines); - break; - case "logout": - this.service.logout(); - return false; - case "displayURI": - this._handleDisplayURI.apply(this, args); - break; - default: - this._log.debug("Received an unknown command: " + command); - break; - } - } - - return true; - })(); - }, - - /** - * 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 command - * Command to invoke on remote clients - * @param args - * Array of arguments to give to the command - * @param clientId - * Client ID to send command to. If undefined, send to all remote - * clients. - */ - sendCommand: function sendCommand(command, args, clientId) { - let commandData = this._commands[command]; - // Don't send commands that we don't know about. - if (!commandData) { - this._log.error("Unknown command to send: " + command); - return; - } - // Don't send a command with the wrong number of arguments. - else if (!args || args.length != commandData.args) { - this._log.error("Expected " + commandData.args + " args for '" + - command + "', but got " + args); - return; - } - - if (clientId) { - this._sendCommandToClient(command, args, clientId); - } else { - for (let id in this._store._remoteClients) { - this._sendCommandToClient(command, args, id); - } - } - }, - - /** - * Send a URI to another client for display. - * - * A side effect is the score is increased dramatically to incur an - * immediate sync. - * - * If an unknown client ID is specified, sendCommand() will throw an - * Error object. - * - * @param uri - * URI (as a string) to send and display on the remote client - * @param clientId - * ID of client to send the command to. If not defined, will be sent - * to all remote clients. - * @param title - * Title of the page being sent. - */ - sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) { - this._log.info("Sending URI to client: " + uri + " -> " + - clientId + " (" + title + ")"); - this.sendCommand("displayURI", [uri, this.localID, title], clientId); - - this._tracker.score += SCORE_INCREMENT_XLARGE; - }, - - /** - * Handle a single received 'displayURI' command. - * - * Interested parties should observe the "weave:engine:clients:display-uri" - * topic. The callback will receive an object as the subject parameter with - * the following keys: - * - * uri URI (string) that is requested for display. - * clientId ID of client that sent the command. - * title Title of page that loaded URI (likely) corresponds to. - * - * The 'data' parameter to the callback will not be defined. - * - * @param uri - * String URI that was received - * @param clientId - * ID of client that sent URI - * @param title - * String title of page that URI corresponds to. Older clients may not - * send this. - */ - _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) { - this._log.info("Received a URI for display: " + uri + " (" + title + - ") from " + clientId); - - let subject = {uri: uri, client: clientId, title: title}; - Svc.Obs.notify("weave:engine:clients:display-uri", subject); - } -}; - -function ClientStore(name, engine) { - Store.call(this, name, engine); -} -ClientStore.prototype = { - __proto__: Store.prototype, - - create(record) { - this.update(record) - }, - - update: function update(record) { - // Only grab commands from the server; local name/type always wins - if (record.id == this.engine.localID) - this.engine.localCommands = record.commands; - else - this._remoteClients[record.id] = record.cleartext; - }, - - createRecord: function createRecord(id, collection) { - let record = new ClientsRec(collection, id); - - // Package the individual components into a record for the local client - if (id == this.engine.localID) { - record.name = this.engine.localName; - record.type = this.engine.localType; - record.commands = this.engine.localCommands; - record.version = Services.appinfo.version; - record.protocols = SUPPORTED_PROTOCOL_VERSIONS; - - // Optional fields. - record.os = Services.appinfo.OS; // "Darwin" - record.appPackage = Services.appinfo.ID; - record.application = this.engine.brandName // "Nightly" - - // We can't compute these yet. - // record.device = ""; // Bug 1100723 - // record.formfactor = ""; // Bug 1100722 - } else { - record.cleartext = this._remoteClients[id]; - } - - return record; - }, - - itemExists(id) { - return id in this.getAllIDs(); - }, - - getAllIDs: function getAllIDs() { - let ids = {}; - ids[this.engine.localID] = true; - for (let id in this._remoteClients) - ids[id] = true; - return ids; - }, - - wipe: function wipe() { - this._remoteClients = {}; - }, -}; - -function ClientsTracker(name, engine) { - Tracker.call(this, name, engine); - Svc.Obs.add("weave:engine:start-tracking", this); - Svc.Obs.add("weave:engine:stop-tracking", this); -} -ClientsTracker.prototype = { - __proto__: Tracker.prototype, - - _enabled: false, - - observe: function observe(subject, topic, data) { - switch (topic) { - case "weave:engine:start-tracking": - if (!this._enabled) { - Svc.Prefs.observe("client.name", this); - this._enabled = true; - } - break; - case "weave:engine:stop-tracking": - if (this._enabled) { - Svc.Prefs.ignore("clients.name", this); - this._enabled = false; - } - break; - case "nsPref:changed": - this._log.debug("client.name preference changed"); - this.addChangedID(Svc.Prefs.get("client.GUID")); - this.score += SCORE_INCREMENT_XLARGE; - break; - } - } -}; diff --git a/components/weave/src/engines/forms.js b/components/weave/src/engines/forms.js deleted file mode 100644 index b5b3f7732..000000000 --- a/components/weave/src/engines/forms.js +++ /dev/null @@ -1,246 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ['FormEngine', 'FormRec']; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://gre/modules/Async.jsm"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Log.jsm"); - -const FORMS_TTL = 5184000; // 60 days - -this.FormRec = function FormRec(collection, id) { - CryptoWrapper.call(this, collection, id); -} -FormRec.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.Form", - ttl: FORMS_TTL -}; - -Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]); - - -var FormWrapper = { - _log: Log.repository.getLogger("Sync.Engine.Forms"), - - _getEntryCols: ["fieldname", "value"], - _guidCols: ["guid"], - - // Do a "sync" search by spinning the event loop until it completes. - _searchSpinningly: function(terms, searchData) { - let results = []; - let cb = Async.makeSpinningCallback(); - let callbacks = { - handleResult: function(result) { - results.push(result); - }, - handleCompletion: function(reason) { - cb(null, results); - } - }; - Svc.FormHistory.search(terms, searchData, callbacks); - return cb.wait(); - }, - - _updateSpinningly: function(changes) { - if (!Svc.FormHistory.enabled) { - return; // update isn't going to do anything. - } - let cb = Async.makeSpinningCallback(); - let callbacks = { - handleCompletion: function(reason) { - cb(); - } - }; - Svc.FormHistory.update(changes, callbacks); - return cb.wait(); - }, - - getEntry: function (guid) { - let results = this._searchSpinningly(this._getEntryCols, {guid: guid}); - if (!results.length) { - return null; - } - return {name: results[0].fieldname, value: results[0].value}; - }, - - getGUID: function (name, value) { - // Query for the provided entry. - let query = { fieldname: name, value: value }; - let results = this._searchSpinningly(this._guidCols, query); - return results.length ? results[0].guid : null; - }, - - hasGUID: function (guid) { - // We could probably use a count function here, but searchSpinningly exists... - return this._searchSpinningly(this._guidCols, {guid: guid}).length != 0; - }, - - replaceGUID: function (oldGUID, newGUID) { - let changes = { - op: "update", - guid: oldGUID, - newGuid: newGUID, - } - this._updateSpinningly(changes); - } - -}; - -this.FormEngine = function FormEngine(service) { - SyncEngine.call(this, "Forms", service); -} -FormEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: FormStore, - _trackerObj: FormTracker, - _recordObj: FormRec, - applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE, - - syncPriority: 6, - - get prefName() "history", - - _findDupe: function _findDupe(item) { - return FormWrapper.getGUID(item.name, item.value); - } -}; - -function FormStore(name, engine) { - Store.call(this, name, engine); -} -FormStore.prototype = { - __proto__: Store.prototype, - - _processChange: function (change) { - // If this._changes is defined, then we are applying a batch, so we - // can defer it. - if (this._changes) { - this._changes.push(change); - return; - } - - // Otherwise we must handle the change synchronously, right now. - FormWrapper._updateSpinningly(change); - }, - - applyIncomingBatch: function (records) { - // We collect all the changes to be made then apply them all at once. - this._changes = []; - let failures = Store.prototype.applyIncomingBatch.call(this, records); - if (this._changes.length) { - FormWrapper._updateSpinningly(this._changes); - } - delete this._changes; - return failures; - }, - - getAllIDs: function () { - let results = FormWrapper._searchSpinningly(["guid"], []) - let guids = {}; - for (let result of results) { - guids[result.guid] = true; - } - return guids; - }, - - changeItemID: function (oldID, newID) { - FormWrapper.replaceGUID(oldID, newID); - }, - - itemExists: function (id) { - return FormWrapper.hasGUID(id); - }, - - createRecord: function (id, collection) { - let record = new FormRec(collection, id); - let entry = FormWrapper.getEntry(id); - if (entry != null) { - record.name = entry.name; - record.value = entry.value; - } else { - record.deleted = true; - } - return record; - }, - - create: function (record) { - this._log.trace("Adding form record for " + record.name); - let change = { - op: "add", - fieldname: record.name, - value: record.value - }; - this._processChange(change); - }, - - remove: function (record) { - this._log.trace("Removing form record: " + record.id); - let change = { - op: "remove", - guid: record.id - }; - this._processChange(change); - }, - - update: function (record) { - this._log.trace("Ignoring form record update request!"); - }, - - wipe: function () { - let change = { - op: "remove" - }; - FormWrapper._updateSpinningly(change); - } -}; - -function FormTracker(name, engine) { - Tracker.call(this, name, engine); -} -FormTracker.prototype = { - __proto__: Tracker.prototype, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - startTracking: function() { - Svc.Obs.add("satchel-storage-changed", this); - }, - - stopTracking: function() { - Svc.Obs.remove("satchel-storage-changed", this); - }, - - observe: function (subject, topic, data) { - Tracker.prototype.observe.call(this, subject, topic, data); - if (this.ignoreAll) { - return; - } - - switch (topic) { - case "satchel-storage-changed": - if (data == "formhistory-add" || data == "formhistory-remove") { - let guid = subject.QueryInterface(Ci.nsISupportsString).toString(); - this.trackEntry(guid); - } - break; - } - }, - - trackEntry: function (guid) { - this.addChangedID(guid); - this.score += SCORE_INCREMENT_MEDIUM; - }, -}; diff --git a/components/weave/src/engines/history.js b/components/weave/src/engines/history.js deleted file mode 100644 index 35dfd7811..000000000 --- a/components/weave/src/engines/history.js +++ /dev/null @@ -1,417 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ['HistoryEngine', 'HistoryRec']; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; -var Cr = Components.results; - -const HISTORY_TTL = 5184000; // 60 days - -Cu.import("resource://gre/modules/PlacesUtils.jsm", this); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Async.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); - -this.HistoryRec = function HistoryRec(collection, id) { - CryptoWrapper.call(this, collection, id); -} -HistoryRec.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.History", - ttl: HISTORY_TTL -}; - -Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]); - - -this.HistoryEngine = function HistoryEngine(service) { - SyncEngine.call(this, "History", service); -} -HistoryEngine.prototype = { - __proto__: SyncEngine.prototype, - _recordObj: HistoryRec, - _storeObj: HistoryStore, - _trackerObj: HistoryTracker, - downloadLimit: MAX_HISTORY_DOWNLOAD, - applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE, - - syncPriority: 7, -}; - -function HistoryStore(name, engine) { - Store.call(this, name, engine); - - // Explicitly nullify our references to our cached services so we don't leak - Svc.Obs.add("places-shutdown", function() { - for (let query in this._stmts) { - let stmt = this._stmts; - stmt.finalize(); - } - this._stmts = {}; - }, this); -} -HistoryStore.prototype = { - __proto__: Store.prototype, - - __asyncHistory: null, - get _asyncHistory() { - if (!this.__asyncHistory) { - this.__asyncHistory = Cc["@mozilla.org/browser/history;1"] - .getService(Ci.mozIAsyncHistory); - } - return this.__asyncHistory; - }, - - _stmts: {}, - _getStmt: function(query) { - if (query in this._stmts) { - return this._stmts[query]; - } - - this._log.trace("Creating SQL statement: " + query); - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) - .DBConnection; - return this._stmts[query] = db.createAsyncStatement(query); - }, - - get _setGUIDStm() { - return this._getStmt( - "UPDATE moz_places " + - "SET guid = :guid " + - "WHERE url = :page_url"); - }, - - // Some helper functions to handle GUIDs - setGUID: function setGUID(uri, guid) { - uri = uri.spec ? uri.spec : uri; - - if (!guid) { - guid = Utils.makeGUID(); - } - - let stmt = this._setGUIDStm; - stmt.params.guid = guid; - stmt.params.page_url = uri; - Async.querySpinningly(stmt); - return guid; - }, - - get _guidStm() { - return this._getStmt( - "SELECT guid " + - "FROM moz_places " + - "WHERE url = :page_url"); - }, - _guidCols: ["guid"], - - GUIDForUri: function GUIDForUri(uri, create) { - let stm = this._guidStm; - stm.params.page_url = uri.spec ? uri.spec : uri; - - // Use the existing GUID if it exists - let result = Async.querySpinningly(stm, this._guidCols)[0]; - if (result && result.guid) - return result.guid; - - // Give the uri a GUID if it doesn't have one - if (create) - return this.setGUID(uri); - }, - - get _visitStm() { - return this._getStmt( - "/* do not warn (bug 599936) */ " + - "SELECT visit_type type, visit_date date " + - "FROM moz_historyvisits " + - "WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " + - "ORDER BY date DESC LIMIT 10"); - }, - _visitCols: ["date", "type"], - - get _urlStm() { - return this._getStmt( - "SELECT url, title, frecency " + - "FROM moz_places " + - "WHERE guid = :guid"); - }, - _urlCols: ["url", "title", "frecency"], - - get _allUrlStm() { - return this._getStmt( - "SELECT url " + - "FROM moz_places " + - "WHERE last_visit_date > :cutoff_date " + - "ORDER BY frecency DESC " + - "LIMIT :max_results"); - }, - _allUrlCols: ["url"], - - // See bug 320831 for why we use SQL here - _getVisits: function HistStore__getVisits(uri) { - this._visitStm.params.url = uri; - return Async.querySpinningly(this._visitStm, this._visitCols); - }, - - // See bug 468732 for why we use SQL here - _findURLByGUID: function HistStore__findURLByGUID(guid) { - this._urlStm.params.guid = guid; - return Async.querySpinningly(this._urlStm, this._urlCols)[0]; - }, - - changeItemID: function HStore_changeItemID(oldID, newID) { - this.setGUID(this._findURLByGUID(oldID).url, newID); - }, - - - getAllIDs: function HistStore_getAllIDs() { - // Only get places visited within the last 30 days (30*24*60*60*1000ms) - this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000; - this._allUrlStm.params.max_results = MAX_HISTORY_UPLOAD; - - let urls = Async.querySpinningly(this._allUrlStm, this._allUrlCols); - let self = this; - return urls.reduce(function(ids, item) { - ids[self.GUIDForUri(item.url, true)] = item.url; - return ids; - }, {}); - }, - - applyIncomingBatch: function applyIncomingBatch(records) { - let failed = []; - - // Convert incoming records to mozIPlaceInfo objects. Some records can be - // ignored or handled directly, so we're rewriting the array in-place. - let i, k; - for (i = 0, k = 0; i < records.length; i++) { - let record = records[k] = records[i]; - let shouldApply; - - // This is still synchronous I/O for now. - try { - if (record.deleted) { - // Consider using nsIBrowserHistory::removePages() here. - this.remove(record); - // No further processing needed. Remove it from the list. - shouldApply = false; - } else { - shouldApply = this._recordToPlaceInfo(record); - } - } catch(ex) { - failed.push(record.id); - shouldApply = false; - } - - if (shouldApply) { - k += 1; - } - } - records.length = k; // truncate array - - // Nothing to do. - if (!records.length) { - return failed; - } - - let updatePlacesCallback = { - handleResult: function handleResult() {}, - handleError: function handleError(resultCode, placeInfo) { - failed.push(placeInfo.guid); - }, - handleCompletion: Async.makeSyncCallback() - }; - this._asyncHistory.updatePlaces(records, updatePlacesCallback); - Async.waitForSyncCallback(updatePlacesCallback.handleCompletion); - return failed; - }, - - /** - * Converts a Sync history record to a mozIPlaceInfo. - * - * Throws if an invalid record is encountered (invalid URI, etc.), - * returns true if the record is to be applied, false otherwise - * (no visits to add, etc.), - */ - _recordToPlaceInfo: function _recordToPlaceInfo(record) { - // Sort out invalid URIs and ones Places just simply doesn't want. - record.uri = Utils.makeURI(record.histUri); - if (!record.uri) { - this._log.warn("Attempted to process invalid URI, skipping."); - throw "Invalid URI in record"; - } - - if (!Utils.checkGUID(record.id)) { - this._log.warn("Encountered record with invalid GUID: " + record.id); - return false; - } - record.guid = record.id; - - if (!PlacesUtils.history.canAddURI(record.uri)) { - this._log.trace("Ignoring record " + record.id + " with URI " - + record.uri.spec + ": can't add this URI."); - return false; - } - - // We dupe visits by date and type. So an incoming visit that has - // the same timestamp and type as a local one won't get applied. - // To avoid creating new objects, we rewrite the query result so we - // can simply check for containment below. - let curVisits = this._getVisits(record.histUri); - let i, k; - for (i = 0; i < curVisits.length; i++) { - curVisits[i] = curVisits[i].date + "," + curVisits[i].type; - } - - // Walk through the visits, make sure we have sound data, and eliminate - // dupes. The latter is done by rewriting the array in-place. - for (i = 0, k = 0; i < record.visits.length; i++) { - let visit = record.visits[k] = record.visits[i]; - - if (!visit.date || typeof visit.date != "number") { - this._log.warn("Encountered record with invalid visit date: " - + visit.date); - throw "Visit has no date!"; - } - - if (!visit.type || !(visit.type >= PlacesUtils.history.TRANSITION_LINK && - visit.type <= PlacesUtils.history.TRANSITION_RELOAD)) { - this._log.warn("Encountered record with invalid visit type: " - + visit.type); - throw "Invalid visit type!"; - } - - // Dates need to be integers. - visit.date = Math.round(visit.date); - - if (curVisits.indexOf(visit.date + "," + visit.type) != -1) { - // Visit is a dupe, don't increment 'k' so the element will be - // overwritten. - continue; - } - visit.visitDate = visit.date; - visit.transitionType = visit.type; - k += 1; - } - record.visits.length = k; // truncate array - - // No update if there aren't any visits to apply. - // mozIAsyncHistory::updatePlaces() wants at least one visit. - // In any case, the only thing we could change would be the title - // and that shouldn't change without a visit. - if (!record.visits.length) { - this._log.trace("Ignoring record " + record.id + " with URI " - + record.uri.spec + ": no visits to add."); - return false; - } - - return true; - }, - - remove: function HistStore_remove(record) { - let page = this._findURLByGUID(record.id); - if (page == null) { - this._log.debug("Page already removed: " + record.id); - return; - } - - let uri = Utils.makeURI(page.url); - PlacesUtils.history.removePage(uri); - this._log.trace("Removed page: " + [record.id, page.url, page.title]); - }, - - itemExists: function HistStore_itemExists(id) { - return !!this._findURLByGUID(id); - }, - - createRecord: function createRecord(id, collection) { - let foo = this._findURLByGUID(id); - let record = new HistoryRec(collection, id); - if (foo) { - record.histUri = foo.url; - record.title = foo.title; - record.sortindex = foo.frecency; - record.visits = this._getVisits(record.histUri); - } else { - record.deleted = true; - } - - return record; - }, - - wipe: function HistStore_wipe() { - PlacesUtils.history.clear(); - } -}; - -function HistoryTracker(name, engine) { - Tracker.call(this, name, engine); -} -HistoryTracker.prototype = { - __proto__: Tracker.prototype, - - startTracking: function() { - this._log.info("Adding Places observer."); - PlacesUtils.history.addObserver(this, true); - }, - - stopTracking: function() { - this._log.info("Removing Places observer."); - PlacesUtils.history.removeObserver(this); - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsINavHistoryObserver, - Ci.nsISupportsWeakReference - ]), - - onDeleteAffectsGUID: function (uri, guid, reason, source, increment) { - if (this.ignoreAll || reason == Ci.nsINavHistoryObserver.REASON_EXPIRED) { - return; - } - this._log.trace(source + ": " + uri.spec + ", reason " + reason); - if (this.addChangedID(guid)) { - this.score += increment; - } - }, - - onDeleteVisits: function (uri, visitTime, guid, reason) { - this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL); - }, - - onDeleteURI: function (uri, guid, reason) { - this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE); - }, - - onVisit: function (uri, vid, time, session, referrer, trans, guid) { - if (this.ignoreAll) { - this._log.trace("ignoreAll: ignoring visit for " + guid); - return; - } - - this._log.trace("onVisit: " + uri.spec); - if (this.addChangedID(guid)) { - this.score += SCORE_INCREMENT_SMALL; - } - }, - - onClearHistory: function () { - this._log.trace("onClearHistory"); - // Note that we're going to trigger a sync, but none of the cleared - // pages are tracked, so the deletions will not be propagated. - // See Bug 578694. - this.score += SCORE_INCREMENT_XLARGE; - }, - - onBeginUpdateBatch: function () {}, - onEndUpdateBatch: function () {}, - onPageChanged: function () {}, - onTitleChanged: function () {}, - onBeforeDeleteURI: function () {}, -}; diff --git a/components/weave/src/engines/passwords.js b/components/weave/src/engines/passwords.js deleted file mode 100644 index 0ccd2e7b0..000000000 --- a/components/weave/src/engines/passwords.js +++ /dev/null @@ -1,305 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ['PasswordEngine', 'LoginRec']; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/util.js"); - -this.LoginRec = function LoginRec(collection, id) { - CryptoWrapper.call(this, collection, id); -} -LoginRec.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.Login", -}; - -Utils.deferGetSet(LoginRec, "cleartext", [ - "hostname", "formSubmitURL", - "httpRealm", "username", "password", "usernameField", "passwordField", - ]); - - -this.PasswordEngine = function PasswordEngine(service) { - SyncEngine.call(this, "Passwords", service); -} -PasswordEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: PasswordStore, - _trackerObj: PasswordTracker, - _recordObj: LoginRec, - - applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE, - - syncPriority: 2, - - _syncFinish: function () { - SyncEngine.prototype._syncFinish.call(this); - - // Delete the Weave credentials from the server once. - if (!Svc.Prefs.get("deletePwdFxA", false)) { - try { - let ids = []; - for (let host of Utils.getSyncCredentialsHosts()) { - for (let info of Services.logins.findLogins({}, host, "", "")) { - ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid); - } - } - if (ids.length) { - let coll = new Collection(this.engineURL, null, this.service); - coll.ids = ids; - let ret = coll.delete(); - this._log.debug("Delete result: " + ret); - if (!ret.success && ret.status != 400) { - // A non-400 failure means try again next time. - return; - } - } else { - this._log.debug("Didn't find any passwords to delete"); - } - // If there were no ids to delete, or we succeeded, or got a 400, - // record success. - Svc.Prefs.set("deletePwdFxA", true); - Svc.Prefs.reset("deletePwd"); // The old prefname we previously used. - } catch (ex) { - this._log.debug("Password deletes failed: ", ex); - } - } - }, - - _findDupe: function (item) { - let login = this._store._nsLoginInfoFromRecord(item); - if (!login) { - return; - } - - let logins = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm); - - this._store._sleep(0); // Yield back to main thread after synchronous operation. - - // Look for existing logins that match the hostname, but ignore the password. - for (let local of logins) { - if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) { - return local.guid; - } - } - }, -}; - -function PasswordStore(name, engine) { - Store.call(this, name, engine); - this._nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); -} -PasswordStore.prototype = { - __proto__: Store.prototype, - - _nsLoginInfoFromRecord: function (record) { - function nullUndefined(x) { - return (x == undefined) ? null : x; - } - - if (record.formSubmitURL && record.httpRealm) { - this._log.warn("Record " + record.id + " has both formSubmitURL and httpRealm. Skipping."); - return null; - } - - // Passing in "undefined" results in an empty string, which later - // counts as a value. Explicitly `|| null` these fields according to JS - // truthiness. Records with empty strings or null will be unmolested. - let info = new this._nsLoginInfo(record.hostname, - nullUndefined(record.formSubmitURL), - nullUndefined(record.httpRealm), - record.username, - record.password, - record.usernameField, - record.passwordField); - info.QueryInterface(Ci.nsILoginMetaInfo); - info.guid = record.id; - return info; - }, - - _getLoginFromGUID: function (id) { - let prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2); - prop.setPropertyAsAUTF8String("guid", id); - - let logins = Services.logins.searchLogins({}, prop); - this._sleep(0); // Yield back to main thread after synchronous operation. - - if (logins.length > 0) { - this._log.trace(logins.length + " items matching " + id + " found."); - return logins[0]; - } - - this._log.trace("No items matching " + id + " found. Ignoring"); - return null; - }, - - getAllIDs: function () { - let items = {}; - let logins = Services.logins.getAllLogins({}); - - for (let i = 0; i < logins.length; i++) { - // Skip over Weave password/passphrase entries. - let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); - if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) { - continue; - } - - items[metaInfo.guid] = metaInfo; - } - - return items; - }, - - changeItemID: function (oldID, newID) { - this._log.trace("Changing item ID: " + oldID + " to " + newID); - - let oldLogin = this._getLoginFromGUID(oldID); - if (!oldLogin) { - this._log.trace("Can't change item ID: item doesn't exist"); - return; - } - if (this._getLoginFromGUID(newID)) { - this._log.trace("Can't change item ID: new ID already in use"); - return; - } - - let prop = Cc["@mozilla.org/hash-property-bag;1"] - .createInstance(Ci.nsIWritablePropertyBag2); - prop.setPropertyAsAUTF8String("guid", newID); - - Services.logins.modifyLogin(oldLogin, prop); - }, - - itemExists: function (id) { - return !!this._getLoginFromGUID(id); - }, - - createRecord: function (id, collection) { - let record = new LoginRec(collection, id); - let login = this._getLoginFromGUID(id); - - if (!login) { - record.deleted = true; - return record; - } - - record.hostname = login.hostname; - record.formSubmitURL = login.formSubmitURL; - record.httpRealm = login.httpRealm; - record.username = login.username; - record.password = login.password; - record.usernameField = login.usernameField; - record.passwordField = login.passwordField; - - return record; - }, - - create: function (record) { - let login = this._nsLoginInfoFromRecord(record); - if (!login) { - return; - } - - this._log.debug("Adding login for " + record.hostname); - this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " + - "formSubmitURL: " + JSON.stringify(login.formSubmitURL)); - try { - Services.logins.addLogin(login); - } catch(ex) { - this._log.debug("Adding record " + record.id + - " resulted in exception ", ex); - } - }, - - remove: function (record) { - this._log.trace("Removing login " + record.id); - - let loginItem = this._getLoginFromGUID(record.id); - if (!loginItem) { - this._log.trace("Asked to remove record that doesn't exist, ignoring"); - return; - } - - Services.logins.removeLogin(loginItem); - }, - - update: function (record) { - let loginItem = this._getLoginFromGUID(record.id); - if (!loginItem) { - this._log.debug("Skipping update for unknown item: " + record.hostname); - return; - } - - this._log.debug("Updating " + record.hostname); - let newinfo = this._nsLoginInfoFromRecord(record); - if (!newinfo) { - return; - } - - try { - Services.logins.modifyLogin(loginItem, newinfo); - } catch(ex) { - this._log.debug("Modifying record " + record.id + - " resulted in exception. Not modifying.", ex); - } - }, - - wipe: function () { - Services.logins.removeAllLogins(); - }, -}; - -function PasswordTracker(name, engine) { - Tracker.call(this, name, engine); - Svc.Obs.add("weave:engine:start-tracking", this); - Svc.Obs.add("weave:engine:stop-tracking", this); -} -PasswordTracker.prototype = { - __proto__: Tracker.prototype, - - startTracking: function () { - Svc.Obs.add("passwordmgr-storage-changed", this); - }, - - stopTracking: function () { - Svc.Obs.remove("passwordmgr-storage-changed", this); - }, - - observe: function (subject, topic, data) { - Tracker.prototype.observe.call(this, subject, topic, data); - - if (this.ignoreAll) { - return; - } - - // A single add, remove or change or removing all items - // will trigger a sync for MULTI_DEVICE. - switch (data) { - case "modifyLogin": - subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo); - // Fall through. - case "addLogin": - case "removeLogin": - // Skip over Weave password/passphrase changes. - subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo); - if (Utils.getSyncCredentialsHosts().has(subject.hostname)) { - break; - } - - this.score += SCORE_INCREMENT_XLARGE; - this._log.trace(data + ": " + subject.guid); - this.addChangedID(subject.guid); - break; - case "removeAllLogins": - this._log.trace(data); - this.score += SCORE_INCREMENT_XLARGE; - break; - } - }, -}; diff --git a/components/weave/src/engines/prefs.js b/components/weave/src/engines/prefs.js deleted file mode 100644 index 1a61c517d..000000000 --- a/components/weave/src/engines/prefs.js +++ /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/. */ - -this.EXPORTED_SYMBOLS = ['PrefsEngine', 'PrefRec']; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cu = Components.utils; - -const SYNC_PREFS_PREFIX = "services.sync.prefs.sync."; - -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://gre/modules/LightweightThemeManager.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); - -const PREFS_GUID = CommonUtils.encodeBase64URL(Services.appinfo.ID); - -this.PrefRec = function PrefRec(collection, id) { - CryptoWrapper.call(this, collection, id); -} -PrefRec.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.Pref", -}; - -Utils.deferGetSet(PrefRec, "cleartext", ["value"]); - - -this.PrefsEngine = function PrefsEngine(service) { - SyncEngine.call(this, "Prefs", service); -} -PrefsEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: PrefStore, - _trackerObj: PrefTracker, - _recordObj: PrefRec, - version: 2, - - syncPriority: 1, - - getChangedIDs: function () { - // No need for a proper timestamp (no conflict resolution needed). - let changedIDs = {}; - if (this._tracker.modified) - changedIDs[PREFS_GUID] = 0; - return changedIDs; - }, - - _wipeClient: function () { - SyncEngine.prototype._wipeClient.call(this); - this.justWiped = true; - }, - - _reconcile: function (item) { - // Apply the incoming item if we don't care about the local data - if (this.justWiped) { - this.justWiped = false; - return true; - } - return SyncEngine.prototype._reconcile.call(this, item); - } -}; - - -function PrefStore(name, engine) { - Store.call(this, name, engine); - Svc.Obs.add("profile-before-change", function () { - this.__prefs = null; - }, this); -} -PrefStore.prototype = { - __proto__: Store.prototype, - - __prefs: null, - get _prefs() { - if (!this.__prefs) { - this.__prefs = new Preferences(); - } - return this.__prefs; - }, - - _getSyncPrefs: function () { - let syncPrefs = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefService) - .getBranch(SYNC_PREFS_PREFIX) - .getChildList("", {}); - // Also sync preferences that determine which prefs get synced. - let controlPrefs = syncPrefs.map(pref => SYNC_PREFS_PREFIX + pref); - return controlPrefs.concat(syncPrefs); - }, - - _isSynced: function (pref) { - return pref.startsWith(SYNC_PREFS_PREFIX) || - this._prefs.get(SYNC_PREFS_PREFIX + pref, false); - }, - - _getAllPrefs: function () { - let values = {}; - for each (let pref in this._getSyncPrefs()) { - if (this._isSynced(pref)) { - // Missing prefs get the null value. - values[pref] = this._prefs.get(pref, null); - } - } - return values; - }, - - _setAllPrefs: function (values) { - let enabledPref = "lightweightThemes.isThemeSelected"; - let enabledBefore = this._prefs.get(enabledPref, false); - let prevTheme = LightweightThemeManager.currentTheme; - - // Update 'services.sync.prefs.sync.foo.pref' before 'foo.pref', otherwise - // _isSynced returns false when 'foo.pref' doesn't exist (e.g., on a new device). - let prefs = Object.keys(values).sort(a => -a.indexOf(SYNC_PREFS_PREFIX)); - for (let pref of prefs) { - if (!this._isSynced(pref)) { - continue; - } - - let value = values[pref]; - - // Pref has gone missing. The best we can do is reset it. - if (value == null) { - this._prefs.reset(pref); - continue; - } - - try { - this._prefs.set(pref, value); - } catch(ex) { - this._log.trace("Failed to set pref: " + pref + ": " + ex); - } - } - - // Notify the lightweight theme manager of all the new values - let enabledNow = this._prefs.get(enabledPref, false); - if (enabledBefore && !enabledNow) { - LightweightThemeManager.currentTheme = null; - } else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) { - LightweightThemeManager.currentTheme = null; - LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0]; - } - }, - - getAllIDs: function () { - /* We store all prefs in just one WBO, with just one GUID */ - let allprefs = {}; - allprefs[PREFS_GUID] = true; - return allprefs; - }, - - changeItemID: function (oldID, newID) { - this._log.trace("PrefStore GUID is constant!"); - }, - - itemExists: function (id) { - return (id === PREFS_GUID); - }, - - createRecord: function (id, collection) { - let record = new PrefRec(collection, id); - - if (id == PREFS_GUID) { - record.value = this._getAllPrefs(); - } else { - record.deleted = true; - } - - return record; - }, - - create: function (record) { - this._log.trace("Ignoring create request"); - }, - - remove: function (record) { - this._log.trace("Ignoring remove request"); - }, - - update: function (record) { - // Silently ignore pref updates that are for other apps. - if (record.id != PREFS_GUID) - return; - - this._log.trace("Received pref updates, applying..."); - this._setAllPrefs(record.value); - }, - - wipe: function () { - this._log.trace("Ignoring wipe request"); - } -}; - -function PrefTracker(name, engine) { - Tracker.call(this, name, engine); - Svc.Obs.add("profile-before-change", this); - Svc.Obs.add("weave:engine:start-tracking", this); - Svc.Obs.add("weave:engine:stop-tracking", this); -} -PrefTracker.prototype = { - __proto__: Tracker.prototype, - - get modified() { - return Svc.Prefs.get("engine.prefs.modified", false); - }, - set modified(value) { - Svc.Prefs.set("engine.prefs.modified", value); - }, - - loadChangedIDs: function loadChangedIDs() { - // Don't read changed IDs from disk at start up. - }, - - clearChangedIDs: function clearChangedIDs() { - this.modified = false; - }, - - __prefs: null, - get _prefs() { - if (!this.__prefs) { - this.__prefs = new Preferences(); - } - return this.__prefs; - }, - - startTracking: function () { - Services.prefs.addObserver("", this, false); - }, - - stopTracking: function () { - this.__prefs = null; - Services.prefs.removeObserver("", this); - }, - - observe: function (subject, topic, data) { - Tracker.prototype.observe.call(this, subject, topic, data); - - switch (topic) { - case "profile-before-change": - this.stopTracking(); - break; - case "nsPref:changed": - // Trigger a sync for MULTI-DEVICE for a change that determines - // which prefs are synced or a regular pref change. - if (data.indexOf(SYNC_PREFS_PREFIX) == 0 || - this._prefs.get(SYNC_PREFS_PREFIX + data, false)) { - this.score += SCORE_INCREMENT_XLARGE; - this.modified = true; - this._log.trace("Preference " + data + " changed"); - } - break; - } - } -}; diff --git a/components/weave/src/engines/tabs.js b/components/weave/src/engines/tabs.js deleted file mode 100644 index 167faf625..000000000 --- a/components/weave/src/engines/tabs.js +++ /dev/null @@ -1,376 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["TabEngine", "TabSetRecord"]; - -var {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -const TABS_TTL = 604800; // 7 days. -const TAB_ENTRIES_LIMIT = 25; // How many URLs to include in tab history. - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/engines/clients.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-sync/constants.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); - -this.TabSetRecord = function TabSetRecord(collection, id) { - CryptoWrapper.call(this, collection, id); -} -TabSetRecord.prototype = { - __proto__: CryptoWrapper.prototype, - _logName: "Sync.Record.Tabs", - ttl: TABS_TTL, -}; - -Utils.deferGetSet(TabSetRecord, "cleartext", ["clientName", "tabs"]); - - -this.TabEngine = function TabEngine(service) { - SyncEngine.call(this, "Tabs", service); - - // Reset the client on every startup so that we fetch recent tabs. - this._resetClient(); -} -TabEngine.prototype = { - __proto__: SyncEngine.prototype, - _storeObj: TabStore, - _trackerObj: TabTracker, - _recordObj: TabSetRecord, - - syncPriority: 3, - - getChangedIDs: function () { - // No need for a proper timestamp (no conflict resolution needed). - let changedIDs = {}; - if (this._tracker.modified) - changedIDs[this.service.clientsEngine.localID] = 0; - return changedIDs; - }, - - // API for use by Sync UI code to give user choices of tabs to open. - getAllClients: function () { - return this._store._remoteClients; - }, - - getClientById: function (id) { - return this._store._remoteClients[id]; - }, - - _resetClient: function () { - SyncEngine.prototype._resetClient.call(this); - this._store.wipe(); - this._tracker.modified = true; - }, - - removeClientData: function () { - let url = this.engineURL + "/" + this.service.clientsEngine.localID; - this.service.resource(url).delete(); - }, - - /** - * Return a Set of open URLs. - */ - getOpenURLs: function () { - let urls = new Set(); - for (let entry of this._store.getAllTabs()) { - urls.add(entry.urlHistory[0]); - } - return urls; - }, - - _reconcile: function (item) { - // Skip our own record. - // TabStore.itemExists tests only against our local client ID. - if (this._store.itemExists(item.id)) { - this._log.trace("Ignoring incoming tab item because of its id: " + item.id); - return false; - } - - return SyncEngine.prototype._reconcile.call(this, item); - } -}; - - -function TabStore(name, engine) { - Store.call(this, name, engine); -} -TabStore.prototype = { - __proto__: Store.prototype, - - itemExists: function (id) { - return id == this.engine.service.clientsEngine.localID; - }, - - getWindowEnumerator: function () { - return Services.wm.getEnumerator("navigator:browser"); - }, - - shouldSkipWindow: function (win) { - return win.closed || - PrivateBrowsingUtils.isWindowPrivate(win); - }, - - getTabState: function (tab) { - return JSON.parse(Svc.Session.getTabState(tab)); - }, - - getAllTabs: function (filter) { - let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); - - let allTabs = []; - - let winEnum = this.getWindowEnumerator(); - while (winEnum.hasMoreElements()) { - let win = winEnum.getNext(); - if (this.shouldSkipWindow(win)) { - continue; - } - - for (let tab of win.gBrowser.tabs) { - tabState = this.getTabState(tab); - - // Make sure there are history entries to look at. - if (!tabState || !tabState.entries.length) { - continue; - } - - let acceptable = !filter ? (url) => url : - (url) => url && !filteredUrls.test(url); - - let entries = tabState.entries; - let index = tabState.index; - let current = entries[index - 1]; - - // We ignore the tab completely if the current entry url is - // not acceptable (we need something accurate to open). - if (!acceptable(current.url)) { - continue; - } - - // The element at `index` is the current page. Previous URLs were - // previously visited URLs; subsequent URLs are in the 'forward' stack, - // which we can't represent in Sync, so we truncate here. - let candidates = (entries.length == index) ? - entries : - entries.slice(0, index); - - let urls = candidates.map((entry) => entry.url) - .filter(acceptable) - .reverse(); // Because Sync puts current at index 0, and history after. - - // Truncate if necessary. - if (urls.length > TAB_ENTRIES_LIMIT) { - urls.length = TAB_ENTRIES_LIMIT; - } - - allTabs.push({ - title: current.title || "", - urlHistory: urls, - icon: tabState.attributes && tabState.attributes.image || "", - lastUsed: Math.floor((tabState.lastAccessed || 0) / 1000), - }); - } - } - - return allTabs; - }, - - createRecord: function (id, collection) { - let record = new TabSetRecord(collection, id); - record.clientName = this.engine.service.clientsEngine.localName; - - // Sort tabs in descending-used order to grab the most recently used - let tabs = this.getAllTabs(true).sort(function (a, b) { - return b.lastUsed - a.lastUsed; - }); - - // Figure out how many tabs we can pack into a payload. Starting with a 28KB - // payload, we can estimate various overheads from encryption/JSON/WBO. - let size = JSON.stringify(tabs).length; - let origLength = tabs.length; - const MAX_TAB_SIZE = 20000; - if (size > MAX_TAB_SIZE) { - // Estimate a little more than the direct fraction to maximize packing - let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size); - tabs = tabs.slice(0, cutoff + 1); - - // Keep dropping off the last entry until the data fits - while (JSON.stringify(tabs).length > MAX_TAB_SIZE) - tabs.pop(); - } - - this._log.trace("Created tabs " + tabs.length + " of " + origLength); - tabs.forEach(function (tab) { - this._log.trace("Wrapping tab: " + JSON.stringify(tab)); - }, this); - - record.tabs = tabs; - return record; - }, - - getAllIDs: function () { - // Don't report any tabs if all windows are in private browsing for - // first syncs. - let ids = {}; - let allWindowsArePrivate = false; - let wins = Services.wm.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext())) { - // Ensure that at least there is a private window. - allWindowsArePrivate = true; - } else { - // If there is a not private windown then finish and continue. - allWindowsArePrivate = false; - break; - } - } - - if (allWindowsArePrivate && - !PrivateBrowsingUtils.permanentPrivateBrowsing) { - return ids; - } - - ids[this.engine.service.clientsEngine.localID] = true; - return ids; - }, - - wipe: function () { - this._remoteClients = {}; - }, - - create: function (record) { - this._log.debug("Adding remote tabs from " + record.clientName); - this._remoteClients[record.id] = record.cleartext; - - // Lose some precision, but that's good enough (seconds). - let roundModify = Math.floor(record.modified / 1000); - let notifyState = Svc.Prefs.get("notifyTabState"); - - // If there's no existing pref, save this first modified time. - if (notifyState == null) { - Svc.Prefs.set("notifyTabState", roundModify); - return; - } - - // Don't change notifyState if it's already 0 (don't notify). - if (notifyState == 0) { - return; - } - - // We must have gotten a new tab that isn't the same as last time. - if (notifyState != roundModify) { - Svc.Prefs.set("notifyTabState", 0); - } - }, - - update: function (record) { - this._log.trace("Ignoring tab updates as local ones win"); - }, -}; - - -function TabTracker(name, engine) { - Tracker.call(this, name, engine); - Svc.Obs.add("weave:engine:start-tracking", this); - Svc.Obs.add("weave:engine:stop-tracking", this); - - // Make sure "this" pointer is always set correctly for event listeners. - this.onTab = Utils.bind2(this, this.onTab); - this._unregisterListeners = Utils.bind2(this, this._unregisterListeners); -} -TabTracker.prototype = { - __proto__: Tracker.prototype, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - - loadChangedIDs: function () { - // Don't read changed IDs from disk at start up. - }, - - clearChangedIDs: function () { - this.modified = false; - }, - - _topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"], - - _registerListenersForWindow: function (window) { - this._log.trace("Registering tab listeners in window"); - for (let topic of this._topics) { - window.addEventListener(topic, this.onTab, false); - } - window.addEventListener("unload", this._unregisterListeners, false); - }, - - _unregisterListeners: function (event) { - this._unregisterListenersForWindow(event.target); - }, - - _unregisterListenersForWindow: function (window) { - this._log.trace("Removing tab listeners in window"); - window.removeEventListener("unload", this._unregisterListeners, false); - for (let topic of this._topics) { - window.removeEventListener(topic, this.onTab, false); - } - }, - - startTracking: function () { - Svc.Obs.add("domwindowopened", this); - let wins = Services.wm.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - this._registerListenersForWindow(wins.getNext()); - } - }, - - stopTracking: function () { - Svc.Obs.remove("domwindowopened", this); - let wins = Services.wm.getEnumerator("navigator:browser"); - while (wins.hasMoreElements()) { - this._unregisterListenersForWindow(wins.getNext()); - } - }, - - observe: function (subject, topic, data) { - Tracker.prototype.observe.call(this, 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. - this._registerListenersForWindow(subject); - }; - - // Add tab listeners now that a window has opened. - subject.addEventListener("load", onLoad, false); - break; - } - }, - - onTab: function (event) { - if (event.originalTarget.linkedBrowser) { - let browser = event.originalTarget.linkedBrowser; - if (PrivateBrowsingUtils.isBrowserPrivate(browser) && - !PrivateBrowsingUtils.permanentPrivateBrowsing) { - this._log.trace("Ignoring tab event from private browsing."); - return; - } - } - - this._log.trace("onTab event: " + event.type); - this.modified = true; - - // For page shows, bump the score 10% of the time, emulating a partial - // score. We don't want to sync too frequently. For all other page - // events, always bump the score. - if (event.type != "pageshow" || Math.random() < .1) { - this.score += SCORE_INCREMENT_SMALL; - } - }, -}; diff --git a/components/weave/src/nsSyncJPAKE.cpp b/components/weave/src/nsSyncJPAKE.cpp deleted file mode 100644 index 23378f56a..000000000 --- a/components/weave/src/nsSyncJPAKE.cpp +++ /dev/null @@ -1,484 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 "nsSyncJPAKE.h" - -#include "base64.h" -#include "keyhi.h" -#include "mozilla/ModuleUtils.h" -#include "mozilla/Move.h" -#include "nsDebug.h" -#include "nsError.h" -#include "nsString.h" -#include "nscore.h" -#include "pk11pub.h" -#include "pkcs11.h" -#include "secerr.h" -#include "secmodt.h" -#include "secport.h" - -using mozilla::fallible; - -static bool -hex_from_2char(const unsigned char *c2, unsigned char *byteval) -{ - int i; - unsigned char offset; - *byteval = 0; - for (i=0; i<2; i++) { - if (c2[i] >= '0' && c2[i] <= '9') { - offset = c2[i] - '0'; - *byteval |= offset << 4*(1-i); - } else if (c2[i] >= 'a' && c2[i] <= 'f') { - offset = c2[i] - 'a'; - *byteval |= (offset + 10) << 4*(1-i); - } else if (c2[i] >= 'A' && c2[i] <= 'F') { - offset = c2[i] - 'A'; - *byteval |= (offset + 10) << 4*(1-i); - } else { - return false; - } - } - return true; -} - -static bool -fromHex(const char * str, unsigned char * p, size_t sLen) -{ - size_t i; - if (sLen & 1) - return false; - - for (i = 0; i < sLen / 2; ++i) { - if (!hex_from_2char((const unsigned char *) str + (2*i), - (unsigned char *) p + i)) { - return false; - } - } - return true; -} - -static nsresult -fromHexString(const nsACString & str, unsigned char * p, size_t pMaxLen) -{ - char * strData = (char *) str.Data(); - unsigned len = str.Length(); - NS_ENSURE_ARG(len / 2 <= pMaxLen); - if (!fromHex(strData, p, len)) { - return NS_ERROR_INVALID_ARG; - } - return NS_OK; -} - -static bool -toHexString(const unsigned char * str, unsigned len, nsACString & out) -{ - static const char digits[] = "0123456789ABCDEF"; - if (!out.SetCapacity(2 * len, fallible)) - return false; - out.SetLength(0); - for (unsigned i = 0; i < len; ++i) { - out.Append(digits[str[i] >> 4]); - out.Append(digits[str[i] & 0x0f]); - } - return true; -} - -static nsresult -mapErrno() -{ - int err = PORT_GetError(); - switch (err) { - case SEC_ERROR_NO_MEMORY: return NS_ERROR_OUT_OF_MEMORY; - default: return NS_ERROR_UNEXPECTED; - } -} - -#define NUM_ELEM(x) (sizeof(x) / sizeof (x)[0]) - -static const char p[] = - "90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C" - "7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F" - "009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA1" - "29F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B" - "6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D159394" - "87E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0" - "E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773E" - "BE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941D" - "AD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504F" - "B0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D" - "597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328E" - "C22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73"; -static const char q[] = - "CFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D"; -static const char g[] = - "5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37" - "F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB" - "805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1" - "F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8" - "A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17" - "A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C" - "4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B3" - "9AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B" - "9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8" - "E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828" - "E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33" - "787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B"; - -NS_IMETHODIMP nsSyncJPAKE::Round1(const nsACString & aSignerID, - nsACString & aGX1, - nsACString & aGV1, - nsACString & aR1, - nsACString & aGX2, - nsACString & aGV2, - nsACString & aR2) -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_NOT_AVAILABLE; - } - - NS_ENSURE_STATE(round == JPAKENotStarted); - NS_ENSURE_STATE(key == nullptr); - - static CK_MECHANISM_TYPE mechanisms[] = { - CKM_NSS_JPAKE_ROUND1_SHA256, - CKM_NSS_JPAKE_ROUND2_SHA256, - CKM_NSS_JPAKE_FINAL_SHA256 - }; - - UniquePK11SlotInfo slot(PK11_GetBestSlotMultiple(mechanisms, - NUM_ELEM(mechanisms), - nullptr)); - NS_ENSURE_STATE(slot != nullptr); - - CK_BYTE pBuf[(NUM_ELEM(p) - 1) / 2]; - CK_BYTE qBuf[(NUM_ELEM(q) - 1) / 2]; - CK_BYTE gBuf[(NUM_ELEM(g) - 1) / 2]; - - CK_KEY_TYPE keyType = CKK_NSS_JPAKE_ROUND1; - NS_ENSURE_STATE(fromHex(p, pBuf, (NUM_ELEM(p) - 1))); - NS_ENSURE_STATE(fromHex(q, qBuf, (NUM_ELEM(q) - 1))); - NS_ENSURE_STATE(fromHex(g, gBuf, (NUM_ELEM(g) - 1))); - CK_ATTRIBUTE keyTemplate[] = { - { CKA_NSS_JPAKE_SIGNERID, (CK_BYTE *) aSignerID.Data(), - aSignerID.Length() }, - { CKA_KEY_TYPE, &keyType, sizeof keyType }, - { CKA_PRIME, pBuf, sizeof pBuf }, - { CKA_SUBPRIME, qBuf, sizeof qBuf }, - { CKA_BASE, gBuf, sizeof gBuf } - }; - - CK_BYTE gx1Buf[NUM_ELEM(p) / 2]; - CK_BYTE gv1Buf[NUM_ELEM(p) / 2]; - CK_BYTE r1Buf [NUM_ELEM(p) / 2]; - CK_BYTE gx2Buf[NUM_ELEM(p) / 2]; - CK_BYTE gv2Buf[NUM_ELEM(p) / 2]; - CK_BYTE r2Buf [NUM_ELEM(p) / 2]; - CK_NSS_JPAKERound1Params rp = { - { gx1Buf, sizeof gx1Buf, gv1Buf, sizeof gv1Buf, r1Buf, sizeof r1Buf }, - { gx2Buf, sizeof gx2Buf, gv2Buf, sizeof gv2Buf, r2Buf, sizeof r2Buf } - }; - SECItem paramsItem; - paramsItem.data = (unsigned char *) &rp; - paramsItem.len = sizeof rp; - key = UniquePK11SymKey( - PK11_KeyGenWithTemplate(slot.get(), CKM_NSS_JPAKE_ROUND1_SHA256, - CKM_NSS_JPAKE_ROUND1_SHA256, ¶msItem, - keyTemplate, NUM_ELEM(keyTemplate), nullptr)); - nsresult rv = key != nullptr - ? NS_OK - : mapErrno(); - if (rv == NS_OK) { - NS_ENSURE_TRUE(toHexString(rp.gx1.pGX, rp.gx1.ulGXLen, aGX1) && - toHexString(rp.gx1.pGV, rp.gx1.ulGVLen, aGV1) && - toHexString(rp.gx1.pR, rp.gx1.ulRLen, aR1) && - toHexString(rp.gx2.pGX, rp.gx2.ulGXLen, aGX2) && - toHexString(rp.gx2.pGV, rp.gx2.ulGVLen, aGV2) && - toHexString(rp.gx2.pR, rp.gx2.ulRLen, aR2), - NS_ERROR_OUT_OF_MEMORY); - round = JPAKEBeforeRound2; - } - return rv; -} - -NS_IMETHODIMP nsSyncJPAKE::Round2(const nsACString & aPeerID, - const nsACString & aPIN, - const nsACString & aGX3, - const nsACString & aGV3, - const nsACString & aR3, - const nsACString & aGX4, - const nsACString & aGV4, - const nsACString & aR4, - nsACString & aA, - nsACString & aGVA, - nsACString & aRA) -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_NOT_AVAILABLE; - } - - NS_ENSURE_STATE(round == JPAKEBeforeRound2); - NS_ENSURE_STATE(key != nullptr); - NS_ENSURE_ARG(!aPeerID.IsEmpty()); - - /* PIN cannot be equal to zero when converted to a bignum. NSS 3.12.9 J-PAKE - assumes that the caller has already done this check. Future versions of - NSS J-PAKE will do this check internally. See Bug 609068 Comment 4 */ - bool foundNonZero = false; - for (size_t i = 0; i < aPIN.Length(); ++i) { - if (aPIN[i] != 0) { - foundNonZero = true; - break; - } - } - NS_ENSURE_ARG(foundNonZero); - - CK_BYTE gx3Buf[NUM_ELEM(p)/2], gv3Buf[NUM_ELEM(p)/2], r3Buf [NUM_ELEM(p)/2]; - CK_BYTE gx4Buf[NUM_ELEM(p)/2], gv4Buf[NUM_ELEM(p)/2], r4Buf [NUM_ELEM(p)/2]; - CK_BYTE gxABuf[NUM_ELEM(p)/2], gvABuf[NUM_ELEM(p)/2], rABuf [NUM_ELEM(p)/2]; - nsresult rv = fromHexString(aGX3, gx3Buf, sizeof gx3Buf); - if (rv == NS_OK) rv = fromHexString(aGV3, gv3Buf, sizeof gv3Buf); - if (rv == NS_OK) rv = fromHexString(aR3, r3Buf, sizeof r3Buf); - if (rv == NS_OK) rv = fromHexString(aGX4, gx4Buf, sizeof gx4Buf); - if (rv == NS_OK) rv = fromHexString(aGV4, gv4Buf, sizeof gv4Buf); - if (rv == NS_OK) rv = fromHexString(aR4, r4Buf, sizeof r4Buf); - if (rv != NS_OK) - return rv; - - CK_NSS_JPAKERound2Params rp; - rp.pSharedKey = (CK_BYTE *) aPIN.Data(); - rp.ulSharedKeyLen = aPIN.Length(); - rp.gx3.pGX = gx3Buf; rp.gx3.ulGXLen = aGX3.Length() / 2; - rp.gx3.pGV = gv3Buf; rp.gx3.ulGVLen = aGV3.Length() / 2; - rp.gx3.pR = r3Buf; rp.gx3.ulRLen = aR3 .Length() / 2; - rp.gx4.pGX = gx4Buf; rp.gx4.ulGXLen = aGX4.Length() / 2; - rp.gx4.pGV = gv4Buf; rp.gx4.ulGVLen = aGV4.Length() / 2; - rp.gx4.pR = r4Buf; rp.gx4.ulRLen = aR4 .Length() / 2; - rp.A.pGX = gxABuf; rp.A .ulGXLen = sizeof gxABuf; - rp.A.pGV = gvABuf; rp.A .ulGVLen = sizeof gxABuf; - rp.A.pR = rABuf; rp.A .ulRLen = sizeof gxABuf; - - // Bug 629090: NSS 3.12.9 J-PAKE fails to check that gx^4 != 1, so check here. - bool gx4Good = false; - for (unsigned i = 0; i < rp.gx4.ulGXLen; ++i) { - if (rp.gx4.pGX[i] > 1 || (rp.gx4.pGX[i] != 0 && i < rp.gx4.ulGXLen - 1)) { - gx4Good = true; - break; - } - } - NS_ENSURE_ARG(gx4Good); - - SECItem paramsItem; - paramsItem.data = (unsigned char *) &rp; - paramsItem.len = sizeof rp; - CK_KEY_TYPE keyType = CKK_NSS_JPAKE_ROUND2; - CK_ATTRIBUTE keyTemplate[] = { - { CKA_NSS_JPAKE_PEERID, (CK_BYTE *) aPeerID.Data(), aPeerID.Length(), }, - { CKA_KEY_TYPE, &keyType, sizeof keyType } - }; - UniquePK11SymKey newKey(PK11_DeriveWithTemplate(key.get(), - CKM_NSS_JPAKE_ROUND2_SHA256, - ¶msItem, - CKM_NSS_JPAKE_FINAL_SHA256, - CKA_DERIVE, 0, - keyTemplate, - NUM_ELEM(keyTemplate), - false)); - if (newKey != nullptr) { - if (toHexString(rp.A.pGX, rp.A.ulGXLen, aA) && - toHexString(rp.A.pGV, rp.A.ulGVLen, aGVA) && - toHexString(rp.A.pR, rp.A.ulRLen, aRA)) { - round = JPAKEAfterRound2; - key = Move(newKey); - return NS_OK; - } else { - rv = NS_ERROR_OUT_OF_MEMORY; - } - } else { - rv = mapErrno(); - } - - return rv; -} - -static nsresult -setBase64(const unsigned char * data, unsigned len, nsACString & out) -{ - nsresult rv = NS_OK; - const char * base64 = BTOA_DataToAscii(data, len); - - if (base64 != nullptr) { - size_t len = PORT_Strlen(base64); - if (out.SetCapacity(len, fallible)) { - out.SetLength(0); - out.Append(base64, len); - } else { - rv = NS_ERROR_OUT_OF_MEMORY; - } - PORT_Free((void*) base64); - } else { - rv = NS_ERROR_OUT_OF_MEMORY; - } - return rv; -} - -static nsresult -base64KeyValue(PK11SymKey * key, nsACString & keyString) -{ - nsresult rv = NS_OK; - if (PK11_ExtractKeyValue(key) == SECSuccess) { - const SECItem * value = PK11_GetKeyData(key); - rv = value != nullptr && value->data != nullptr && value->len > 0 - ? setBase64(value->data, value->len, keyString) - : NS_ERROR_UNEXPECTED; - } else { - rv = mapErrno(); - } - return rv; -} - -static nsresult -extractBase64KeyValue(UniquePK11SymKey & keyBlock, CK_ULONG bitPosition, - CK_MECHANISM_TYPE destMech, int keySize, - nsACString & keyString) -{ - SECItem paramsItem; - paramsItem.data = (CK_BYTE *) &bitPosition; - paramsItem.len = sizeof bitPosition; - PK11SymKey * key = PK11_Derive(keyBlock.get(), CKM_EXTRACT_KEY_FROM_KEY, - ¶msItem, destMech, - CKA_SIGN, keySize); - if (key == nullptr) - return mapErrno(); - nsresult rv = base64KeyValue(key, keyString); - PK11_FreeSymKey(key); - return rv; -} - - -NS_IMETHODIMP nsSyncJPAKE::Final(const nsACString & aB, - const nsACString & aGVB, - const nsACString & aRB, - const nsACString & aHKDFInfo, - nsACString & aAES256Key, - nsACString & aHMAC256Key) -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return NS_ERROR_NOT_AVAILABLE; - } - - static const unsigned AES256_KEY_SIZE = 256 / 8; - static const unsigned HMAC_SHA256_KEY_SIZE = 256 / 8; - CK_EXTRACT_PARAMS aesBitPosition = 0; - CK_EXTRACT_PARAMS hmacBitPosition = aesBitPosition + (AES256_KEY_SIZE * 8); - - NS_ENSURE_STATE(round == JPAKEAfterRound2); - NS_ENSURE_STATE(key != nullptr); - - CK_BYTE gxBBuf[NUM_ELEM(p)/2], gvBBuf[NUM_ELEM(p)/2], rBBuf [NUM_ELEM(p)/2]; - nsresult rv = fromHexString(aB, gxBBuf, sizeof gxBBuf); - if (rv == NS_OK) rv = fromHexString(aGVB, gvBBuf, sizeof gvBBuf); - if (rv == NS_OK) rv = fromHexString(aRB, rBBuf, sizeof rBBuf); - if (rv != NS_OK) - return rv; - - CK_NSS_JPAKEFinalParams rp; - rp.B.pGX = gxBBuf; rp.B.ulGXLen = aB .Length() / 2; - rp.B.pGV = gvBBuf; rp.B.ulGVLen = aGVB.Length() / 2; - rp.B.pR = rBBuf; rp.B.ulRLen = aRB .Length() / 2; - SECItem paramsItem; - paramsItem.data = (unsigned char *) &rp; - paramsItem.len = sizeof rp; - UniquePK11SymKey keyMaterial(PK11_Derive(key.get(), CKM_NSS_JPAKE_FINAL_SHA256, - ¶msItem, CKM_NSS_HKDF_SHA256, - CKA_DERIVE, 0)); - UniquePK11SymKey keyBlock; - - if (keyMaterial == nullptr) - rv = mapErrno(); - - if (rv == NS_OK) { - CK_NSS_HKDFParams hkdfParams; - hkdfParams.bExtract = CK_TRUE; - hkdfParams.pSalt = nullptr; - hkdfParams.ulSaltLen = 0; - hkdfParams.bExpand = CK_TRUE; - hkdfParams.pInfo = (CK_BYTE *) aHKDFInfo.Data(); - hkdfParams.ulInfoLen = aHKDFInfo.Length(); - paramsItem.data = (unsigned char *) &hkdfParams; - paramsItem.len = sizeof hkdfParams; - keyBlock = UniquePK11SymKey( - PK11_Derive(keyMaterial.get(), CKM_NSS_HKDF_SHA256, ¶msItem, - CKM_EXTRACT_KEY_FROM_KEY, CKA_DERIVE, - AES256_KEY_SIZE + HMAC_SHA256_KEY_SIZE)); - if (keyBlock == nullptr) - rv = mapErrno(); - } - - if (rv == NS_OK) { - rv = extractBase64KeyValue(keyBlock, aesBitPosition, CKM_AES_CBC, - AES256_KEY_SIZE, aAES256Key); - } - if (rv == NS_OK) { - rv = extractBase64KeyValue(keyBlock, hmacBitPosition, CKM_SHA256_HMAC, - HMAC_SHA256_KEY_SIZE, aHMAC256Key); - } - - if (rv == NS_OK) { - SECStatus srv = PK11_ExtractKeyValue(keyMaterial.get()); - NS_ENSURE_TRUE(srv == SECSuccess, NS_ERROR_UNEXPECTED); - SECItem * keyMaterialBytes = PK11_GetKeyData(keyMaterial.get()); - NS_ENSURE_TRUE(keyMaterialBytes != nullptr, NS_ERROR_UNEXPECTED); - } - - return rv; -} - -NS_GENERIC_FACTORY_CONSTRUCTOR(nsSyncJPAKE) -NS_DEFINE_NAMED_CID(NS_SYNCJPAKE_CID); - -nsSyncJPAKE::nsSyncJPAKE() : round(JPAKENotStarted), key(nullptr) { } - -nsSyncJPAKE::~nsSyncJPAKE() -{ - nsNSSShutDownPreventionLock locker; - if (isAlreadyShutDown()) { - return; - } - destructorSafeDestroyNSSReference(); - shutdown(ShutdownCalledFrom::Object); -} - -void -nsSyncJPAKE::virtualDestroyNSSReference() -{ - destructorSafeDestroyNSSReference(); -} - -void -nsSyncJPAKE::destructorSafeDestroyNSSReference() -{ - key = nullptr; -} - -static const mozilla::Module::CIDEntry kServicesCryptoCIDs[] = { - { &kNS_SYNCJPAKE_CID, false, nullptr, nsSyncJPAKEConstructor }, - { nullptr } -}; - -static const mozilla::Module::ContractIDEntry kServicesCryptoContracts[] = { - { NS_SYNCJPAKE_CONTRACTID, &kNS_SYNCJPAKE_CID }, - { nullptr } -}; - -static const mozilla::Module kServicesCryptoModule = { - mozilla::Module::kVersion, - kServicesCryptoCIDs, - kServicesCryptoContracts -}; - -NSMODULE_DEFN(nsServicesCryptoModule) = &kServicesCryptoModule; diff --git a/components/weave/src/nsSyncJPAKE.h b/components/weave/src/nsSyncJPAKE.h deleted file mode 100644 index 0c737d997..000000000 --- a/components/weave/src/nsSyncJPAKE.h +++ /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/. */ -#ifndef nsSyncJPAKE_h__ -#define nsSyncJPAKE_h__ - -#include "ScopedNSSTypes.h" -#include "nsISyncJPAKE.h" -#include "nsNSSShutDown.h" - -#define NS_SYNCJPAKE_CONTRACTID \ - "@mozilla.org/services-crypto/sync-jpake;1" - -#define NS_SYNCJPAKE_CID \ - {0x0b9721c0, 0x1805, 0x47c3, {0x86, 0xce, 0x68, 0x13, 0x79, 0x5a, 0x78, 0x3f}} - -using namespace mozilla; - -class nsSyncJPAKE : public nsISyncJPAKE - , public nsNSSShutDownObject -{ -public: - NS_DECL_ISUPPORTS - NS_DECL_NSISYNCJPAKE - nsSyncJPAKE(); -protected: - virtual ~nsSyncJPAKE(); -private: - virtual void virtualDestroyNSSReference() override; - void destructorSafeDestroyNSSReference(); - - enum { JPAKENotStarted, JPAKEBeforeRound2, JPAKEAfterRound2 } round; - UniquePK11SymKey key; -}; - -NS_IMPL_ISUPPORTS(nsSyncJPAKE, nsISyncJPAKE) - -#endif // nsSyncJPAKE_h__ diff --git a/components/weave/src/stages/cluster.js b/components/weave/src/stages/cluster.js deleted file mode 100644 index 41afe61d8..000000000 --- a/components/weave/src/stages/cluster.js +++ /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/. */ - -this.EXPORTED_SYMBOLS = ["ClusterManager"]; - -var {utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/policies.js"); -Cu.import("resource://services-sync/util.js"); - -/** - * Contains code for managing the Sync cluster we are in. - */ -this.ClusterManager = function ClusterManager(service) { - this._log = Log.repository.getLogger("Sync.Service"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; - - this.service = service; -} -ClusterManager.prototype = { - get identity() { - return this.service.identity; - }, - - /** - * Obtain the cluster for the current user. - * - * Returns the string URL of the cluster or null on error. - */ - _findCluster: function _findCluster() { - this._log.debug("Finding cluster for user " + this.identity.username); - - // This should ideally use UserAPI10Client but the legacy hackiness is - // strong with this code. - let fail; - let url = this.service.userAPIURI + this.identity.username + "/node/weave"; - let res = this.service.resource(url); - try { - let node = res.get(); - switch (node.status) { - case 400: - this.service.status.login = LOGIN_FAILED_LOGIN_REJECTED; - fail = "Find cluster denied: " + this.service.errorHandler.errorStr(node); - break; - case 404: - this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); - return this.service.serverURL; - case 0: - case 200: - if (node == "null") { - node = null; - } - this._log.trace("_findCluster successfully returning " + node); - return node; - default: - this.service.errorHandler.checkServerError(node); - fail = "Unexpected response code: " + node.status; - break; - } - } catch (e) { - this._log.debug("Network error on findCluster"); - this.service.status.login = LOGIN_FAILED_NETWORK_ERROR; - this.service.errorHandler.checkServerError(e); - fail = e; - } - throw fail; - }, - - /** - * Determine the cluster for the current user and update state. - */ - setCluster: function setCluster() { - // Make sure we didn't get some unexpected response for the cluster. - let cluster = this._findCluster(); - this._log.debug("Cluster value = " + cluster); - if (cluster == null) { - return false; - } - - // Don't update stuff if we already have the right cluster - if (cluster == this.service.clusterURL) { - return false; - } - - this._log.debug("Setting cluster to " + cluster); - this.service.clusterURL = cluster; - Svc.Prefs.set("lastClusterUpdate", Date.now().toString()); - - return true; - }, - - getUserBaseURL: function getUserBaseURL() { - // Legacy Sync and FxA Sync construct the userBaseURL differently. Legacy - // Sync appends path components onto an empty path, and in FxA Sync, the - // token server constructs this for us in an opaque manner. Since the - // cluster manager already sets the clusterURL on Service and also has - // access to the current identity, we added this functionality here. - - // If the clusterURL hasn't been set, the userBaseURL shouldn't be set - // either. Some tests expect "undefined" to be returned here. - if (!this.service.clusterURL) { - return undefined; - } - let storageAPI = this.service.clusterURL + SYNC_API_VERSION + "/"; - return storageAPI + this.identity.username + "/"; - } -}; -Object.freeze(ClusterManager.prototype); diff --git a/components/weave/src/stages/declined.js b/components/weave/src/stages/declined.js deleted file mode 100644 index e74a45c6e..000000000 --- a/components/weave/src/stages/declined.js +++ /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/. */ - -/** - * This file contains code for maintaining the set of declined engines, - * in conjunction with EngineManager. - */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["DeclinedEngines"]; - -var {utils: Cu} = Components; - -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/modules/Preferences.jsm"); - - - -this.DeclinedEngines = function (service) { - this._log = Log.repository.getLogger("Sync.Declined"); - this._log.level = Log.Level[new Preferences(PREFS_BRANCH).get("log.logger.declined")]; - - this.service = service; -} -this.DeclinedEngines.prototype = { - updateDeclined: function (meta, engineManager=this.service.engineManager) { - let enabled = new Set(engineManager.getEnabled().map(e => e.name)); - let known = new Set(engineManager.getAll().map(e => e.name)); - let remoteDeclined = new Set(meta.payload.declined || []); - let localDeclined = new Set(engineManager.getDeclined()); - - this._log.debug("Handling remote declined: " + JSON.stringify([...remoteDeclined])); - this._log.debug("Handling local declined: " + JSON.stringify([...localDeclined])); - - // Any engines that are locally enabled should be removed from the remote - // declined list. - // - // Any engines that are locally declined should be added to the remote - // declined list. - let newDeclined = CommonUtils.union(localDeclined, CommonUtils.difference(remoteDeclined, enabled)); - - // If our declined set has changed, put it into the meta object and mark - // it as changed. - let declinedChanged = !CommonUtils.setEqual(newDeclined, remoteDeclined); - this._log.debug("Declined changed? " + declinedChanged); - if (declinedChanged) { - meta.changed = true; - meta.payload.declined = [...newDeclined]; - } - - // Update the engine manager regardless. - engineManager.setDeclined(newDeclined); - - // Any engines that are locally known, locally disabled, and not remotely - // or locally declined, are candidates for enablement. - let undecided = CommonUtils.difference(CommonUtils.difference(known, enabled), newDeclined); - if (undecided.size) { - let subject = { - declined: newDeclined, - enabled: enabled, - known: known, - undecided: undecided, - }; - CommonUtils.nextTick(() => { - Observers.notify("weave:engines:notdeclined", subject); - }); - } - - return declinedChanged; - }, -}; diff --git a/components/weave/src/stages/enginesync.js b/components/weave/src/stages/enginesync.js deleted file mode 100644 index 61f2005d8..000000000 --- a/components/weave/src/stages/enginesync.js +++ /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/. */ - -/** - * This file contains code for synchronizing engines. - */ - -this.EXPORTED_SYMBOLS = ["EngineSynchronizer"]; - -var {utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/policies.js"); -Cu.import("resource://services-sync/util.js"); - -/** - * Perform synchronization of engines. - * - * This was originally split out of service.js. The API needs lots of love. - */ -this.EngineSynchronizer = function EngineSynchronizer(service) { - this._log = Log.repository.getLogger("Sync.Synchronizer"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.synchronizer")]; - - this.service = service; - - this.onComplete = null; -} - -EngineSynchronizer.prototype = { - sync: function sync() { - if (!this.onComplete) { - throw new Error("onComplete handler not installed."); - } - - let startTime = Date.now(); - - this.service.status.resetSync(); - - // Make sure we should sync or record why we shouldn't. - let reason = this.service._checkSync(); - if (reason) { - if (reason == kSyncNetworkOffline) { - this.service.status.sync = LOGIN_FAILED_NETWORK_ERROR; - } - - // this is a purposeful abort rather than a failure, so don't set - // any status bits - reason = "Can't sync: " + reason; - this.onComplete(new Error("Can't sync: " + reason)); - return; - } - - // If we don't have a node, get one. If that fails, retry in 10 minutes. - if (!this.service.clusterURL && !this.service._clusterManager.setCluster()) { - this.service.status.sync = NO_SYNC_NODE_FOUND; - this._log.info("No cluster URL found. Cannot sync."); - this.onComplete(null); - return; - } - - // Ping the server with a special info request once a day. - let infoURL = this.service.infoURL; - let now = Math.floor(Date.now() / 1000); - let lastPing = Svc.Prefs.get("lastPing", 0); - if (now - lastPing > 86400) { // 60 * 60 * 24 - infoURL += "?v=" + WEAVE_VERSION; - Svc.Prefs.set("lastPing", now); - } - - let engineManager = this.service.engineManager; - - // Figure out what the last modified time is for each collection - let info = this.service._fetchInfo(infoURL); - - // Convert the response to an object and read out the modified times - for (let engine of [this.service.clientsEngine].concat(engineManager.getAll())) { - engine.lastModified = info.obj[engine.name] || 0; - } - - if (!(this.service._remoteSetup(info))) { - this.onComplete(new Error("Aborting sync, remote setup failed")); - return; - } - - // Make sure we have an up-to-date list of clients before sending commands - this._log.debug("Refreshing client list."); - if (!this._syncEngine(this.service.clientsEngine)) { - // Clients is an engine like any other; it can fail with a 401, - // and we can elect to abort the sync. - this._log.warn("Client engine sync failed. Aborting."); - this.onComplete(null); - return; - } - - // Wipe data in the desired direction if necessary - switch (Svc.Prefs.get("firstSync")) { - case "resetClient": - this.service.resetClient(engineManager.enabledEngineNames); - break; - case "wipeClient": - this.service.wipeClient(engineManager.enabledEngineNames); - break; - case "wipeRemote": - this.service.wipeRemote(engineManager.enabledEngineNames); - break; - } - - if (this.service.clientsEngine.localCommands) { - try { - if (!(this.service.clientsEngine.processIncomingCommands())) { - this.service.status.sync = ABORT_SYNC_COMMAND; - this.onComplete(new Error("Processed command aborted sync.")); - return; - } - - // Repeat remoteSetup in-case the commands forced us to reset - if (!(this.service._remoteSetup(info))) { - this.onComplete(new Error("Remote setup failed after processing commands.")); - return; - } - } - finally { - // Always immediately attempt to push back the local client (now - // without commands). - // Note that we don't abort here; if there's a 401 because we've - // been reassigned, we'll handle it around another engine. - this._syncEngine(this.service.clientsEngine); - } - } - - // Update engines because it might change what we sync. - try { - this._updateEnabledEngines(); - } catch (ex) { - this._log.debug("Updating enabled engines failed: ", ex); - this.service.errorHandler.checkServerError(ex); - this.onComplete(ex); - return; - } - - try { - for (let engine of engineManager.getEnabled()) { - // If there's any problems with syncing the engine, report the failure - if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) { - this._log.info("Aborting sync for failure in " + engine.name); - break; - } - } - - // If _syncEngine fails for a 401, we might not have a cluster URL here. - // If that's the case, break out of this immediately, rather than - // throwing an exception when trying to fetch metaURL. - if (!this.service.clusterURL) { - this._log.debug("Aborting sync, no cluster URL: " + - "not uploading new meta/global."); - this.onComplete(null); - return; - } - - // Upload meta/global if any engines changed anything. - let meta = this.service.recordManager.get(this.service.metaURL); - if (meta.isNew || meta.changed) { - this._log.info("meta/global changed locally: reuploading."); - try { - this.service.uploadMetaGlobal(meta); - delete meta.isNew; - delete meta.changed; - } catch (error) { - this._log.error("Unable to upload meta/global. Leaving marked as new."); - } - } - - // If there were no sync engine failures - if (this.service.status.service != SYNC_FAILED_PARTIAL) { - Svc.Prefs.set("lastSync", new Date().toString()); - this.service.status.sync = SYNC_SUCCEEDED; - } - } finally { - Svc.Prefs.reset("firstSync"); - - let syncTime = ((Date.now() - startTime) / 1000).toFixed(2); - let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT); - this._log.info("Sync completed at " + dateStr - + " after " + syncTime + " secs."); - } - - this.onComplete(null); - }, - - // Returns true if sync should proceed. - // false / no return value means sync should be aborted. - _syncEngine: function _syncEngine(engine) { - try { - engine.sync(); - } - catch(e) { - if (e.status == 401) { - // Maybe a 401, cluster update perhaps needed? - // We rely on ErrorHandler observing the sync failure notification to - // schedule another sync and clear node assignment values. - // Here we simply want to muffle the exception and return an - // appropriate value. - return false; - } - } - - return true; - }, - - _updateEnabledFromMeta: function (meta, numClients, engineManager=this.service.engineManager) { - this._log.info("Updating enabled engines: " + - numClients + " clients."); - - if (meta.isNew || !meta.payload.engines) { - this._log.debug("meta/global isn't new, or is missing engines. Not updating enabled state."); - return; - } - - // If we're the only client, and no engines are marked as enabled, - // thumb our noses at the server data: it can't be right. - // Belt-and-suspenders approach to Bug 615926. - let hasEnabledEngines = false; - for (let e in meta.payload.engines) { - if (e != "clients") { - hasEnabledEngines = true; - break; - } - } - - if ((numClients <= 1) && !hasEnabledEngines) { - this._log.info("One client and no enabled engines: not touching local engine status."); - return; - } - - this.service._ignorePrefObserver = true; - - let enabled = engineManager.enabledEngineNames; - - let toDecline = new Set(); - let toUndecline = new Set(); - - for (let engineName in meta.payload.engines) { - if (engineName == "clients") { - // Clients is special. - continue; - } - let index = enabled.indexOf(engineName); - if (index != -1) { - // The engine is enabled locally. Nothing to do. - enabled.splice(index, 1); - continue; - } - let engine = engineManager.get(engineName); - if (!engine) { - // The engine doesn't exist locally. Nothing to do. - continue; - } - - let attemptedEnable = false; - // If the engine was enabled remotely, enable it locally. - if (!Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) { - this._log.trace("Engine " + engineName + " was enabled. Marking as non-declined."); - toUndecline.add(engineName); - this._log.trace(engineName + " engine was enabled remotely."); - engine.enabled = true; - // Note that setting engine.enabled to true might not have worked for - // the password engine if a master-password is enabled. However, it's - // still OK that we added it to undeclined - the user *tried* to enable - // it remotely - so it still winds up as not being flagged as declined - // even though it's disabled remotely. - attemptedEnable = true; - } - - // If either the engine was disabled locally or enabling the engine - // failed (see above re master-password) then wipe server data and - // disable it everywhere. - if (!engine.enabled) { - this._log.trace("Wiping data for " + engineName + " engine."); - engine.wipeServer(); - delete meta.payload.engines[engineName]; - meta.changed = true; // the new enabled state must propagate - // We also here mark the engine as declined, because the pref - // was explicitly changed to false - unless we tried, and failed, - // to enable it - in which case we leave the declined state alone. - if (!attemptedEnable) { - // This will be reflected in meta/global in the next stage. - this._log.trace("Engine " + engineName + " was disabled locally. Marking as declined."); - toDecline.add(engineName); - } - } - } - - // Any remaining engines were either enabled locally or disabled remotely. - for (let engineName of enabled) { - let engine = engineManager.get(engineName); - if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) { - this._log.trace("The " + engineName + " engine was enabled locally."); - toUndecline.add(engineName); - } else { - this._log.trace("The " + engineName + " engine was disabled remotely."); - - // Don't automatically mark it as declined! - engine.enabled = false; - } - } - - engineManager.decline(toDecline); - engineManager.undecline(toUndecline); - - Svc.Prefs.resetBranch("engineStatusChanged."); - this.service._ignorePrefObserver = false; - }, - - _updateEnabledEngines: function () { - let meta = this.service.recordManager.get(this.service.metaURL); - let numClients = this.service.scheduler.numClients; - let engineManager = this.service.engineManager; - - this._updateEnabledFromMeta(meta, numClients, engineManager); - }, -}; -Object.freeze(EngineSynchronizer.prototype); diff --git a/components/weave/src/sync/addonsreconciler.js b/components/weave/src/sync/addonsreconciler.js deleted file mode 100644 index ec0896bb2..000000000 --- a/components/weave/src/sync/addonsreconciler.js +++ /dev/null @@ -1,674 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 file contains middleware to reconcile state of AddonManager for - * purposes of tracking events for Sync. The content in this file exists - * because AddonManager does not have a getChangesSinceX() API and adding - * that functionality properly was deemed too time-consuming at the time - * add-on sync was originally written. If/when AddonManager adds this API, - * this file can go away and the add-ons engine can be rewritten to use it. - * - * It was decided to have this tracking functionality exist in a separate - * standalone file so it could be more easily understood, tested, and - * hopefully ported. - */ - -"use strict"; - -var Cu = Components.utils; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://gre/modules/AddonManager.jsm"); - -const DEFAULT_STATE_FILE = "addonsreconciler"; - -this.CHANGE_INSTALLED = 1; -this.CHANGE_UNINSTALLED = 2; -this.CHANGE_ENABLED = 3; -this.CHANGE_DISABLED = 4; - -this.EXPORTED_SYMBOLS = ["AddonsReconciler", "CHANGE_INSTALLED", - "CHANGE_UNINSTALLED", "CHANGE_ENABLED", - "CHANGE_DISABLED"]; -/** - * Maintains state of add-ons. - * - * State is maintained in 2 data structures, an object mapping add-on IDs - * to metadata and an array of changes over time. The object mapping can be - * thought of as a minimal copy of data from AddonManager which is needed for - * Sync. The array is effectively a log of changes over time. - * - * The data structures are persisted to disk by serializing to a JSON file in - * the current profile. The data structures are updated by 2 mechanisms. First, - * they can be refreshed from the global state of the AddonManager. This is a - * sure-fire way of ensuring the reconciler is up to date. Second, the - * reconciler adds itself as an AddonManager listener. When it receives change - * notifications, it updates its internal state incrementally. - * - * The internal state is persisted to a JSON file in the profile directory. - * - * An instance of this is bound to an AddonsEngine instance. In reality, it - * likely exists as a singleton. To AddonsEngine, it functions as a store and - * an entity which emits events for tracking. - * - * The usage pattern for instances of this class is: - * - * let reconciler = new AddonsReconciler(); - * reconciler.loadState(null, function(error) { ... }); - * - * // At this point, your instance should be ready to use. - * - * When you are finished with the instance, please call: - * - * reconciler.stopListening(); - * reconciler.saveState(...); - * - * There are 2 classes of listeners in the AddonManager: AddonListener and - * InstallListener. This class is a listener for both (member functions just - * get called directly). - * - * When an add-on is installed, listeners are called in the following order: - * - * IL.onInstallStarted, AL.onInstalling, IL.onInstallEnded, AL.onInstalled - * - * For non-restartless add-ons, an application restart may occur between - * IL.onInstallEnded and AL.onInstalled. Unfortunately, Sync likely will - * not be loaded when AL.onInstalled is fired shortly after application - * start, so it won't see this event. Therefore, for add-ons requiring a - * restart, Sync treats the IL.onInstallEnded event as good enough to - * indicate an install. For restartless add-ons, Sync assumes AL.onInstalled - * will follow shortly after IL.onInstallEnded and thus it ignores - * IL.onInstallEnded. - * - * The listeners can also see events related to the download of the add-on. - * This class isn't interested in those. However, there are failure events, - * IL.onDownloadFailed and IL.onDownloadCanceled which get called if a - * download doesn't complete successfully. - * - * For uninstalls, we see AL.onUninstalling then AL.onUninstalled. Like - * installs, the events could be separated by an application restart and Sync - * may not see the onUninstalled event. Again, if we require a restart, we - * react to onUninstalling. If not, we assume we'll get onUninstalled. - * - * Enabling and disabling work by sending: - * - * AL.onEnabling, AL.onEnabled - * AL.onDisabling, AL.onDisabled - * - * Again, they may be separated by a restart, so we heed the requiresRestart - * flag. - * - * Actions can be undone. All undoable actions notify the same - * AL.onOperationCancelled event. We treat this event like any other. - * - * Restartless add-ons have interesting behavior during uninstall. These - * add-ons are first disabled then they are actually uninstalled. So, we will - * see AL.onDisabling and AL.onDisabled. The onUninstalling and onUninstalled - * events only come after the Addon Manager is closed or another view is - * switched to. In the case of Sync performing the uninstall, the uninstall - * events will occur immediately. However, we still see disabling events and - * heed them like they were normal. In the end, the state is proper. - */ -this.AddonsReconciler = function AddonsReconciler() { - this._log = Log.repository.getLogger("Sync.AddonsReconciler"); - let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug"); - this._log.level = Log.Level[level]; - - Svc.Obs.add("xpcom-shutdown", this.stopListening, this); -}; -AddonsReconciler.prototype = { - /** Flag indicating whether we are listening to AddonManager events. */ - _listening: false, - - /** - * Whether state has been loaded from a file. - * - * State is loaded on demand if an operation requires it. - */ - _stateLoaded: false, - - /** - * Define this as false if the reconciler should not persist state - * to disk when handling events. - * - * This allows test code to avoid spinning to write during observer - * notifications and xpcom shutdown, which appears to cause hangs on WinXP - * (Bug 873861). - */ - _shouldPersist: true, - - /** Log logger instance */ - _log: null, - - /** - * Container for add-on metadata. - * - * Keys are add-on IDs. Values are objects which describe the state of the - * add-on. This is a minimal mirror of data that can be queried from - * AddonManager. In some cases, we retain data longer than AddonManager. - */ - _addons: {}, - - /** - * List of add-on changes over time. - * - * Each element is an array of [time, change, id]. - */ - _changes: [], - - /** - * Objects subscribed to changes made to this instance. - */ - _listeners: [], - - /** - * Accessor for add-ons in this object. - * - * Returns an object mapping add-on IDs to objects containing metadata. - */ - get addons() { - this._ensureStateLoaded(); - return this._addons; - }, - - /** - * Load reconciler state from a file. - * - * The path is relative to the weave directory in the profile. If no - * path is given, the default one is used. - * - * If the file does not exist or there was an error parsing the file, the - * state will be transparently defined as empty. - * - * @param path - * Path to load. ".json" is appended automatically. If not defined, - * a default path will be consulted. - * @param callback - * Callback to be executed upon file load. The callback receives a - * truthy error argument signifying whether an error occurred and a - * boolean indicating whether data was loaded. - */ - loadState: function loadState(path, callback) { - let file = path || DEFAULT_STATE_FILE; - Utils.jsonLoad(file, this, function(json) { - this._addons = {}; - this._changes = []; - - if (!json) { - this._log.debug("No data seen in loaded file: " + file); - if (callback) { - callback(null, false); - } - - return; - } - - let version = json.version; - if (!version || version != 1) { - this._log.error("Could not load JSON file because version not " + - "supported: " + version); - if (callback) { - callback(null, false); - } - - return; - } - - this._addons = json.addons; - for (let id in this._addons) { - let record = this._addons[id]; - record.modified = new Date(record.modified); - } - - for (let [time, change, id] of json.changes) { - this._changes.push([new Date(time), change, id]); - } - - if (callback) { - callback(null, true); - } - }); - }, - - /** - * Saves the current state to a file in the local profile. - * - * @param path - * String path in profile to save to. If not defined, the default - * will be used. - * @param callback - * Function to be invoked on save completion. No parameters will be - * passed to callback. - */ - saveState: function saveState(path, callback) { - let file = path || DEFAULT_STATE_FILE; - let state = {version: 1, addons: {}, changes: []}; - - for (let [id, record] of Object.entries(this._addons)) { - state.addons[id] = {}; - for (let [k, v] of Object.entries(record)) { - if (k == "modified") { - state.addons[id][k] = v.getTime(); - } - else { - state.addons[id][k] = v; - } - } - } - - for (let [time, change, id] of this._changes) { - state.changes.push([time.getTime(), change, id]); - } - - this._log.info("Saving reconciler state to file: " + file); - Utils.jsonSave(file, this, state, callback); - }, - - /** - * Registers a change listener with this instance. - * - * Change listeners are called every time a change is recorded. The listener - * is an object with the function "changeListener" that takes 3 arguments, - * the Date at which the change happened, the type of change (a CHANGE_* - * constant), and the add-on state object reflecting the current state of - * the add-on at the time of the change. - * - * @param listener - * Object containing changeListener function. - */ - addChangeListener: function addChangeListener(listener) { - if (this._listeners.indexOf(listener) == -1) { - this._log.debug("Adding change listener."); - this._listeners.push(listener); - } - }, - - /** - * Removes a previously-installed change listener from the instance. - * - * @param listener - * Listener instance to remove. - */ - removeChangeListener: function removeChangeListener(listener) { - this._listeners = this._listeners.filter(function(element) { - if (element == listener) { - this._log.debug("Removing change listener."); - return false; - } else { - return true; - } - }.bind(this)); - }, - - /** - * Tells the instance to start listening for AddonManager changes. - * - * This is typically called automatically when Sync is loaded. - */ - startListening: function startListening() { - if (this._listening) { - return; - } - - this._log.info("Registering as Add-on Manager listener."); - AddonManager.addAddonListener(this); - AddonManager.addInstallListener(this); - this._listening = true; - }, - - /** - * Tells the instance to stop listening for AddonManager changes. - * - * The reconciler should always be listening. This should only be called when - * the instance is being destroyed. - * - * This function will get called automatically on XPCOM shutdown. However, it - * is a best practice to call it yourself. - */ - stopListening: function stopListening() { - if (!this._listening) { - return; - } - - this._log.debug("Stopping listening and removing AddonManager listeners."); - AddonManager.removeInstallListener(this); - AddonManager.removeAddonListener(this); - this._listening = false; - }, - - /** - * Refreshes the global state of add-ons by querying the AddonManager. - */ - refreshGlobalState: function refreshGlobalState(callback) { - this._log.info("Refreshing global state from AddonManager."); - this._ensureStateLoaded(); - - let installs; - - AddonManager.getAllAddons(function (addons) { - let ids = {}; - - for (let addon of addons) { - ids[addon.id] = true; - this.rectifyStateFromAddon(addon); - } - - // Look for locally-defined add-ons that no longer exist and update their - // record. - for (let [id, addon] of Object.entries(this._addons)) { - if (id in ids) { - continue; - } - - // If the id isn't in ids, it means that the add-on has been deleted or - // the add-on is in the process of being installed. We detect the - // latter by seeing if an AddonInstall is found for this add-on. - - if (!installs) { - let cb = Async.makeSyncCallback(); - AddonManager.getAllInstalls(cb); - installs = Async.waitForSyncCallback(cb); - } - - let installFound = false; - for (let install of installs) { - if (install.addon && install.addon.id == id && - install.state == AddonManager.STATE_INSTALLED) { - - installFound = true; - break; - } - } - - if (installFound) { - continue; - } - - if (addon.installed) { - addon.installed = false; - this._log.debug("Adding change because add-on not present in " + - "Add-on Manager: " + id); - this._addChange(new Date(), CHANGE_UNINSTALLED, addon); - } - } - - // See note for _shouldPersist. - if (this._shouldPersist) { - this.saveState(null, callback); - } else { - callback(); - } - }.bind(this)); - }, - - /** - * Rectifies the state of an add-on from an Addon instance. - * - * This basically says "given an Addon instance, assume it is truth and - * apply changes to the local state to reflect it." - * - * This function could result in change listeners being called if the local - * state differs from the passed add-on's state. - * - * @param addon - * Addon instance being updated. - */ - rectifyStateFromAddon: function rectifyStateFromAddon(addon) { - this._log.debug(`Rectifying state for addon ${addon.name} (version=${addon.version}, id=${addon.id})`); - this._ensureStateLoaded(); - - let id = addon.id; - let enabled = !addon.userDisabled; - let guid = addon.syncGUID; - let now = new Date(); - - if (!(id in this._addons)) { - let record = { - id: id, - guid: guid, - enabled: enabled, - installed: true, - modified: now, - type: addon.type, - scope: addon.scope, - foreignInstall: addon.foreignInstall - }; - this._addons[id] = record; - this._log.debug("Adding change because add-on not present locally: " + - id); - this._addChange(now, CHANGE_INSTALLED, record); - return; - } - - let record = this._addons[id]; - - if (!record.installed) { - // It is possible the record is marked as uninstalled because an - // uninstall is pending. - if (!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL)) { - record.installed = true; - record.modified = now; - } - } - - if (record.enabled != enabled) { - record.enabled = enabled; - record.modified = now; - let change = enabled ? CHANGE_ENABLED : CHANGE_DISABLED; - this._log.debug("Adding change because enabled state changed: " + id); - this._addChange(new Date(), change, record); - } - - if (record.guid != guid) { - record.guid = guid; - // We don't record a change because the Sync engine rectifies this on its - // own. This is tightly coupled with Sync. If this code is ever lifted - // outside of Sync, this exception should likely be removed. - } - }, - - /** - * Record a change in add-on state. - * - * @param date - * Date at which the change occurred. - * @param change - * The type of the change. A CHANGE_* constant. - * @param state - * The new state of the add-on. From this.addons. - */ - _addChange: function _addChange(date, change, state) { - this._log.info("Change recorded for " + state.id); - this._changes.push([date, change, state.id]); - - for (let listener of this._listeners) { - try { - listener.changeListener.call(listener, date, change, state); - } catch (ex) { - this._log.warn("Exception calling change listener: ", ex); - } - } - }, - - /** - * Obtain the set of changes to add-ons since the date passed. - * - * This will return an array of arrays. Each entry in the array has the - * elements [date, change_type, id], where - * - * date - Date instance representing when the change occurred. - * change_type - One of CHANGE_* constants. - * id - ID of add-on that changed. - */ - getChangesSinceDate: function getChangesSinceDate(date) { - this._ensureStateLoaded(); - - let length = this._changes.length; - for (let i = 0; i < length; i++) { - if (this._changes[i][0] >= date) { - return this._changes.slice(i); - } - } - - return []; - }, - - /** - * Prunes all recorded changes from before the specified Date. - * - * @param date - * Entries older than this Date will be removed. - */ - pruneChangesBeforeDate: function pruneChangesBeforeDate(date) { - this._ensureStateLoaded(); - - this._changes = this._changes.filter(function test_age(change) { - return change[0] >= date; - }); - }, - - /** - * Obtains the set of all known Sync GUIDs for add-ons. - * - * @return Object with guids as keys and values of true. - */ - getAllSyncGUIDs: function getAllSyncGUIDs() { - let result = {}; - for (let id in this.addons) { - result[id] = true; - } - - return result; - }, - - /** - * Obtain the add-on state record for an add-on by Sync GUID. - * - * If the add-on could not be found, returns null. - * - * @param guid - * Sync GUID of add-on to retrieve. - * @return Object on success on null on failure. - */ - getAddonStateFromSyncGUID: function getAddonStateFromSyncGUID(guid) { - for (let id in this.addons) { - let addon = this.addons[id]; - if (addon.guid == guid) { - return addon; - } - } - - return null; - }, - - /** - * Ensures that state is loaded before continuing. - * - * This is called internally by anything that accesses the internal data - * structures. It effectively just-in-time loads serialized state. - */ - _ensureStateLoaded: function _ensureStateLoaded() { - if (this._stateLoaded) { - return; - } - - let cb = Async.makeSpinningCallback(); - this.loadState(null, cb); - cb.wait(); - this._stateLoaded = true; - }, - - /** - * Handler that is invoked as part of the AddonManager listeners. - */ - _handleListener: function _handlerListener(action, addon, requiresRestart) { - // Since this is called as an observer, we explicitly trap errors and - // log them to ourselves so we don't see errors reported elsewhere. - try { - let id = addon.id; - this._log.debug("Add-on change: " + action + " to " + id); - - // We assume that every event for non-restartless add-ons is - // followed by another event and that this follow-up event is the most - // appropriate to react to. Currently we ignore onEnabling, onDisabling, - // and onUninstalling for non-restartless add-ons. - if (requiresRestart === false) { - this._log.debug("Ignoring " + action + " for restartless add-on."); - return; - } - - switch (action) { - case "onEnabling": - case "onEnabled": - case "onDisabling": - case "onDisabled": - case "onInstalled": - case "onInstallEnded": - case "onOperationCancelled": - this.rectifyStateFromAddon(addon); - break; - - case "onUninstalling": - case "onUninstalled": - let id = addon.id; - let addons = this.addons; - if (id in addons) { - let now = new Date(); - let record = addons[id]; - record.installed = false; - record.modified = now; - this._log.debug("Adding change because of uninstall listener: " + - id); - this._addChange(now, CHANGE_UNINSTALLED, record); - } - } - - // See note for _shouldPersist. - if (this._shouldPersist) { - let cb = Async.makeSpinningCallback(); - this.saveState(null, cb); - cb.wait(); - } - } - catch (ex) { - this._log.warn("Exception: ", ex); - } - }, - - // AddonListeners - onEnabling: function onEnabling(addon, requiresRestart) { - this._handleListener("onEnabling", addon, requiresRestart); - }, - onEnabled: function onEnabled(addon) { - this._handleListener("onEnabled", addon); - }, - onDisabling: function onDisabling(addon, requiresRestart) { - this._handleListener("onDisabling", addon, requiresRestart); - }, - onDisabled: function onDisabled(addon) { - this._handleListener("onDisabled", addon); - }, - onInstalling: function onInstalling(addon, requiresRestart) { - this._handleListener("onInstalling", addon, requiresRestart); - }, - onInstalled: function onInstalled(addon) { - this._handleListener("onInstalled", addon); - }, - onUninstalling: function onUninstalling(addon, requiresRestart) { - this._handleListener("onUninstalling", addon, requiresRestart); - }, - onUninstalled: function onUninstalled(addon) { - this._handleListener("onUninstalled", addon); - }, - onOperationCancelled: function onOperationCancelled(addon) { - this._handleListener("onOperationCancelled", addon); - }, - - // InstallListeners - onInstallEnded: function onInstallEnded(install, addon) { - this._handleListener("onInstallEnded", addon); - } -}; diff --git a/components/weave/src/sync/addonutils.js b/components/weave/src/sync/addonutils.js deleted file mode 100644 index 3332f4cfc..000000000 --- a/components/weave/src/sync/addonutils.js +++ /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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["AddonUtils"]; - -var {interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/util.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", - "resource://gre/modules/addons/AddonRepository.jsm"); - -function AddonUtilsInternal() { - this._log = Log.repository.getLogger("Sync.AddonUtils"); - this._log.Level = Log.Level[Svc.Prefs.get("log.logger.addonutils")]; -} -AddonUtilsInternal.prototype = { - /** - * Obtain an AddonInstall object from an AddonSearchResult instance. - * - * The callback will be invoked with the result of the operation. The - * callback receives 2 arguments, error and result. Error will be falsy - * on success or some kind of error value otherwise. The result argument - * will be an AddonInstall on success or null on failure. It is possible - * for the error to be falsy but result to be null. This could happen if - * an install was not found. - * - * @param addon - * AddonSearchResult to obtain install from. - * @param cb - * Function to be called with result of operation. - */ - getInstallFromSearchResult: - function getInstallFromSearchResult(addon, cb, requireSecureURI=true) { - - this._log.debug("Obtaining install for " + addon.id); - - // Verify that the source URI uses TLS. We don't allow installs from - // insecure sources for security reasons. The Addon Manager ensures that - // cert validation, etc is performed. - if (requireSecureURI) { - let scheme = addon.sourceURI.scheme; - if (scheme != "https") { - cb(new Error("Insecure source URI scheme: " + scheme), addon.install); - return; - } - } - - // We should theoretically be able to obtain (and use) addon.install if - // it is available. However, the addon.sourceURI rewriting won't be - // reflected in the AddonInstall, so we can't use it. If we ever get rid - // of sourceURI rewriting, we can avoid having to reconstruct the - // AddonInstall. - AddonManager.getInstallForURL( - addon.sourceURI.spec, - function handleInstall(install) { - cb(null, install); - }, - "application/x-xpinstall", - undefined, - addon.name, - addon.iconURL, - addon.version - ); - }, - - /** - * Installs an add-on from an AddonSearchResult instance. - * - * The options argument defines extra options to control the install. - * Recognized keys in this map are: - * - * syncGUID - Sync GUID to use for the new add-on. - * enabled - Boolean indicating whether the add-on should be enabled upon - * install. - * requireSecureURI - Boolean indicating whether to require a secure - * URI to install from. This defaults to true. - * - * When complete it calls a callback with 2 arguments, error and result. - * - * If error is falsy, result is an object. If error is truthy, result is - * null. - * - * The result object has the following keys: - * - * id ID of add-on that was installed. - * install AddonInstall that was installed. - * addon Addon that was installed. - * - * @param addon - * AddonSearchResult to install add-on from. - * @param options - * Object with additional metadata describing how to install add-on. - * @param cb - * Function to be invoked with result of operation. - */ - installAddonFromSearchResult: - function installAddonFromSearchResult(addon, options, cb) { - this._log.info("Trying to install add-on from search result: " + addon.id); - - if (options.requireSecureURI === undefined) { - options.requireSecureURI = true; - } - - this.getInstallFromSearchResult(addon, function onResult(error, install) { - if (error) { - cb(error, null); - return; - } - - if (!install) { - cb(new Error("AddonInstall not available: " + addon.id), null); - return; - } - - try { - this._log.info("Installing " + addon.id); - let log = this._log; - - let listener = { - onInstallStarted: function onInstallStarted(install) { - if (!options) { - return; - } - - if (options.syncGUID) { - log.info("Setting syncGUID of " + install.name +": " + - options.syncGUID); - install.addon.syncGUID = options.syncGUID; - } - - // We only need to change userDisabled if it is disabled because - // enabled is the default. - if ("enabled" in options && !options.enabled) { - log.info("Marking add-on as disabled for install: " + - install.name); - install.addon.userDisabled = true; - } - }, - onInstallEnded: function(install, addon) { - install.removeListener(listener); - - cb(null, {id: addon.id, install: install, addon: addon}); - }, - onInstallFailed: function(install) { - install.removeListener(listener); - - cb(new Error("Install failed: " + install.error), null); - }, - onDownloadFailed: function(install) { - install.removeListener(listener); - - cb(new Error("Download failed: " + install.error), null); - } - }; - install.addListener(listener); - install.install(); - } - catch (ex) { - this._log.error("Error installing add-on: ", ex); - cb(ex, null); - } - }.bind(this), options.requireSecureURI); - }, - - /** - * Uninstalls the Addon instance and invoke a callback when it is done. - * - * @param addon - * Addon instance to uninstall. - * @param cb - * Function to be invoked when uninstall has finished. It receives a - * truthy value signifying error and the add-on which was uninstalled. - */ - uninstallAddon: function uninstallAddon(addon, cb) { - let listener = { - onUninstalling: function(uninstalling, needsRestart) { - if (addon.id != uninstalling.id) { - return; - } - - // We assume restartless add-ons will send the onUninstalled event - // soon. - if (!needsRestart) { - return; - } - - // For non-restartless add-ons, we issue the callback on uninstalling - // because we will likely never see the uninstalled event. - AddonManager.removeAddonListener(listener); - cb(null, addon); - }, - onUninstalled: function(uninstalled) { - if (addon.id != uninstalled.id) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(null, addon); - } - }; - AddonManager.addAddonListener(listener); - addon.uninstall(); - }, - - /** - * Installs multiple add-ons specified by metadata. - * - * The first argument is an array of objects. Each object must have the - * following keys: - * - * id - public ID of the add-on to install. - * syncGUID - syncGUID for new add-on. - * enabled - boolean indicating whether the add-on should be enabled. - * requireSecureURI - Boolean indicating whether to require a secure - * URI when installing from a remote location. This defaults to - * true. - * - * The callback will be called when activity on all add-ons is complete. The - * callback receives 2 arguments, error and result. - * - * If error is truthy, it contains a string describing the overall error. - * - * The 2nd argument to the callback is always an object with details on the - * overall execution state. It contains the following keys: - * - * installedIDs Array of add-on IDs that were installed. - * installs Array of AddonInstall instances that were installed. - * addons Array of Addon instances that were installed. - * errors Array of errors encountered. Only has elements if error is - * truthy. - * - * @param installs - * Array of objects describing add-ons to install. - * @param cb - * Function to be called when all actions are complete. - */ - installAddons: function installAddons(installs, cb) { - if (!cb) { - throw new Error("Invalid argument: cb is not defined."); - } - - let ids = []; - for (let addon of installs) { - ids.push(addon.id); - } - - AddonRepository.getAddonsByIDs(ids, { - searchSucceeded: function searchSucceeded(addons, addonsLength, total) { - this._log.info("Found " + addonsLength + "/" + ids.length + - " add-ons during repository search."); - - let ourResult = { - installedIDs: [], - installs: [], - addons: [], - errors: [] - }; - - if (!addonsLength) { - cb(null, ourResult); - return; - } - - let expectedInstallCount = 0; - let finishedCount = 0; - let installCallback = function installCallback(error, result) { - finishedCount++; - - if (error) { - ourResult.errors.push(error); - } else { - ourResult.installedIDs.push(result.id); - ourResult.installs.push(result.install); - ourResult.addons.push(result.addon); - } - - if (finishedCount >= expectedInstallCount) { - if (ourResult.errors.length > 0) { - cb(new Error("1 or more add-ons failed to install"), ourResult); - } else { - cb(null, ourResult); - } - } - }.bind(this); - - let toInstall = []; - - // Rewrite the "src" query string parameter of the source URI to note - // that the add-on was installed by Sync and not something else so - // server-side metrics aren't skewed (bug 708134). The server should - // ideally send proper URLs, but this solution was deemed too - // complicated at the time the functionality was implemented. - for (let addon of addons) { - // sourceURI presence isn't enforced by AddonRepository. So, we skip - // add-ons without a sourceURI. - if (!addon.sourceURI) { - this._log.info("Skipping install of add-on because missing " + - "sourceURI: " + addon.id); - continue; - } - - toInstall.push(addon); - - // We should always be able to QI the nsIURI to nsIURL. If not, we - // still try to install the add-on, but we don't rewrite the URL, - // potentially skewing metrics. - try { - addon.sourceURI.QueryInterface(Ci.nsIURL); - } catch (ex) { - this._log.warn("Unable to QI sourceURI to nsIURL: " + - addon.sourceURI.spec); - continue; - } - - let params = addon.sourceURI.query.split("&").map( - function rewrite(param) { - - if (param.indexOf("src=") == 0) { - return "src=sync"; - } else { - return param; - } - }); - - addon.sourceURI.query = params.join("&"); - } - - expectedInstallCount = toInstall.length; - - if (!expectedInstallCount) { - cb(null, ourResult); - return; - } - - // Start all the installs asynchronously. They will report back to us - // as they finish, eventually triggering the global callback. - for (let addon of toInstall) { - let options = {}; - for (let install of installs) { - if (install.id == addon.id) { - options = install; - break; - } - } - - this.installAddonFromSearchResult(addon, options, installCallback); - } - - }.bind(this), - - searchFailed: function searchFailed() { - cb(new Error("AddonRepository search failed"), null); - }, - }); - }, - - /** - * Update the user disabled flag for an add-on. - * - * The supplied callback will be called when the operation is - * complete. If the new flag matches the existing or if the add-on - * isn't currently active, the function will fire the callback - * immediately. Else, the callback is invoked when the AddonManager - * reports the change has taken effect or has been registered. - * - * The callback receives as arguments: - * - * (Error) Encountered error during operation or null on success. - * (Addon) The add-on instance being operated on. - * - * @param addon - * (Addon) Add-on instance to operate on. - * @param value - * (bool) New value for add-on's userDisabled property. - * @param cb - * (function) Callback to be invoked on completion. - */ - updateUserDisabled: function updateUserDisabled(addon, value, cb) { - if (addon.userDisabled == value) { - cb(null, addon); - return; - } - - let listener = { - onEnabling: function onEnabling(wrapper, needsRestart) { - this._log.debug("onEnabling: " + wrapper.id); - if (wrapper.id != addon.id) { - return; - } - - // We ignore the restartless case because we'll get onEnabled shortly. - if (!needsRestart) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(null, wrapper); - }.bind(this), - - onEnabled: function onEnabled(wrapper) { - this._log.debug("onEnabled: " + wrapper.id); - if (wrapper.id != addon.id) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(null, wrapper); - }.bind(this), - - onDisabling: function onDisabling(wrapper, needsRestart) { - this._log.debug("onDisabling: " + wrapper.id); - if (wrapper.id != addon.id) { - return; - } - - if (!needsRestart) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(null, wrapper); - }.bind(this), - - onDisabled: function onDisabled(wrapper) { - this._log.debug("onDisabled: " + wrapper.id); - if (wrapper.id != addon.id) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(null, wrapper); - }.bind(this), - - onOperationCancelled: function onOperationCancelled(wrapper) { - this._log.debug("onOperationCancelled: " + wrapper.id); - if (wrapper.id != addon.id) { - return; - } - - AddonManager.removeAddonListener(listener); - cb(new Error("Operation cancelled"), wrapper); - }.bind(this) - }; - - // The add-on listeners are only fired if the add-on is active. If not, the - // change is silently updated and made active when/if the add-on is active. - - if (!addon.appDisabled) { - AddonManager.addAddonListener(listener); - } - - this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value); - addon.userDisabled = !!value; - - if (!addon.appDisabled) { - cb(null, addon); - return; - } - // Else the listener will handle invoking the callback. - }, - -}; - -XPCOMUtils.defineLazyGetter(this, "AddonUtils", function() { - return new AddonUtilsInternal(); -}); diff --git a/components/weave/src/sync/constants.js b/components/weave/src/sync/constants.js deleted file mode 100644 index 88464f023..000000000 --- a/components/weave/src/sync/constants.js +++ /dev/null @@ -1,193 +0,0 @@ -#filter substitution -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Process each item in the "constants hash" to add to "global" and give a name -this.EXPORTED_SYMBOLS = []; -for (let [key, val] of Object.entries({ - -WEAVE_VERSION: "@weave_version@", - -// Sync Server API version that the client supports. -SYNC_API_VERSION: "1.1", -USER_API_VERSION: "1.0", -MISC_API_VERSION: "1.0", - -// Version of the data format this client supports. The data format describes -// how records are packaged; this is separate from the Server API version and -// the per-engine cleartext formats. -STORAGE_VERSION: 5, -PREFS_BRANCH: "services.sync.", - -// Host "key" to access Weave Identity in the password manager -PWDMGR_HOST: "chrome://weave", -PWDMGR_PASSWORD_REALM: "Mozilla Services Password", -PWDMGR_PASSPHRASE_REALM: "Mozilla Services Encryption Passphrase", -PWDMGR_KEYBUNDLE_REALM: "Mozilla Services Key Bundles", - -// Put in [] because those aren't allowed in a collection name. -DEFAULT_KEYBUNDLE_NAME: "[default]", - -// Our extra input to SHA256-HMAC in generateEntry. -// This includes the full crypto spec; change this when our algo changes. -HMAC_INPUT: "Sync-AES_256_CBC-HMAC256", - -// Key dimensions. -SYNC_KEY_ENCODED_LENGTH: 26, -SYNC_KEY_DECODED_LENGTH: 16, -SYNC_KEY_HYPHENATED_LENGTH: 31, // 26 chars, 5 hyphens. - -NO_SYNC_NODE_INTERVAL: 10 * 60 * 1000, // 10 minutes - -MAX_ERROR_COUNT_BEFORE_BACKOFF: 3, -MAX_IGNORE_ERROR_COUNT: 5, - -// Backoff intervals -MINIMUM_BACKOFF_INTERVAL: 15 * 60 * 1000, // 15 minutes -MAXIMUM_BACKOFF_INTERVAL: 8 * 60 * 60 * 1000, // 8 hours - -// HMAC event handling timeout. -// 10 minutes: a compromise between the multi-desktop sync interval -// and the mobile sync interval. -HMAC_EVENT_INTERVAL: 600000, - -// How long to wait between sync attempts if the Master Password is locked. -MASTER_PASSWORD_LOCKED_RETRY_INTERVAL: 15 * 60 * 1000, // 15 minutes - -// The default for how long we "block" sync from running when doing a migration. -DEFAULT_BLOCK_PERIOD: 2 * 24 * 60 * 60 * 1000, // 2 days - -// Separate from the ID fetch batch size to allow tuning for mobile. -MOBILE_BATCH_SIZE: 50, - -// 50 is hardcoded here because of URL length restrictions. -// (GUIDs can be up to 64 chars long.) -// Individual engines can set different values for their limit if their -// identifiers are shorter. -DEFAULT_GUID_FETCH_BATCH_SIZE: 50, -DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE: 50, - -// Default batch size for applying incoming records. -DEFAULT_STORE_BATCH_SIZE: 1, -HISTORY_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE -FORMS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE -PASSWORDS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE -ADDONS_STORE_BATCH_SIZE: 1000000, // process all addons at once -APPS_STORE_BATCH_SIZE: 50, // same as MOBILE_BATCH_SIZE - -// score thresholds for early syncs -SINGLE_USER_THRESHOLD: 1000, -MULTI_DEVICE_THRESHOLD: 300, - -// Other score increment constants -SCORE_INCREMENT_SMALL: 1, -SCORE_INCREMENT_MEDIUM: 10, - -// Instant sync score increment -SCORE_INCREMENT_XLARGE: 300 + 1, //MULTI_DEVICE_THRESHOLD + 1 - -// Delay before incrementing global score -SCORE_UPDATE_DELAY: 100, - -// Delay for the back observer debouncer. This is chosen to be longer than any -// observed spurious idle/back events and short enough to pre-empt user activity. -IDLE_OBSERVER_BACK_DELAY: 100, - -// Number of records to upload in a single POST (multiple POSTS if exceeded) -// FIXME: Record size limit is 256k (new cluster), so this can be quite large! -// (Bug 569295) -MAX_UPLOAD_RECORDS: 100, -MAX_HISTORY_UPLOAD: 5000, -MAX_HISTORY_DOWNLOAD: 5000, - -// Top-level statuses: -STATUS_OK: "success.status_ok", -SYNC_FAILED: "error.sync.failed", -LOGIN_FAILED: "error.login.failed", -SYNC_FAILED_PARTIAL: "error.sync.failed_partial", -CLIENT_NOT_CONFIGURED: "service.client_not_configured", -STATUS_DISABLED: "service.disabled", -MASTER_PASSWORD_LOCKED: "service.master_password_locked", - -// success states -LOGIN_SUCCEEDED: "success.login", -SYNC_SUCCEEDED: "success.sync", -ENGINE_SUCCEEDED: "success.engine", - -// login failure status codes: -LOGIN_FAILED_NO_USERNAME: "error.login.reason.no_username", -LOGIN_FAILED_NO_PASSWORD: "error.login.reason.no_password2", -LOGIN_FAILED_NO_PASSPHRASE: "error.login.reason.no_recoverykey", -LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network", -LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server", -LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.recoverykey", -LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.account", -LOGIN_FAILED_NOT_READY: "error.login.reason.initializing", - -// sync failure status codes -METARECORD_DOWNLOAD_FAIL: "error.sync.reason.metarecord_download_fail", -VERSION_OUT_OF_DATE: "error.sync.reason.version_out_of_date", -DESKTOP_VERSION_OUT_OF_DATE: "error.sync.reason.desktop_version_out_of_date", -SETUP_FAILED_NO_PASSPHRASE: "error.sync.reason.setup_failed_no_passphrase", -CREDENTIALS_CHANGED: "error.sync.reason.credentials_changed", -ABORT_SYNC_COMMAND: "aborting sync, process commands said so", -NO_SYNC_NODE_FOUND: "error.sync.reason.no_node_found", -OVER_QUOTA: "error.sync.reason.over_quota", -PROLONGED_SYNC_FAILURE: "error.sync.prolonged_failure", -SERVER_MAINTENANCE: "error.sync.reason.serverMaintenance", - -RESPONSE_OVER_QUOTA: "14", - -// engine failure status codes -ENGINE_UPLOAD_FAIL: "error.engine.reason.record_upload_fail", -ENGINE_DOWNLOAD_FAIL: "error.engine.reason.record_download_fail", -ENGINE_UNKNOWN_FAIL: "error.engine.reason.unknown_fail", -ENGINE_APPLY_FAIL: "error.engine.reason.apply_fail", -ENGINE_METARECORD_DOWNLOAD_FAIL: "error.engine.reason.metarecord_download_fail", -ENGINE_METARECORD_UPLOAD_FAIL: "error.engine.reason.metarecord_upload_fail", - -JPAKE_ERROR_CHANNEL: "jpake.error.channel", -JPAKE_ERROR_NETWORK: "jpake.error.network", -JPAKE_ERROR_SERVER: "jpake.error.server", -JPAKE_ERROR_TIMEOUT: "jpake.error.timeout", -JPAKE_ERROR_INTERNAL: "jpake.error.internal", -JPAKE_ERROR_INVALID: "jpake.error.invalid", -JPAKE_ERROR_NODATA: "jpake.error.nodata", -JPAKE_ERROR_KEYMISMATCH: "jpake.error.keymismatch", -JPAKE_ERROR_WRONGMESSAGE: "jpake.error.wrongmessage", -JPAKE_ERROR_USERABORT: "jpake.error.userabort", -JPAKE_ERROR_DELAYUNSUPPORTED: "jpake.error.delayunsupported", - -// info types for Service.getStorageInfo -INFO_COLLECTIONS: "collections", -INFO_COLLECTION_USAGE: "collection_usage", -INFO_COLLECTION_COUNTS: "collection_counts", -INFO_QUOTA: "quota", - -// Ways that a sync can be disabled (messages only to be printed in debug log) -kSyncMasterPasswordLocked: "User elected to leave Master Password locked", -kSyncWeaveDisabled: "Weave is disabled", -kSyncNetworkOffline: "Network is offline", -kSyncBackoffNotMet: "Trying to sync before the server said it's okay", -kFirstSyncChoiceNotMade: "User has not selected an action for first sync", - -// Application IDs -FIREFOX_ID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", -PALEMOON_ID: "{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}", -FENNEC_ID: "{a23983c0-fd0e-11dc-95ff-0800200c9a66}", -SEAMONKEY_ID: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", -TEST_HARNESS_ID: "xuth@mozilla.org", - -MIN_PP_LENGTH: 12, -MIN_PASS_LENGTH: 8, - -DEVICE_TYPE_DESKTOP: "desktop", -DEVICE_TYPE_MOBILE: "mobile", - -LOG_DATE_FORMAT: "%Y-%m-%d %H:%M:%S", - -})) { - this[key] = val; - this.EXPORTED_SYMBOLS.push(key); -} diff --git a/components/weave/src/sync/engines.js b/components/weave/src/sync/engines.js deleted file mode 100644 index cdc6812e0..000000000 --- a/components/weave/src/sync/engines.js +++ /dev/null @@ -1,1613 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "EngineManager", - "Engine", - "SyncEngine", - "Tracker", - "Store" -]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Async.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/resource.js"); -Cu.import("resource://services-sync/util.js"); - -/* - * Trackers are associated with a single engine and deal with - * listening for changes to their particular data type. - * - * There are two things they keep track of: - * 1) A score, indicating how urgently the engine wants to sync - * 2) A list of IDs for all the changed items that need to be synced - * and updating their 'score', indicating how urgently they - * want to sync. - * - */ -this.Tracker = function Tracker(name, engine) { - if (!engine) { - throw new Error("Tracker must be associated with an Engine instance."); - } - - name = name || "Unnamed"; - this.name = this.file = name.toLowerCase(); - this.engine = engine; - - this._log = Log.repository.getLogger("Sync.Tracker." + name); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log.Level[level]; - - this._score = 0; - this._ignored = []; - this.ignoreAll = false; - this.changedIDs = {}; - this.loadChangedIDs(); - - Svc.Obs.add("weave:engine:start-tracking", this); - Svc.Obs.add("weave:engine:stop-tracking", this); - - Svc.Prefs.observe("engine." + this.engine.prefName, this); -}; - -Tracker.prototype = { - /* - * Score can be called as often as desired to decide which engines to sync - * - * Valid values for score: - * -1: Do not sync unless the user specifically requests it (almost disabled) - * 0: Nothing has changed - * 100: Please sync me ASAP! - * - * Setting it to other values should (but doesn't currently) throw an exception - */ - get score() { - return this._score; - }, - - set score(value) { - this._score = value; - Observers.notify("weave:engine:score:updated", this.name); - }, - - // Should be called by service everytime a sync has been done for an engine - resetScore: function () { - this._score = 0; - }, - - persistChangedIDs: true, - - /** - * Persist changedIDs to disk at a later date. - * Optionally pass a callback to be invoked when the write has occurred. - */ - saveChangedIDs: function (cb) { - if (!this.persistChangedIDs) { - this._log.debug("Not saving changedIDs."); - return; - } - Utils.namedTimer(function () { - this._log.debug("Saving changed IDs to " + this.file); - Utils.jsonSave("changes/" + this.file, this, this.changedIDs, cb); - }, 1000, this, "_lazySave"); - }, - - loadChangedIDs: function (cb) { - Utils.jsonLoad("changes/" + this.file, this, function(json) { - if (json && (typeof(json) == "object")) { - this.changedIDs = json; - } else if (json !== null) { - this._log.warn("Changed IDs file " + this.file + " contains non-object value."); - json = null; - } - if (cb) { - cb.call(this, json); - } - }); - }, - - // ignore/unignore specific IDs. Useful for ignoring items that are - // being processed, or that shouldn't be synced. - // But note: not persisted to disk - - ignoreID: function (id) { - this.unignoreID(id); - this._ignored.push(id); - }, - - unignoreID: function (id) { - let index = this._ignored.indexOf(id); - if (index != -1) - this._ignored.splice(index, 1); - }, - - addChangedID: function (id, when) { - if (!id) { - this._log.warn("Attempted to add undefined ID to tracker"); - return false; - } - - if (this.ignoreAll || this._ignored.includes(id)) { - return false; - } - - // Default to the current time in seconds if no time is provided. - if (when == null) { - when = Math.floor(Date.now() / 1000); - } - - // Add/update the entry if we have a newer time. - if ((this.changedIDs[id] || -Infinity) < when) { - this._log.trace("Adding changed ID: " + id + ", " + when); - this.changedIDs[id] = when; - this.saveChangedIDs(this.onSavedChangedIDs); - } - - return true; - }, - - removeChangedID: function (id) { - if (!id) { - this._log.warn("Attempted to remove undefined ID to tracker"); - return false; - } - if (this.ignoreAll || this._ignored.includes(id)) { - return false; - } - if (this.changedIDs[id] != null) { - this._log.trace("Removing changed ID " + id); - delete this.changedIDs[id]; - this.saveChangedIDs(); - } - return true; - }, - - clearChangedIDs: function () { - this._log.trace("Clearing changed ID list"); - this.changedIDs = {}; - this.saveChangedIDs(); - }, - - _isTracking: false, - - // Override these in your subclasses. - startTracking: function () { - }, - - stopTracking: function () { - }, - - engineIsEnabled: function () { - if (!this.engine) { - // Can't tell -- we must be running in a test! - return true; - } - return this.engine.enabled; - }, - - onEngineEnabledChanged: function (engineEnabled) { - if (engineEnabled == this._isTracking) { - return; - } - - if (engineEnabled) { - this.startTracking(); - this._isTracking = true; - } else { - this.stopTracking(); - this._isTracking = false; - this.clearChangedIDs(); - } - }, - - observe: function (subject, topic, data) { - switch (topic) { - case "weave:engine:start-tracking": - if (!this.engineIsEnabled()) { - return; - } - this._log.trace("Got start-tracking."); - if (!this._isTracking) { - this.startTracking(); - this._isTracking = true; - } - return; - case "weave:engine:stop-tracking": - this._log.trace("Got stop-tracking."); - if (this._isTracking) { - this.stopTracking(); - this._isTracking = false; - } - return; - case "nsPref:changed": - if (data == PREFS_BRANCH + "engine." + this.engine.prefName) { - this.onEngineEnabledChanged(this.engine.enabled); - } - return; - } - } -}; - - - -/** - * The Store serves as the interface between Sync and stored data. - * - * The name "store" is slightly a misnomer because it doesn't actually "store" - * anything. Instead, it serves as a gateway to something that actually does - * the "storing." - * - * The store is responsible for record management inside an engine. It tells - * Sync what items are available for Sync, converts items to and from Sync's - * record format, and applies records from Sync into changes on the underlying - * store. - * - * Store implementations require a number of functions to be implemented. These - * are all documented below. - * - * For stores that deal with many records or which have expensive store access - * routines, it is highly recommended to implement a custom applyIncomingBatch - * and/or applyIncoming function on top of the basic APIs. - */ - -this.Store = function Store(name, engine) { - if (!engine) { - throw new Error("Store must be associated with an Engine instance."); - } - - name = name || "Unnamed"; - this.name = name.toLowerCase(); - this.engine = engine; - - this._log = Log.repository.getLogger("Sync.Store." + name); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log.Level[level]; - - XPCOMUtils.defineLazyGetter(this, "_timer", function() { - return Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - }); -} -Store.prototype = { - - _sleep: function _sleep(delay) { - let cb = Async.makeSyncCallback(); - this._timer.initWithCallback(cb, delay, Ci.nsITimer.TYPE_ONE_SHOT); - Async.waitForSyncCallback(cb); - }, - - /** - * Apply multiple incoming records against the store. - * - * This is called with a set of incoming records to process. The function - * should look at each record, reconcile with the current local state, and - * make the local changes required to bring its state in alignment with the - * record. - * - * The default implementation simply iterates over all records and calls - * applyIncoming(). Store implementations may overwrite this function - * if desired. - * - * @param records Array of records to apply - * @return Array of record IDs which did not apply cleanly - */ - applyIncomingBatch: function (records) { - let failed = []; - for (let record of records) { - try { - this.applyIncoming(record); - } catch (ex if (ex.code == Engine.prototype.eEngineAbortApplyIncoming)) { - // This kind of exception should have a 'cause' attribute, which is an - // originating exception. - // ex.cause will carry its stack with it when rethrown. - throw ex.cause; - } catch (ex) { - this._log.warn("Failed to apply incoming record " + record.id); - this._log.warn("Encountered exception: ", ex); - failed.push(record.id); - } - }; - return failed; - }, - - /** - * Apply a single record against the store. - * - * This takes a single record and makes the local changes required so the - * local state matches what's in the record. - * - * The default implementation calls one of remove(), create(), or update() - * depending on the state obtained from the store itself. Store - * implementations may overwrite this function if desired. - * - * @param record - * Record to apply - */ - applyIncoming: function (record) { - if (record.deleted) - this.remove(record); - else if (!this.itemExists(record.id)) - this.create(record); - else - this.update(record); - }, - - // override these in derived objects - - /** - * Create an item in the store from a record. - * - * This is called by the default implementation of applyIncoming(). If using - * applyIncomingBatch(), this won't be called unless your store calls it. - * - * @param record - * The store record to create an item from - */ - create: function (record) { - throw "override create in a subclass"; - }, - - /** - * Remove an item in the store from a record. - * - * This is called by the default implementation of applyIncoming(). If using - * applyIncomingBatch(), this won't be called unless your store calls it. - * - * @param record - * The store record to delete an item from - */ - remove: function (record) { - throw "override remove in a subclass"; - }, - - /** - * Update an item from a record. - * - * This is called by the default implementation of applyIncoming(). If using - * applyIncomingBatch(), this won't be called unless your store calls it. - * - * @param record - * The record to use to update an item from - */ - update: function (record) { - throw "override update in a subclass"; - }, - - /** - * Determine whether a record with the specified ID exists. - * - * Takes a string record ID and returns a booleans saying whether the record - * exists. - * - * @param id - * string record ID - * @return boolean indicating whether record exists locally - */ - itemExists: function (id) { - throw "override itemExists in a subclass"; - }, - - /** - * Create a record from the specified ID. - * - * If the ID is known, the record should be populated with metadata from - * the store. If the ID is not known, the record should be created with the - * delete field set to true. - * - * @param id - * string record ID - * @param collection - * Collection to add record to. This is typically passed into the - * constructor for the newly-created record. - * @return record type for this engine - */ - createRecord: function (id, collection) { - throw "override createRecord in a subclass"; - }, - - /** - * Change the ID of a record. - * - * @param oldID - * string old/current record ID - * @param newID - * string new record ID - */ - changeItemID: function (oldID, newID) { - throw "override changeItemID in a subclass"; - }, - - /** - * Obtain the set of all known record IDs. - * - * @return Object with ID strings as keys and values of true. The values - * are ignored. - */ - getAllIDs: function () { - throw "override getAllIDs in a subclass"; - }, - - /** - * Wipe all data in the store. - * - * This function is called during remote wipes or when replacing local data - * with remote data. - * - * This function should delete all local data that the store is managing. It - * can be thought of as clearing out all state and restoring the "new - * browser" state. - */ - wipe: function () { - throw "override wipe in a subclass"; - } -}; - -this.EngineManager = function EngineManager(service) { - this.service = service; - - this._engines = {}; - - // This will be populated by Service on startup. - this._declined = new Set(); - this._log = Log.repository.getLogger("Sync.EngineManager"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.engines", "Debug")]; -} -EngineManager.prototype = { - get: function (name) { - // Return an array of engines if we have an array of names - if (Array.isArray(name)) { - let engines = []; - name.forEach(function(name) { - let engine = this.get(name); - if (engine) { - engines.push(engine); - } - }, this); - return engines; - } - - let engine = this._engines[name]; - if (!engine) { - this._log.debug("Could not get engine: " + name); - if (Object.keys) { - this._log.debug("Engines are: " + JSON.stringify(Object.keys(this._engines))); - } - } - return engine; - }, - - getAll: function () { - let engines = []; - for (let [, engine] of Object.entries(this._engines)) { - engines.push(engine); - } - return engines; - }, - - /** - * N.B., does not pay attention to the declined list. - */ - getEnabled: function () { - return this.getAll() - .filter((engine) => engine.enabled) - .sort((a, b) => a.syncPriority - b.syncPriority); - }, - - get enabledEngineNames() { - return this.getEnabled().map(e => e.name); - }, - - persistDeclined: function () { - Svc.Prefs.set("declinedEngines", [...this._declined].join(",")); - }, - - /** - * Returns an array. - */ - getDeclined: function () { - return [...this._declined]; - }, - - setDeclined: function (engines) { - this._declined = new Set(engines); - this.persistDeclined(); - }, - - isDeclined: function (engineName) { - return this._declined.has(engineName); - }, - - /** - * Accepts a Set or an array. - */ - decline: function (engines) { - for (let e of engines) { - this._declined.add(e); - } - this.persistDeclined(); - }, - - undecline: function (engines) { - for (let e of engines) { - this._declined.delete(e); - } - this.persistDeclined(); - }, - - /** - * Mark any non-enabled engines as declined. - * - * This is useful after initial customization during setup. - */ - declineDisabled: function () { - for (let e of this.getAll()) { - if (!e.enabled) { - this._log.debug("Declining disabled engine " + e.name); - this._declined.add(e.name); - } - } - this.persistDeclined(); - }, - - /** - * Register an Engine to the service. Alternatively, give an array of engine - * objects to register. - * - * @param engineObject - * Engine object used to get an instance of the engine - * @return The engine object if anything failed - */ - register: function (engineObject) { - if (Array.isArray(engineObject)) { - return engineObject.map(this.register, this); - } - - try { - let engine = new engineObject(this.service); - let name = engine.name; - if (name in this._engines) { - this._log.error("Engine '" + name + "' is already registered!"); - } else { - this._engines[name] = engine; - } - } catch (ex) { - this._log.error("Engine init error: ", ex); - - let mesg = ex.message ? ex.message : ex; - let name = engineObject || ""; - name = name.prototype || ""; - name = name.name || ""; - - let out = "Could not initialize engine '" + name + "': " + mesg; - this._log.error(out); - - return engineObject; - } - }, - - unregister: function (val) { - let name = val; - if (val instanceof Engine) { - name = val.name; - } - delete this._engines[name]; - }, - - clear: function () { - for (let name in this._engines) { - delete this._engines[name]; - } - }, -}; - -this.Engine = function Engine(name, service) { - if (!service) { - throw new Error("Engine must be associated with a Service instance."); - } - - this.Name = name || "Unnamed"; - this.name = name.toLowerCase(); - this.service = service; - - this._notify = Utils.notify("weave:engine:"); - this._log = Log.repository.getLogger("Sync.Engine." + this.Name); - let level = Svc.Prefs.get("log.logger.engine." + this.name, "Debug"); - this._log.level = Log.Level[level]; - - this._tracker; // initialize tracker to load previously changed IDs - this._log.debug("Engine initialized"); -} -Engine.prototype = { - // _storeObj, and _trackerObj should to be overridden in subclasses - _storeObj: Store, - _trackerObj: Tracker, - - // Local 'constant'. - // Signal to the engine that processing further records is pointless. - eEngineAbortApplyIncoming: "error.engine.abort.applyincoming", - - get prefName() { - return this.name; - }, - - get enabled() { - // XXX: Disable non-functional add-ons syncing for the time being - // This check can go away when add-on syncing is addressed - if (this.prefName == "addons") - return false; - - return Svc.Prefs.get("engine." + this.prefName, false); - }, - - set enabled(val) { - Svc.Prefs.set("engine." + this.prefName, !!val); - }, - - get score() { - return this._tracker.score; - }, - - get _store() { - let store = new this._storeObj(this.Name, this); - this.__defineGetter__("_store", () => store); - return store; - }, - - get _tracker() { - let tracker = new this._trackerObj(this.Name, this); - this.__defineGetter__("_tracker", () => tracker); - return tracker; - }, - - sync: function () { - if (!this.enabled) { - return; - } - - if (!this._sync) { - throw "engine does not implement _sync method"; - } - - this._notify("sync", this.name, this._sync)(); - }, - - /** - * Get rid of any local meta-data. - */ - resetClient: function () { - if (!this._resetClient) { - throw "engine does not implement _resetClient method"; - } - - this._notify("reset-client", this.name, this._resetClient)(); - }, - - _wipeClient: function () { - this.resetClient(); - this._log.debug("Deleting all local data"); - this._tracker.ignoreAll = true; - this._store.wipe(); - this._tracker.ignoreAll = false; - this._tracker.clearChangedIDs(); - }, - - wipeClient: function () { - this._notify("wipe-client", this.name, this._wipeClient)(); - } -}; - -this.SyncEngine = function SyncEngine(name, service) { - Engine.call(this, name || "SyncEngine", service); - - this.loadToFetch(); - this.loadPreviousFailed(); -} - -// Enumeration to define approaches to handling bad records. -// Attached to the constructor to allow use as a kind of static enumeration. -SyncEngine.kRecoveryStrategy = { - ignore: "ignore", - retry: "retry", - error: "error" -}; - -SyncEngine.prototype = { - __proto__: Engine.prototype, - _recordObj: CryptoWrapper, - version: 1, - - // Which sortindex to use when retrieving records for this engine. - _defaultSort: undefined, - - // A relative priority to use when computing an order - // for engines to be synced. Higher-priority engines - // (lower numbers) are synced first. - // It is recommended that a unique value be used for each engine, - // in order to guarantee a stable sequence. - syncPriority: 0, - - // How many records to pull in a single sync. This is primarily to avoid very - // long first syncs against profiles with many history records. - downloadLimit: null, - - // How many records to pull at one time when specifying IDs. This is to avoid - // URI length limitations. - guidFetchBatchSize: DEFAULT_GUID_FETCH_BATCH_SIZE, - mobileGUIDFetchBatchSize: DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE, - - // How many records to process in a single batch. - applyIncomingBatchSize: DEFAULT_STORE_BATCH_SIZE, - - get storageURL() { - return this.service.storageURL; - }, - - get engineURL() { - return this.storageURL + this.name; - }, - - get cryptoKeysURL() { - return this.storageURL + "crypto/keys"; - }, - - get metaURL() { - return this.storageURL + "meta/global"; - }, - - get syncID() { - // Generate a random syncID if we don't have one - let syncID = Svc.Prefs.get(this.name + ".syncID", ""); - return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; - }, - set syncID(value) { - Svc.Prefs.set(this.name + ".syncID", value); - }, - - /* - * lastSync is a timestamp in server time. - */ - get lastSync() { - return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0")); - }, - set lastSync(value) { - // Reset the pref in-case it's a number instead of a string - Svc.Prefs.reset(this.name + ".lastSync"); - // Store the value as a string to keep floating point precision - Svc.Prefs.set(this.name + ".lastSync", value.toString()); - }, - resetLastSync: function () { - this._log.debug("Resetting " + this.name + " last sync time"); - Svc.Prefs.reset(this.name + ".lastSync"); - Svc.Prefs.set(this.name + ".lastSync", "0"); - this.lastSyncLocal = 0; - }, - - get toFetch() { - return this._toFetch; - }, - set toFetch(val) { - let cb = (error) => this._log.error("Failed to read JSON records to fetch: ", error); - // Coerce the array to a string for more efficient comparison. - if (val + "" == this._toFetch) { - return; - } - this._toFetch = val; - Utils.namedTimer(function () { - Utils.jsonSave("toFetch/" + this.name, this, val, cb); - }, 0, this, "_toFetchDelay"); - }, - - loadToFetch: function () { - // Initialize to empty if there's no file. - this._toFetch = []; - Utils.jsonLoad("toFetch/" + this.name, this, function(toFetch) { - if (toFetch) { - this._toFetch = toFetch; - } - }); - }, - - get previousFailed() { - return this._previousFailed; - }, - set previousFailed(val) { - let cb = (error) => { - if (error) { - this._log.error("Failed to set previousFailed", error); - } else { - this._log.debug("Successfully wrote previousFailed."); - } - } - // Coerce the array to a string for more efficient comparison. - if (val + "" == this._previousFailed) { - return; - } - this._previousFailed = val; - Utils.namedTimer(function () { - Utils.jsonSave("failed/" + this.name, this, val, cb); - }, 0, this, "_previousFailedDelay"); - }, - - loadPreviousFailed: function () { - // Initialize to empty if there's no file - this._previousFailed = []; - Utils.jsonLoad("failed/" + this.name, this, function(previousFailed) { - if (previousFailed) { - this._previousFailed = previousFailed; - } - }); - }, - - /* - * lastSyncLocal is a timestamp in local time. - */ - get lastSyncLocal() { - return parseInt(Svc.Prefs.get(this.name + ".lastSyncLocal", "0"), 10); - }, - set lastSyncLocal(value) { - // Store as a string because pref can only store C longs as numbers. - Svc.Prefs.set(this.name + ".lastSyncLocal", value.toString()); - }, - - /* - * Returns a mapping of IDs -> changed timestamp. Engine implementations - * can override this method to bypass the tracker for certain or all - * changed items. - */ - getChangedIDs: function () { - return this._tracker.changedIDs; - }, - - // Create a new record using the store and add in crypto fields. - _createRecord: function (id) { - let record = this._store.createRecord(id, this.name); - record.id = id; - record.collection = this.name; - return record; - }, - - // Any setup that needs to happen at the beginning of each sync. - _syncStartup: function () { - - // Determine if we need to wipe on outdated versions - let metaGlobal = this.service.recordManager.get(this.metaURL); - let engines = metaGlobal.payload.engines || {}; - let engineData = engines[this.name] || {}; - - let needsWipe = false; - - // Assume missing versions are 0 and wipe the server - if ((engineData.version || 0) < this.version) { - this._log.debug("Old engine data: " + [engineData.version, this.version]); - - // Prepare to clear the server and upload everything - needsWipe = true; - this.syncID = ""; - - // Set the newer version and newly generated syncID - engineData.version = this.version; - engineData.syncID = this.syncID; - - // Put the new data back into meta/global and mark for upload - engines[this.name] = engineData; - metaGlobal.payload.engines = engines; - metaGlobal.changed = true; - } - // Don't sync this engine if the server has newer data - else if (engineData.version > this.version) { - let error = new String("New data: " + [engineData.version, this.version]); - error.failureCode = VERSION_OUT_OF_DATE; - throw error; - } - // Changes to syncID mean we'll need to upload everything - else if (engineData.syncID != this.syncID) { - this._log.debug("Engine syncIDs: " + [engineData.syncID, this.syncID]); - this.syncID = engineData.syncID; - this._resetClient(); - }; - - // Delete any existing data and reupload on bad version or missing meta. - // No crypto component here...? We could regenerate per-collection keys... - if (needsWipe) { - this.wipeServer(); - } - - // Save objects that need to be uploaded in this._modified. We also save - // the timestamp of this fetch in this.lastSyncLocal. As we successfully - // upload objects we remove them from this._modified. If an error occurs - // or any objects fail to upload, they will remain in this._modified. At - // the end of a sync, or after an error, we add all objects remaining in - // this._modified to the tracker. - this.lastSyncLocal = Date.now(); - if (this.lastSync) { - this._modified = this.getChangedIDs(); - } else { - // Mark all items to be uploaded, but treat them as changed from long ago - this._log.debug("First sync, uploading all items"); - this._modified = {}; - for (let id in this._store.getAllIDs()) { - this._modified[id] = 0; - } - } - // Clear the tracker now. If the sync fails we'll add the ones we failed - // to upload back. - this._tracker.clearChangedIDs(); - - this._log.info(Object.keys(this._modified).length + - " outgoing items pre-reconciliation"); - - // Keep track of what to delete at the end of sync - this._delete = {}; - }, - - /** - * A tiny abstraction to make it easier to test incoming record - * application. - */ - _itemSource: function () { - return new Collection(this.engineURL, this._recordObj, this.service); - }, - - /** - * Process incoming records. - * In the most awful and untestable way possible. - * This now accepts something that makes testing vaguely less impossible. - */ - _processIncoming: function (newitems) { - this._log.trace("Downloading & applying server changes"); - - // Figure out how many total items to fetch this sync; do less on mobile. - let batchSize = this.downloadLimit || Infinity; - let isMobile = (Svc.Prefs.get("client.type") == "mobile"); - - if (!newitems) { - newitems = this._itemSource(); - } - - if (this._defaultSort) { - newitems.sort = this._defaultSort; - } - - if (isMobile) { - batchSize = MOBILE_BATCH_SIZE; - } - newitems.newer = this.lastSync; - newitems.full = true; - newitems.limit = batchSize; - - // applied => number of items that should be applied. - // failed => number of items that failed in this sync. - // newFailed => number of items that failed for the first time in this sync. - // reconciled => number of items that were reconciled. - let count = {applied: 0, failed: 0, newFailed: 0, reconciled: 0}; - let handled = []; - let applyBatch = []; - let failed = []; - let failedInPreviousSync = this.previousFailed; - let fetchBatch = Utils.arrayUnion(this.toFetch, failedInPreviousSync); - // Reset previousFailed for each sync since previously failed items may not fail again. - this.previousFailed = []; - - // Used (via exceptions) to allow the record handler/reconciliation/etc. - // methods to signal that they would like processing of incoming records to - // cease. - let aborting = undefined; - - function doApplyBatch() { - this._tracker.ignoreAll = true; - try { - failed = failed.concat(this._store.applyIncomingBatch(applyBatch)); - } catch (ex) { - // Catch any error that escapes from applyIncomingBatch. At present - // those will all be abort events. - this._log.warn("Got exception, aborting processIncoming. ", ex); - aborting = ex; - } - this._tracker.ignoreAll = false; - applyBatch = []; - } - - function doApplyBatchAndPersistFailed() { - // Apply remaining batch. - if (applyBatch.length) { - doApplyBatch.call(this); - } - // Persist failed items so we refetch them. - if (failed.length) { - this.previousFailed = Utils.arrayUnion(failed, this.previousFailed); - count.failed += failed.length; - this._log.debug("Records that failed to apply: " + failed); - failed = []; - } - } - - let key = this.service.collectionKeys.keyForCollection(this.name); - - // Not binding this method to 'this' for performance reasons. It gets - // called for every incoming record. - let self = this; - - newitems.recordHandler = function(item) { - if (aborting) { - return; - } - - // Grab a later last modified if possible - if (self.lastModified == null || item.modified > self.lastModified) - self.lastModified = item.modified; - - // Track the collection for the WBO. - item.collection = self.name; - - // Remember which records were processed - handled.push(item.id); - - try { - try { - item.decrypt(key); - } catch (ex if Utils.isHMACMismatch(ex)) { - let strategy = self.handleHMACMismatch(item, true); - if (strategy == SyncEngine.kRecoveryStrategy.retry) { - // You only get one retry. - try { - // Try decrypting again, typically because we've got new keys. - self._log.info("Trying decrypt again..."); - key = self.service.collectionKeys.keyForCollection(self.name); - item.decrypt(key); - strategy = null; - } catch (ex if Utils.isHMACMismatch(ex)) { - strategy = self.handleHMACMismatch(item, false); - } - } - - switch (strategy) { - case null: - // Retry succeeded! No further handling. - break; - case SyncEngine.kRecoveryStrategy.retry: - self._log.debug("Ignoring second retry suggestion."); - // Fall through to error case. - case SyncEngine.kRecoveryStrategy.error: - self._log.warn("Error decrypting record: ", ex); - failed.push(item.id); - return; - case SyncEngine.kRecoveryStrategy.ignore: - self._log.debug("Ignoring record " + item.id + - " with bad HMAC: already handled."); - return; - } - } - } catch (ex) { - self._log.warn("Error decrypting record: ", ex); - failed.push(item.id); - return; - } - - let shouldApply; - try { - shouldApply = self._reconcile(item); - } catch (ex if (ex.code == Engine.prototype.eEngineAbortApplyIncoming)) { - self._log.warn("Reconciliation failed: aborting incoming processing."); - failed.push(item.id); - aborting = ex.cause; - } catch (ex) { - self._log.warn("Failed to reconcile incoming record " + item.id); - self._log.warn("Encountered exception: ", ex); - failed.push(item.id); - return; - } - - if (shouldApply) { - count.applied++; - applyBatch.push(item); - } else { - count.reconciled++; - self._log.trace("Skipping reconciled incoming item " + item.id); - } - - if (applyBatch.length == self.applyIncomingBatchSize) { - doApplyBatch.call(self); - } - self._store._sleep(0); - }; - - // Only bother getting data from the server if there's new things - if (this.lastModified == null || this.lastModified > this.lastSync) { - let resp = newitems.get(); - doApplyBatchAndPersistFailed.call(this); - if (!resp.success) { - resp.failureCode = ENGINE_DOWNLOAD_FAIL; - throw resp; - } - - if (aborting) { - throw aborting; - } - } - - // Mobile: check if we got the maximum that we requested; get the rest if so. - if (handled.length == newitems.limit) { - let guidColl = new Collection(this.engineURL, null, this.service); - - // Sort and limit so that on mobile we only get the last X records. - guidColl.limit = this.downloadLimit; - guidColl.newer = this.lastSync; - - // index: Orders by the sortindex descending (highest weight first). - guidColl.sort = "index"; - - let guids = guidColl.get(); - if (!guids.success) - throw guids; - - // Figure out which guids weren't just fetched then remove any guids that - // were already waiting and prepend the new ones - let extra = Utils.arraySub(guids.obj, handled); - if (extra.length > 0) { - fetchBatch = Utils.arrayUnion(extra, fetchBatch); - this.toFetch = Utils.arrayUnion(extra, this.toFetch); - } - } - - // Fast-foward the lastSync timestamp since we have stored the - // remaining items in toFetch. - if (this.lastSync < this.lastModified) { - this.lastSync = this.lastModified; - } - - // Process any backlog of GUIDs. - // At this point we impose an upper limit on the number of items to fetch - // in a single request, even for desktop, to avoid hitting URI limits. - batchSize = isMobile ? this.mobileGUIDFetchBatchSize : - this.guidFetchBatchSize; - - while (fetchBatch.length && !aborting) { - // Reuse the original query, but get rid of the restricting params - // and batch remaining records. - newitems.limit = 0; - newitems.newer = 0; - newitems.ids = fetchBatch.slice(0, batchSize); - - // Reuse the existing record handler set earlier - let resp = newitems.get(); - if (!resp.success) { - resp.failureCode = ENGINE_DOWNLOAD_FAIL; - throw resp; - } - - // This batch was successfully applied. Not using - // doApplyBatchAndPersistFailed() here to avoid writing toFetch twice. - fetchBatch = fetchBatch.slice(batchSize); - this.toFetch = Utils.arraySub(this.toFetch, newitems.ids); - this.previousFailed = Utils.arrayUnion(this.previousFailed, failed); - if (failed.length) { - count.failed += failed.length; - this._log.debug("Records that failed to apply: " + failed); - } - failed = []; - - if (aborting) { - throw aborting; - } - - if (this.lastSync < this.lastModified) { - this.lastSync = this.lastModified; - } - } - - // Apply remaining items. - doApplyBatchAndPersistFailed.call(this); - - count.newFailed = Utils.arraySub(this.previousFailed, failedInPreviousSync).length; - count.succeeded = Math.max(0, count.applied - count.failed); - this._log.info(["Records:", - count.applied, "applied,", - count.succeeded, "successfully,", - count.failed, "failed to apply,", - count.newFailed, "newly failed to apply,", - count.reconciled, "reconciled."].join(" ")); - Observers.notify("weave:engine:sync:applied", count, this.name); - }, - - /** - * Find a GUID of an item that is a duplicate of the incoming item but happens - * to have a different GUID - * - * @return GUID of the similar item; falsy otherwise - */ - _findDupe: function (item) { - // By default, assume there's no dupe items for the engine - }, - - _deleteId: function (id) { - this._tracker.removeChangedID(id); - - // Remember this id to delete at the end of sync - if (this._delete.ids == null) - this._delete.ids = [id]; - else - this._delete.ids.push(id); - }, - - /** - * Reconcile incoming record with local state. - * - * This function essentially determines whether to apply an incoming record. - * - * @param item - * Record from server to be tested for application. - * @return boolean - * Truthy if incoming record should be applied. False if not. - */ - _reconcile: function (item) { - if (this._log.level <= Log.Level.Trace) { - this._log.trace("Incoming: " + item); - } - - // We start reconciling by collecting a bunch of state. We do this here - // because some state may change during the course of this function and we - // need to operate on the original values. - let existsLocally = this._store.itemExists(item.id); - let locallyModified = item.id in this._modified; - - // TODO Handle clock drift better. Tracked in bug 721181. - let remoteAge = AsyncResource.serverTime - item.modified; - let localAge = locallyModified ? - (Date.now() / 1000 - this._modified[item.id]) : null; - let remoteIsNewer = remoteAge < localAge; - - this._log.trace("Reconciling " + item.id + ". exists=" + - existsLocally + "; modified=" + locallyModified + - "; local age=" + localAge + "; incoming age=" + - remoteAge); - - // We handle deletions first so subsequent logic doesn't have to check - // deleted flags. - if (item.deleted) { - // If the item doesn't exist locally, there is nothing for us to do. We - // can't check for duplicates because the incoming record has no data - // which can be used for duplicate detection. - if (!existsLocally) { - this._log.trace("Ignoring incoming item because it was deleted and " + - "the item does not exist locally."); - return false; - } - - // We decide whether to process the deletion by comparing the record - // ages. If the item is not modified locally, the remote side wins and - // the deletion is processed. If it is modified locally, we take the - // newer record. - if (!locallyModified) { - this._log.trace("Applying incoming delete because the local item " + - "exists and isn't modified."); - return true; - } - - // TODO As part of bug 720592, determine whether we should do more here. - // In the case where the local changes are newer, it is quite possible - // that the local client will restore data a remote client had tried to - // delete. There might be a good reason for that delete and it might be - // enexpected for this client to restore that data. - this._log.trace("Incoming record is deleted but we had local changes. " + - "Applying the youngest record."); - return remoteIsNewer; - } - - // At this point the incoming record is not for a deletion and must have - // data. If the incoming record does not exist locally, we check for a local - // duplicate existing under a different ID. The default implementation of - // _findDupe() is empty, so engines have to opt in to this functionality. - // - // If we find a duplicate, we change the local ID to the incoming ID and we - // refresh the metadata collected above. See bug 710448 for the history - // of this logic. - if (!existsLocally) { - let dupeID = this._findDupe(item); - if (dupeID) { - this._log.trace("Local item " + dupeID + " is a duplicate for " + - "incoming item " + item.id); - - // The local, duplicate ID is always deleted on the server. - this._deleteId(dupeID); - - // The current API contract does not mandate that the ID returned by - // _findDupe() actually exists. Therefore, we have to perform this - // check. - existsLocally = this._store.itemExists(dupeID); - - // We unconditionally change the item's ID in case the engine knows of - // an item but doesn't expose it through itemExists. If the API - // contract were stronger, this could be changed. - this._log.debug("Switching local ID to incoming: " + dupeID + " -> " + - item.id); - this._store.changeItemID(dupeID, item.id); - - // If the local item was modified, we carry its metadata forward so - // appropriate reconciling can be performed. - if (dupeID in this._modified) { - locallyModified = true; - localAge = Date.now() / 1000 - this._modified[dupeID]; - remoteIsNewer = remoteAge < localAge; - - this._modified[item.id] = this._modified[dupeID]; - delete this._modified[dupeID]; - } else { - locallyModified = false; - localAge = null; - } - - this._log.debug("Local item after duplication: age=" + localAge + - "; modified=" + locallyModified + "; exists=" + - existsLocally); - } else { - this._log.trace("No duplicate found for incoming item: " + item.id); - } - } - - // At this point we've performed duplicate detection. But, nothing here - // should depend on duplicate detection as the above should have updated - // state seamlessly. - - if (!existsLocally) { - // If the item doesn't exist locally and we have no local modifications - // to the item (implying that it was not deleted), always apply the remote - // item. - if (!locallyModified) { - this._log.trace("Applying incoming because local item does not exist " + - "and was not deleted."); - return true; - } - - // If the item was modified locally but isn't present, it must have - // been deleted. If the incoming record is younger, we restore from - // that record. - if (remoteIsNewer) { - this._log.trace("Applying incoming because local item was deleted " + - "before the incoming item was changed."); - delete this._modified[item.id]; - return true; - } - - this._log.trace("Ignoring incoming item because the local item's " + - "deletion is newer."); - return false; - } - - // If the remote and local records are the same, there is nothing to be - // done, so we don't do anything. In the ideal world, this logic wouldn't - // be here and the engine would take a record and apply it. The reason we - // want to defer this logic is because it would avoid a redundant and - // possibly expensive dip into the storage layer to query item state. - // This should get addressed in the async rewrite, so we ignore it for now. - let localRecord = this._createRecord(item.id); - let recordsEqual = Utils.deepEquals(item.cleartext, - localRecord.cleartext); - - // If the records are the same, we don't need to do anything. This does - // potentially throw away a local modification time. But, if the records - // are the same, does it matter? - if (recordsEqual) { - this._log.trace("Ignoring incoming item because the local item is " + - "identical."); - - delete this._modified[item.id]; - return false; - } - - // At this point the records are different. - - // If we have no local modifications, always take the server record. - if (!locallyModified) { - this._log.trace("Applying incoming record because no local conflicts."); - return true; - } - - // At this point, records are different and the local record is modified. - // We resolve conflicts by record age, where the newest one wins. This does - // result in data loss and should be handled by giving the engine an - // opportunity to merge the records. Bug 720592 tracks this feature. - this._log.warn("DATA LOSS: Both local and remote changes to record: " + - item.id); - return remoteIsNewer; - }, - - // Upload outgoing records. - _uploadOutgoing: function () { - this._log.trace("Uploading local changes to server."); - - let modifiedIDs = Object.keys(this._modified); - if (modifiedIDs.length) { - this._log.trace("Preparing " + modifiedIDs.length + - " outgoing records"); - - // collection we'll upload - let up = new Collection(this.engineURL, null, this.service); - let count = 0; - - // Upload what we've got so far in the collection - let doUpload = Utils.bind2(this, function(desc) { - this._log.info("Uploading " + desc + " of " + modifiedIDs.length + - " records"); - let resp = up.post(); - if (!resp.success) { - this._log.debug("Uploading records failed: " + resp); - resp.failureCode = ENGINE_UPLOAD_FAIL; - throw resp; - } - - // Update server timestamp from the upload. - let modified = resp.headers["x-weave-timestamp"]; - if (modified > this.lastSync) - this.lastSync = modified; - - let failed_ids = Object.keys(resp.obj.failed); - if (failed_ids.length) - this._log.debug("Records that will be uploaded again because " - + "the server couldn't store them: " - + failed_ids.join(", ")); - - // Clear successfully uploaded objects. - for (let id of resp.obj.success) { - delete this._modified[id]; - } - - up.clearRecords(); - }); - - for (let id of modifiedIDs) { - try { - let out = this._createRecord(id); - if (this._log.level <= Log.Level.Trace) - this._log.trace("Outgoing: " + out); - - out.encrypt(this.service.collectionKeys.keyForCollection(this.name)); - up.pushData(out); - } - catch(ex) { - this._log.warn("Error creating record: ", ex); - } - - // Partial upload - if ((++count % MAX_UPLOAD_RECORDS) == 0) - doUpload((count - MAX_UPLOAD_RECORDS) + " - " + count + " out"); - - this._store._sleep(0); - } - - // Final upload - if (count % MAX_UPLOAD_RECORDS > 0) - doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all"); - } - }, - - // Any cleanup necessary. - // Save the current snapshot so as to calculate changes at next sync - _syncFinish: function () { - this._log.trace("Finishing up sync"); - this._tracker.resetScore(); - - let doDelete = Utils.bind2(this, function(key, val) { - let coll = new Collection(this.engineURL, this._recordObj, this.service); - coll[key] = val; - coll.delete(); - }); - - for (let [key, val] of Object.entries(this._delete)) { - // Remove the key for future uses - delete this._delete[key]; - - // Send a simple delete for the property - if (key != "ids" || val.length <= 100) - doDelete(key, val); - else { - // For many ids, split into chunks of at most 100 - while (val.length > 0) { - doDelete(key, val.slice(0, 100)); - val = val.slice(100); - } - } - } - }, - - _syncCleanup: function () { - if (!this._modified) { - return; - } - - // Mark failed WBOs as changed again so they are reuploaded next time. - for (let [id, when] in Iterator(this._modified)) { - this._tracker.addChangedID(id, when); - } - this._modified = {}; - }, - - _sync: function () { - try { - this._syncStartup(); - Observers.notify("weave:engine:sync:status", "process-incoming"); - this._processIncoming(); - Observers.notify("weave:engine:sync:status", "upload-outgoing"); - this._uploadOutgoing(); - this._syncFinish(); - } finally { - this._syncCleanup(); - } - }, - - canDecrypt: function () { - // Report failure even if there's nothing to decrypt - let canDecrypt = false; - - // Fetch the most recently uploaded record and try to decrypt it - let test = new Collection(this.engineURL, this._recordObj, this.service); - test.limit = 1; - test.sort = "newest"; - test.full = true; - - let key = this.service.collectionKeys.keyForCollection(this.name); - test.recordHandler = function recordHandler(record) { - record.decrypt(key); - canDecrypt = true; - }.bind(this); - - // Any failure fetching/decrypting will just result in false - try { - this._log.trace("Trying to decrypt a record from the server.."); - test.get(); - } - catch(ex) { - this._log.debug("Failed test decrypt: ", ex); - } - - return canDecrypt; - }, - - _resetClient: function () { - this.resetLastSync(); - this.previousFailed = []; - this.toFetch = []; - }, - - wipeServer: function () { - let response = this.service.resource(this.engineURL).delete(); - if (response.status != 200 && response.status != 404) { - throw response; - } - this._resetClient(); - }, - - removeClientData: function () { - // Implement this method in engines that store client specific data - // on the server. - }, - - /* - * Decide on (and partially effect) an error-handling strategy. - * - * Asks the Service to respond to an HMAC error, which might result in keys - * being downloaded. That call returns true if an action which might allow a - * retry to occur. - * - * If `mayRetry` is truthy, and the Service suggests a retry, - * handleHMACMismatch returns kRecoveryStrategy.retry. Otherwise, it returns - * kRecoveryStrategy.error. - * - * Subclasses of SyncEngine can override this method to allow for different - * behavior -- e.g., to delete and ignore erroneous entries. - * - * All return values will be part of the kRecoveryStrategy enumeration. - */ - handleHMACMismatch: function (item, mayRetry) { - // By default we either try again, or bail out noisily. - return (this.service.handleHMACEvent() && mayRetry) ? - SyncEngine.kRecoveryStrategy.retry : - SyncEngine.kRecoveryStrategy.error; - } -}; diff --git a/components/weave/src/sync/identity.js b/components/weave/src/sync/identity.js deleted file mode 100644 index 795901f89..000000000 --- a/components/weave/src/sync/identity.js +++ /dev/null @@ -1,603 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["IdentityManager"]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/util.js"); - -// Lazy import to prevent unnecessary load on startup. -for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) { - XPCOMUtils.defineLazyModuleGetter(this, symbol, - "resource://services-sync/keys.js", - symbol); -} - -/** - * Manages identity and authentication for Sync. - * - * The following entities are managed: - * - * account - The main Sync/services account. This is typically an email - * address. - * username - A normalized version of your account. This is what's - * transmitted to the server. - * basic password - UTF-8 password used for authenticating when using HTTP - * basic authentication. - * sync key - The main encryption key used by Sync. - * sync key bundle - A representation of your sync key. - * - * When changes are made to entities that are stored in the password manager - * (basic password, sync key), those changes are merely staged. To commit them - * to the password manager, you'll need to call persistCredentials(). - * - * This type also manages authenticating Sync's network requests. Sync's - * network code calls into getRESTRequestAuthenticator and - * getResourceAuthenticator (depending on the network layer being used). Each - * returns a function which can be used to add authentication information to an - * outgoing request. - * - * In theory, this type supports arbitrary identity and authentication - * mechanisms. You can add support for them by monkeypatching the global - * instance of this type. Specifically, you'll need to redefine the - * aforementioned network code functions to do whatever your authentication - * mechanism needs them to do. In addition, you may wish to install custom - * functions to support your API. Although, that is certainly not required. - * If you do monkeypatch, please be advised that Sync expects the core - * attributes to have values. You will need to carry at least account and - * username forward. If you do not wish to support one of the built-in - * authentication mechanisms, you'll probably want to redefine currentAuthState - * and any other function that involves the built-in functionality. - */ -this.IdentityManager = function IdentityManager() { - this._log = Log.repository.getLogger("Sync.Identity"); - this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")]; - - this._basicPassword = null; - this._basicPasswordAllowLookup = true; - this._basicPasswordUpdated = false; - this._syncKey = null; - this._syncKeyAllowLookup = true; - this._syncKeySet = false; - this._syncKeyBundle = null; -} -IdentityManager.prototype = { - _log: null, - - _basicPassword: null, - _basicPasswordAllowLookup: true, - _basicPasswordUpdated: false, - - _syncKey: null, - _syncKeyAllowLookup: true, - _syncKeySet: false, - - _syncKeyBundle: null, - - /** - * Initialize the identity provider. Returns a promise that is resolved - * when initialization is complete and the provider can be queried for - * its state - */ - initialize: function() { - // Nothing to do for this identity provider. - return Promise.resolve(); - }, - - finalize: function() { - // Nothing to do for this identity provider. - return Promise.resolve(); - }, - - /** - * Called whenever Service.logout() is called. - */ - logout: function() { - // nothing to do for this identity provider. - }, - - /** - * Ensure the user is logged in. Returns a promise that resolves when - * the user is logged in, or is rejected if the login attempt has failed. - */ - ensureLoggedIn: function() { - // nothing to do for this identity provider - return Promise.resolve(); - }, - - /** - * Indicates if the identity manager is still initializing - */ - get readyToAuthenticate() { - // We initialize in a fully sync manner, so we are always finished. - return true; - }, - - get account() { - return Svc.Prefs.get("account", this.username); - }, - - /** - * Sets the active account name. - * - * This should almost always be called in favor of setting username, as - * username is derived from account. - * - * Changing the account name has the side-effect of wiping out stored - * credentials. Keep in mind that persistCredentials() will need to be called - * to flush the changes to disk. - * - * Set this value to null to clear out identity information. - */ - set account(value) { - if (value) { - value = value.toLowerCase(); - Svc.Prefs.set("account", value); - } else { - Svc.Prefs.reset("account"); - } - - this.username = this.usernameFromAccount(value); - }, - - get username() { - return Svc.Prefs.get("username", null); - }, - - /** - * Set the username value. - * - * Changing the username has the side-effect of wiping credentials. - */ - set username(value) { - if (value) { - value = value.toLowerCase(); - - if (value == this.username) { - return; - } - - Svc.Prefs.set("username", value); - } else { - Svc.Prefs.reset("username"); - } - - // If we change the username, we interpret this as a major change event - // and wipe out the credentials. - this._log.info("Username changed. Removing stored credentials."); - this.resetCredentials(); - }, - - /** - * Resets/Drops all credentials we hold for the current user. - */ - resetCredentials: function() { - this.basicPassword = null; - this.resetSyncKey(); - }, - - /** - * Resets/Drops the sync key we hold for the current user. - */ - resetSyncKey: function() { - this.syncKey = null; - // syncKeyBundle cleared as a result of setting syncKey. - }, - - /** - * Obtains the HTTP Basic auth password. - * - * Returns a string if set or null if it is not set. - */ - get basicPassword() { - if (this._basicPasswordAllowLookup) { - // We need a username to find the credentials. - let username = this.username; - if (!username) { - return null; - } - - for (let login of this._getLogins(PWDMGR_PASSWORD_REALM)) { - if (login.username.toLowerCase() == username) { - // It should already be UTF-8 encoded, but we don't take any chances. - this._basicPassword = Utils.encodeUTF8(login.password); - } - } - - this._basicPasswordAllowLookup = false; - } - - return this._basicPassword; - }, - - /** - * Set the HTTP basic password to use. - * - * Changes will not persist unless persistSyncCredentials() is called. - */ - set basicPassword(value) { - // Wiping out value. - if (!value) { - this._log.info("Basic password has no value. Removing."); - this._basicPassword = null; - this._basicPasswordUpdated = true; - this._basicPasswordAllowLookup = false; - return; - } - - let username = this.username; - if (!username) { - throw new Error("basicPassword cannot be set before username."); - } - - this._log.info("Basic password being updated."); - this._basicPassword = Utils.encodeUTF8(value); - this._basicPasswordUpdated = true; - }, - - /** - * Obtain the Sync Key. - * - * This returns a 26 character "friendly" Base32 encoded string on success or - * null if no Sync Key could be found. - * - * If the Sync Key hasn't been set in this session, this will look in the - * password manager for the sync key. - */ - get syncKey() { - if (this._syncKeyAllowLookup) { - let username = this.username; - if (!username) { - return null; - } - - for (let login of this._getLogins(PWDMGR_PASSPHRASE_REALM)) { - if (login.username.toLowerCase() == username) { - this._syncKey = login.password; - } - } - - this._syncKeyAllowLookup = false; - } - - return this._syncKey; - }, - - /** - * Set the active Sync Key. - * - * If being set to null, the Sync Key and its derived SyncKeyBundle are - * removed. However, the Sync Key won't be deleted from the password manager - * until persistSyncCredentials() is called. - * - * If a value is provided, it should be a 26 or 32 character "friendly" - * Base32 string for which Utils.isPassphrase() returns true. - * - * A side-effect of setting the Sync Key is that a SyncKeyBundle is - * generated. For historical reasons, this will silently error out if the - * value is not a proper Sync Key (!Utils.isPassphrase()). This should be - * fixed in the future (once service.js is more sane) to throw if the passed - * value is not valid. - */ - set syncKey(value) { - if (!value) { - this._log.info("Sync Key has no value. Deleting."); - this._syncKey = null; - this._syncKeyBundle = null; - this._syncKeyUpdated = true; - return; - } - - if (!this.username) { - throw new Error("syncKey cannot be set before username."); - } - - this._log.info("Sync Key being updated."); - this._syncKey = value; - - // Clear any cached Sync Key Bundle and regenerate it. - this._syncKeyBundle = null; - let bundle = this.syncKeyBundle; - - this._syncKeyUpdated = true; - }, - - /** - * Obtain the active SyncKeyBundle. - * - * This returns a SyncKeyBundle representing a key pair derived from the - * Sync Key on success. If no Sync Key is present or if the Sync Key is not - * valid, this returns null. - * - * The SyncKeyBundle should be treated as immutable. - */ - get syncKeyBundle() { - // We can't obtain a bundle without a username set. - if (!this.username) { - this._log.warn("Attempted to obtain Sync Key Bundle with no username set!"); - return null; - } - - if (!this.syncKey) { - this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " + - "set!"); - return null; - } - - if (!this._syncKeyBundle) { - try { - this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey); - } catch (ex) { - this._log.warn("Failed to create sync key bundle", ex); - return null; - } - } - - return this._syncKeyBundle; - }, - - /** - * The current state of the auth credentials. - * - * This essentially validates that enough credentials are available to use - * Sync. - */ - get currentAuthState() { - if (!this.username) { - return LOGIN_FAILED_NO_USERNAME; - } - - if (Utils.mpLocked()) { - return STATUS_OK; - } - - if (!this.basicPassword) { - return LOGIN_FAILED_NO_PASSWORD; - } - - if (!this.syncKey) { - return LOGIN_FAILED_NO_PASSPHRASE; - } - - // If we have a Sync Key but no bundle, bundle creation failed, which - // implies a bad Sync Key. - if (!this.syncKeyBundle) { - return LOGIN_FAILED_INVALID_PASSPHRASE; - } - - return STATUS_OK; - }, - - /** - * Verify the current auth state, unlocking the master-password if necessary. - * - * Returns a promise that resolves with the current auth state after - * attempting to unlock. - */ - unlockAndVerifyAuthState: function() { - // Try to fetch the passphrase - this will prompt for MP unlock as a - // side-effect... - try { - this.syncKey; - } catch (ex) { - this._log.debug("Fetching passphrase threw " + ex + - "; assuming master password locked."); - return Promise.resolve(MASTER_PASSWORD_LOCKED); - } - return Promise.resolve(STATUS_OK); - }, - - /** - * Persist credentials to password store. - * - * When credentials are updated, they are changed in memory only. This will - * need to be called to save them to the underlying password store. - * - * If the password store is locked (e.g. if the master password hasn't been - * entered), this could throw an exception. - */ - persistCredentials: function persistCredentials(force) { - if (this._basicPasswordUpdated || force) { - if (this._basicPassword) { - this._setLogin(PWDMGR_PASSWORD_REALM, this.username, - this._basicPassword); - } else { - for (let login of this._getLogins(PWDMGR_PASSWORD_REALM)) { - Services.logins.removeLogin(login); - } - } - - this._basicPasswordUpdated = false; - } - - if (this._syncKeyUpdated || force) { - if (this._syncKey) { - this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey); - } else { - for (let login of this._getLogins(PWDMGR_PASSPHRASE_REALM)) { - Services.logins.removeLogin(login); - } - } - - this._syncKeyUpdated = false; - } - - }, - - /** - * Deletes the Sync Key from the system. - */ - deleteSyncKey: function deleteSyncKey() { - this.syncKey = null; - this.persistCredentials(); - }, - - hasBasicCredentials: function hasBasicCredentials() { - // Because JavaScript. - return this.username && this.basicPassword && true; - }, - - /** - * Pre-fetches any information that might help with migration away from this - * identity. Called after every sync and is really just an optimization that - * allows us to avoid a network request for when we actually need the - * migration info. - */ - prefetchMigrationSentinel: function(service) { - // Try and fetch the migration sentinel - it will end up in the recordManager - // cache. - try { - service.recordManager.get(service.storageURL + "meta/fxa_credentials"); - } catch (ex) { - this._log.warn("Failed to pre-fetch the migration sentinel", ex); - } - }, - - /** - * Obtains the array of basic logins from nsiPasswordManager. - */ - _getLogins: function _getLogins(realm) { - return Services.logins.findLogins({}, PWDMGR_HOST, null, realm); - }, - - /** - * Set a login in the password manager. - * - * This has the side-effect of deleting any other logins for the specified - * realm. - */ - _setLogin: function _setLogin(realm, username, password) { - let exists = false; - for (let login of this._getLogins(realm)) { - if (login.username == username && login.password == password) { - exists = true; - } else { - this._log.debug("Pruning old login for " + username + " from " + realm); - Services.logins.removeLogin(login); - } - } - - if (exists) { - return; - } - - this._log.debug("Updating saved password for " + username + " in " + - realm); - - let loginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); - let login = new loginInfo(PWDMGR_HOST, null, realm, username, - password, "", ""); - Services.logins.addLogin(login); - }, - - /** - * Return credentials hosts for this identity only. - */ - _getSyncCredentialsHosts: function() { - return Utils.getSyncCredentialsHostsLegacy(); - }, - - /** - * Deletes Sync credentials from the password manager. - */ - deleteSyncCredentials: function deleteSyncCredentials() { - for (let host of this._getSyncCredentialsHosts()) { - let logins = Services.logins.findLogins({}, host, "", ""); - for (let login of logins) { - Services.logins.removeLogin(login); - } - } - - // Wait until after store is updated in case it fails. - this._basicPassword = null; - this._basicPasswordAllowLookup = true; - this._basicPasswordUpdated = false; - - this._syncKey = null; - // this._syncKeyBundle is nullified as part of _syncKey setter. - this._syncKeyAllowLookup = true; - this._syncKeyUpdated = false; - }, - - usernameFromAccount: function usernameFromAccount(value) { - // If we encounter characters not allowed by the API (as found for - // instance in an email address), hash the value. - if (value && value.match(/[^A-Z0-9._-]/i)) { - return Utils.sha1Base32(value.toLowerCase()).toLowerCase(); - } - - return value ? value.toLowerCase() : value; - }, - - /** - * Obtain a function to be used for adding auth to Resource HTTP requests. - */ - getResourceAuthenticator: function getResourceAuthenticator() { - if (this.hasBasicCredentials()) { - return this._onResourceRequestBasic.bind(this); - } - - return null; - }, - - /** - * Helper method to return an authenticator for basic Resource requests. - */ - getBasicResourceAuthenticator: - function getBasicResourceAuthenticator(username, password) { - - return function basicAuthenticator(resource) { - let value = "Basic " + btoa(username + ":" + password); - return {headers: {authorization: value}}; - }; - }, - - _onResourceRequestBasic: function _onResourceRequestBasic(resource) { - let value = "Basic " + btoa(this.username + ":" + this.basicPassword); - return {headers: {authorization: value}}; - }, - - _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) { - // TODO Get identifier and key from somewhere. - let identifier; - let key; - let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri); - - return {headers: {authorization: result.header}}; - }, - - /** - * Obtain a function to be used for adding auth to RESTRequest instances. - */ - getRESTRequestAuthenticator: function getRESTRequestAuthenticator() { - if (this.hasBasicCredentials()) { - return this.onRESTRequestBasic.bind(this); - } - - return null; - }, - - onRESTRequestBasic: function onRESTRequestBasic(request) { - let up = this.username + ":" + this.basicPassword; - request.setHeader("authorization", "Basic " + btoa(up)); - }, - - createClusterManager: function(service) { - Cu.import("resource://services-sync/stages/cluster.js"); - return new ClusterManager(service); - }, - - offerSyncOptions: function () { - // Do nothing for Sync 1.1. - return {accepted: true}; - }, -}; diff --git a/components/weave/src/sync/jpakeclient.js b/components/weave/src/sync/jpakeclient.js deleted file mode 100644 index 625dc91b6..000000000 --- a/components/weave/src/sync/jpakeclient.js +++ /dev/null @@ -1,773 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["JPAKEClient", "SendCredentialsController"]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -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/util.js"); - -const REQUEST_TIMEOUT = 60; // 1 minute -const KEYEXCHANGE_VERSION = 3; - -const JPAKE_SIGNERID_SENDER = "sender"; -const JPAKE_SIGNERID_RECEIVER = "receiver"; -const JPAKE_LENGTH_SECRET = 8; -const JPAKE_LENGTH_CLIENTID = 256; -const JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; - - -/** - * Client to exchange encrypted data using the J-PAKE algorithm. - * The exchange between two clients of this type looks like this: - * - * - * Mobile Server Desktop - * =================================================================== - * | - * retrieve channel <---------------| - * generate random secret | - * show PIN = secret + channel | ask user for PIN - * upload Mobile's message 1 ------>| - * |----> retrieve Mobile's message 1 - * |<----- upload Desktop's message 1 - * retrieve Desktop's message 1 <---| - * upload Mobile's message 2 ------>| - * |----> retrieve Mobile's message 2 - * | compute key - * |<----- upload Desktop's message 2 - * retrieve Desktop's message 2 <---| - * compute key | - * encrypt known value ------------>| - * |-------> retrieve encrypted value - * | verify against local known value - * - * At this point Desktop knows whether the PIN was entered correctly. - * If it wasn't, Desktop deletes the session. If it was, the account - * setup can proceed. If Desktop doesn't yet have an account set up, - * it will keep the channel open and let the user connect to or - * create an account. - * - * | encrypt credentials - * |<------------- upload credentials - * retrieve credentials <-----------| - * verify HMAC | - * decrypt credentials | - * delete session ----------------->| - * start syncing | - * - * - * Create a client object like so: - * - * let client = new JPAKEClient(controller); - * - * The 'controller' object must implement the following methods: - * - * displayPIN(pin) -- Called when a PIN has been generated and is ready to - * be displayed to the user. Only called on the client where the pairing - * was initiated with 'receiveNoPIN()'. - * - * onPairingStart() -- Called when the pairing has started and messages are - * being sent back and forth over the channel. Only called on the client - * where the pairing was initiated with 'receiveNoPIN()'. - * - * onPaired() -- Called when the device pairing has been established and - * we're ready to send the credentials over. To do that, the controller - * must call 'sendAndComplete()' while the channel is active. - * - * onComplete(data) -- Called after transfer has been completed. On - * the sending side this is called with no parameter and as soon as the - * data has been uploaded. This does not mean the receiving side has - * actually retrieved them yet. - * - * onAbort(error) -- Called whenever an error is encountered. All errors lead - * to an abort and the process has to be started again on both sides. - * - * To start the data transfer on the receiving side, call - * - * client.receiveNoPIN(); - * - * This will allocate a new channel on the server, generate a PIN, have it - * displayed and then do the transfer once the protocol has been completed - * with the sending side. - * - * To initiate the transfer from the sending side, call - * - * client.pairWithPIN(pin, true); - * - * Once the pairing has been established, the controller's 'onPaired()' method - * will be called. To then transmit the data, call - * - * client.sendAndComplete(data); - * - * To abort the process, call - * - * client.abort(); - * - * Note that after completion or abort, the 'client' instance may not be reused. - * You will have to create a new one in case you'd like to restart the process. - */ -this.JPAKEClient = function JPAKEClient(controller) { - this.controller = controller; - - this._log = Log.repository.getLogger("Sync.JPAKEClient"); - this._log.level = Log.Level[Svc.Prefs.get( - "log.logger.service.jpakeclient", "Debug")]; - - this._serverURL = Svc.Prefs.get("jpake.serverURL"); - this._pollInterval = Svc.Prefs.get("jpake.pollInterval"); - this._maxTries = Svc.Prefs.get("jpake.maxTries"); - if (this._serverURL.slice(-1) != "/") { - this._serverURL += "/"; - } - - this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"] - .createInstance(Ci.nsISyncJPAKE); - - this._setClientID(); -} -JPAKEClient.prototype = { - - _chain: Async.chain, - - /* - * Public API - */ - - /** - * Initiate pairing and receive data without providing a PIN. The PIN will - * be generated and passed on to the controller to be displayed to the user. - * - * This is typically called on mobile devices where typing is tedious. - */ - receiveNoPIN: function receiveNoPIN() { - this._my_signerid = JPAKE_SIGNERID_RECEIVER; - this._their_signerid = JPAKE_SIGNERID_SENDER; - - this._secret = this._createSecret(); - - // Allow a large number of tries first while we wait for the PIN - // to be entered on the other device. - this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries"); - this._chain(this._getChannel, - this._computeStepOne, - this._putStep, - this._getStep, - function(callback) { - // We fetched the first response from the other client. - // Notify controller of the pairing starting. - Utils.nextTick(this.controller.onPairingStart, - this.controller); - - // Now we can switch back to the smaller timeout. - this._maxTries = Svc.Prefs.get("jpake.maxTries"); - callback(); - }, - this._computeStepTwo, - this._putStep, - this._getStep, - this._computeFinal, - this._computeKeyVerification, - this._putStep, - function(callback) { - // Allow longer time-out for the last message. - this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries"); - callback(); - }, - this._getStep, - this._decryptData, - this._complete)(); - }, - - /** - * Initiate pairing based on the PIN entered by the user. - * - * This is typically called on desktop devices where typing is easier than - * on mobile. - * - * @param pin - * 12 character string (in human-friendly base32) containing the PIN - * entered by the user. - * @param expectDelay - * Flag that indicates that a significant delay between the pairing - * and the sending should be expected. v2 and earlier of the protocol - * did not allow for this and the pairing to a v2 or earlier client - * will be aborted if this flag is 'true'. - */ - pairWithPIN: function pairWithPIN(pin, expectDelay) { - this._my_signerid = JPAKE_SIGNERID_SENDER; - this._their_signerid = JPAKE_SIGNERID_RECEIVER; - - this._channel = pin.slice(JPAKE_LENGTH_SECRET); - this._channelURL = this._serverURL + this._channel; - this._secret = pin.slice(0, JPAKE_LENGTH_SECRET); - - this._chain(this._computeStepOne, - this._getStep, - function (callback) { - // Ensure that the other client can deal with a delay for - // the last message if that's requested by the caller. - if (!expectDelay) { - return callback(); - } - if (!this._incoming.version || this._incoming.version < 3) { - return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED); - } - return callback(); - }, - this._putStep, - this._computeStepTwo, - this._getStep, - this._putStep, - this._computeFinal, - this._getStep, - this._verifyPairing)(); - }, - - /** - * Send data after a successful pairing. - * - * @param obj - * Object containing the data to send. It will be serialized as JSON. - */ - sendAndComplete: function sendAndComplete(obj) { - if (!this._paired || this._finished) { - this._log.error("Can't send data, no active pairing!"); - throw "No active pairing!"; - } - this._data = JSON.stringify(obj); - this._chain(this._encryptData, - this._putStep, - this._complete)(); - }, - - /** - * Abort the current pairing. The channel on the server will be deleted - * if the abort wasn't due to a network or server error. The controller's - * 'onAbort()' method is notified in all cases. - * - * @param error [optional] - * Error constant indicating the reason for the abort. Defaults to - * user abort. - */ - abort: function abort(error) { - this._log.debug("Aborting..."); - this._finished = true; - let self = this; - - // Default to "user aborted". - if (!error) { - error = JPAKE_ERROR_USERABORT; - } - - if (error == JPAKE_ERROR_CHANNEL || - error == JPAKE_ERROR_NETWORK || - error == JPAKE_ERROR_NODATA) { - Utils.nextTick(function() { this.controller.onAbort(error); }, this); - } else { - this._reportFailure(error, function() { self.controller.onAbort(error); }); - } - }, - - /* - * Utilities - */ - - _setClientID: function _setClientID() { - let rng = Cc["@mozilla.org/security/random-generator;1"] - .createInstance(Ci.nsIRandomGenerator); - let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2); - this._clientID = bytes.map(byte => ("0" + byte.toString(16)).slice(-2)).join(""); - }, - - _createSecret: function _createSecret() { - // 0-9a-z without 1,l,o,0 - const key = "23456789abcdefghijkmnpqrstuvwxyz"; - let rng = Cc["@mozilla.org/security/random-generator;1"] - .createInstance(Ci.nsIRandomGenerator); - let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET); - return bytes.map(byte => key[Math.floor(byte * key.length / 256)]).join(""); - }, - - _newRequest: function _newRequest(uri) { - let request = new RESTRequest(uri); - request.setHeader("X-KeyExchange-Id", this._clientID); - request.timeout = REQUEST_TIMEOUT; - return request; - }, - - /* - * Steps of J-PAKE procedure - */ - - _getChannel: function _getChannel(callback) { - this._log.trace("Requesting channel."); - let request = this._newRequest(this._serverURL + "new_channel"); - request.get(Utils.bind2(this, function handleChannel(error) { - if (this._finished) { - return; - } - - if (error) { - this._log.error("Error acquiring channel ID. " + error); - this.abort(JPAKE_ERROR_CHANNEL); - return; - } - if (request.response.status != 200) { - this._log.error("Error acquiring channel ID. Server responded with HTTP " - + request.response.status); - this.abort(JPAKE_ERROR_CHANNEL); - return; - } - - try { - this._channel = JSON.parse(request.response.body); - } catch (ex) { - this._log.error("Server responded with invalid JSON."); - this.abort(JPAKE_ERROR_CHANNEL); - return; - } - this._log.debug("Using channel " + this._channel); - this._channelURL = this._serverURL + this._channel; - - // Don't block on UI code. - let pin = this._secret + this._channel; - Utils.nextTick(function() { this.controller.displayPIN(pin); }, this); - callback(); - })); - }, - - // Generic handler for uploading data. - _putStep: function _putStep(callback) { - this._log.trace("Uploading message " + this._outgoing.type); - let request = this._newRequest(this._channelURL); - if (this._their_etag) { - request.setHeader("If-Match", this._their_etag); - } else { - request.setHeader("If-None-Match", "*"); - } - request.put(this._outgoing, Utils.bind2(this, function (error) { - if (this._finished) { - return; - } - - if (error) { - this._log.error("Error uploading data. " + error); - this.abort(JPAKE_ERROR_NETWORK); - return; - } - if (request.response.status != 200) { - this._log.error("Could not upload data. Server responded with HTTP " - + request.response.status); - this.abort(JPAKE_ERROR_SERVER); - return; - } - // There's no point in returning early here since the next step will - // always be a GET so let's pause for twice the poll interval. - this._my_etag = request.response.headers["etag"]; - Utils.namedTimer(function () { callback(); }, this._pollInterval * 2, - this, "_pollTimer"); - })); - }, - - // Generic handler for polling for and retrieving data. - _pollTries: 0, - _getStep: function _getStep(callback) { - this._log.trace("Retrieving next message."); - let request = this._newRequest(this._channelURL); - if (this._my_etag) { - request.setHeader("If-None-Match", this._my_etag); - } - - request.get(Utils.bind2(this, function (error) { - if (this._finished) { - return; - } - - if (error) { - this._log.error("Error fetching data. " + error); - this.abort(JPAKE_ERROR_NETWORK); - return; - } - - if (request.response.status == 304) { - this._log.trace("Channel hasn't been updated yet. Will try again later."); - if (this._pollTries >= this._maxTries) { - this._log.error("Tried for " + this._pollTries + " times, aborting."); - this.abort(JPAKE_ERROR_TIMEOUT); - return; - } - this._pollTries += 1; - Utils.namedTimer(function() { this._getStep(callback); }, - this._pollInterval, this, "_pollTimer"); - return; - } - this._pollTries = 0; - - if (request.response.status == 404) { - this._log.error("No data found in the channel."); - this.abort(JPAKE_ERROR_NODATA); - return; - } - if (request.response.status != 200) { - this._log.error("Could not retrieve data. Server responded with HTTP " - + request.response.status); - this.abort(JPAKE_ERROR_SERVER); - return; - } - - this._their_etag = request.response.headers["etag"]; - if (!this._their_etag) { - this._log.error("Server did not supply ETag for message: " - + request.response.body); - this.abort(JPAKE_ERROR_SERVER); - return; - } - - try { - this._incoming = JSON.parse(request.response.body); - } catch (ex) { - this._log.error("Server responded with invalid JSON."); - this.abort(JPAKE_ERROR_INVALID); - return; - } - this._log.trace("Fetched message " + this._incoming.type); - callback(); - })); - }, - - _reportFailure: function _reportFailure(reason, callback) { - this._log.debug("Reporting failure to server."); - let request = this._newRequest(this._serverURL + "report"); - request.setHeader("X-KeyExchange-Cid", this._channel); - request.setHeader("X-KeyExchange-Log", reason); - request.post("", Utils.bind2(this, function (error) { - if (error) { - this._log.warn("Report failed: " + error); - } else if (request.response.status != 200) { - this._log.warn("Report failed. Server responded with HTTP " - + request.response.status); - } - - // Do not block on errors, we're done or aborted by now anyway. - callback(); - })); - }, - - _computeStepOne: function _computeStepOne(callback) { - this._log.trace("Computing round 1."); - let gx1 = {}; - let gv1 = {}; - let r1 = {}; - let gx2 = {}; - let gv2 = {}; - let r2 = {}; - try { - this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2); - } catch (ex) { - this._log.error("JPAKE round 1 threw: " + ex); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - let one = {gx1: gx1.value, - gx2: gx2.value, - zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid}, - zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}}; - this._outgoing = {type: this._my_signerid + "1", - version: KEYEXCHANGE_VERSION, - payload: one}; - this._log.trace("Generated message " + this._outgoing.type); - callback(); - }, - - _computeStepTwo: function _computeStepTwo(callback) { - this._log.trace("Computing round 2."); - if (this._incoming.type != this._their_signerid + "1") { - this._log.error("Invalid round 1 message: " - + JSON.stringify(this._incoming)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - - let step1 = this._incoming.payload; - if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid - || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) { - this._log.error("Invalid round 1 payload: " + JSON.stringify(step1)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - - let A = {}; - let gvA = {}; - let rA = {}; - - try { - this._jpake.round2(this._their_signerid, this._secret, - step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b, - step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b, - A, gvA, rA); - } catch (ex) { - this._log.error("JPAKE round 2 threw: " + ex); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - let two = {A: A.value, - zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}}; - this._outgoing = {type: this._my_signerid + "2", - version: KEYEXCHANGE_VERSION, - payload: two}; - this._log.trace("Generated message " + this._outgoing.type); - callback(); - }, - - _computeFinal: function _computeFinal(callback) { - if (this._incoming.type != this._their_signerid + "2") { - this._log.error("Invalid round 2 message: " - + JSON.stringify(this._incoming)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - - let step2 = this._incoming.payload; - if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) { - this._log.error("Invalid round 2 payload: " + JSON.stringify(step1)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - - let aes256Key = {}; - let hmac256Key = {}; - - try { - this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT, - aes256Key, hmac256Key); - } catch (ex) { - this._log.error("JPAKE final round threw: " + ex); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - - this._crypto_key = aes256Key.value; - let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value)); - this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key); - - callback(); - }, - - _computeKeyVerification: function _computeKeyVerification(callback) { - this._log.trace("Encrypting key verification value."); - let iv, ciphertext; - try { - iv = Svc.Crypto.generateRandomIV(); - ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE, - this._crypto_key, iv); - } catch (ex) { - this._log.error("Failed to encrypt key verification value."); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - this._outgoing = {type: this._my_signerid + "3", - version: KEYEXCHANGE_VERSION, - payload: {ciphertext: ciphertext, IV: iv}}; - this._log.trace("Generated message " + this._outgoing.type); - callback(); - }, - - _verifyPairing: function _verifyPairing(callback) { - this._log.trace("Verifying their key."); - if (this._incoming.type != this._their_signerid + "3") { - this._log.error("Invalid round 3 data: " + - JSON.stringify(this._incoming)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - let step3 = this._incoming.payload; - let ciphertext; - try { - ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE, - this._crypto_key, step3.IV); - if (ciphertext != step3.ciphertext) { - throw "Key mismatch!"; - } - } catch (ex) { - this._log.error("Keys don't match!"); - this.abort(JPAKE_ERROR_KEYMISMATCH); - return; - } - - this._log.debug("Verified pairing!"); - this._paired = true; - Utils.nextTick(function () { this.controller.onPaired(); }, this); - callback(); - }, - - _encryptData: function _encryptData(callback) { - this._log.trace("Encrypting data."); - let iv, ciphertext, hmac; - try { - iv = Svc.Crypto.generateRandomIV(); - ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv); - hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher)); - } catch (ex) { - this._log.error("Failed to encrypt data."); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - this._outgoing = {type: this._my_signerid + "3", - version: KEYEXCHANGE_VERSION, - payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}}; - this._log.trace("Generated message " + this._outgoing.type); - callback(); - }, - - _decryptData: function _decryptData(callback) { - this._log.trace("Verifying their key."); - if (this._incoming.type != this._their_signerid + "3") { - this._log.error("Invalid round 3 data: " - + JSON.stringify(this._incoming)); - this.abort(JPAKE_ERROR_WRONGMESSAGE); - return; - } - let step3 = this._incoming.payload; - try { - let hmac = Utils.bytesAsHex( - Utils.digestUTF8(step3.ciphertext, this._hmac_hasher)); - if (hmac != step3.hmac) { - throw "HMAC validation failed!"; - } - } catch (ex) { - this._log.error("HMAC validation failed."); - this.abort(JPAKE_ERROR_KEYMISMATCH); - return; - } - - this._log.trace("Decrypting data."); - let cleartext; - try { - cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key, - step3.IV); - } catch (ex) { - this._log.error("Failed to decrypt data."); - this.abort(JPAKE_ERROR_INTERNAL); - return; - } - - try { - this._newData = JSON.parse(cleartext); - } catch (ex) { - this._log.error("Invalid data data: " + JSON.stringify(cleartext)); - this.abort(JPAKE_ERROR_INVALID); - return; - } - - this._log.trace("Decrypted data."); - callback(); - }, - - _complete: function _complete() { - this._log.debug("Exchange completed."); - this._finished = true; - Utils.nextTick(function () { this.controller.onComplete(this._newData); }, - this); - } - -}; - - -/** - * Send credentials over an active J-PAKE channel. - * - * This object is designed to take over as the JPAKEClient controller, - * presumably replacing one that is UI-based which would either cause - * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM - * context disappears. This object stays alive for the duration of the - * transfer by being strong-ref'ed as an nsIObserver. - * - * Credentials are sent after the first sync has been completed - * (successfully or not.) - * - * Usage: - * - * jpakeclient.controller = new SendCredentialsController(jpakeclient, - * service); - * - */ -this.SendCredentialsController = - function SendCredentialsController(jpakeclient, service) { - this._log = Log.repository.getLogger("Sync.SendCredentialsController"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; - - this._log.trace("Loading."); - this.jpakeclient = jpakeclient; - this.service = service; - - // Register ourselves as observers the first Sync finishing (either - // successfully or unsuccessfully, we don't care) or for removing - // this device's sync configuration, in case that happens while we - // haven't finished the first sync yet. - Services.obs.addObserver(this, "weave:service:sync:finish", false); - Services.obs.addObserver(this, "weave:service:sync:error", false); - Services.obs.addObserver(this, "weave:service:start-over", false); -} -SendCredentialsController.prototype = { - - unload: function unload() { - this._log.trace("Unloading."); - try { - Services.obs.removeObserver(this, "weave:service:sync:finish"); - Services.obs.removeObserver(this, "weave:service:sync:error"); - Services.obs.removeObserver(this, "weave:service:start-over"); - } catch (ex) { - // Ignore. - } - }, - - observe: function observe(subject, topic, data) { - switch (topic) { - case "weave:service:sync:finish": - case "weave:service:sync:error": - Utils.nextTick(this.sendCredentials, this); - break; - case "weave:service:start-over": - // This will call onAbort which will call unload(). - this.jpakeclient.abort(); - break; - } - }, - - sendCredentials: function sendCredentials() { - this._log.trace("Sending credentials."); - let credentials = {account: this.service.identity.account, - password: this.service.identity.basicPassword, - synckey: this.service.identity.syncKey, - serverURL: this.service.serverURL}; - this.jpakeclient.sendAndComplete(credentials); - }, - - // JPAKEClient controller API - - onComplete: function onComplete() { - this._log.debug("Exchange was completed successfully!"); - this.unload(); - - // Schedule a Sync for soonish to fetch the data uploaded by the - // device with which we just paired. - this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval); - }, - - onAbort: function onAbort(error) { - // It doesn't really matter why we aborted, but the channel is closed - // for sure, so we won't be able to do anything with it. - this._log.debug("Exchange was aborted with error: " + error); - this.unload(); - }, - - // Irrelevant methods for this controller: - displayPIN: function displayPIN() {}, - onPairingStart: function onPairingStart() {}, - onPaired: function onPaired() {}, -}; diff --git a/components/weave/src/sync/keys.js b/components/weave/src/sync/keys.js deleted file mode 100644 index b93de7f31..000000000 --- a/components/weave/src/sync/keys.js +++ /dev/null @@ -1,214 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "BulkKeyBundle", - "SyncKeyBundle" -]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/util.js"); - -/** - * Represents a pair of keys. - * - * Each key stored in a key bundle is 256 bits. One key is used for symmetric - * encryption. The other is used for HMAC. - * - * A KeyBundle by itself is just an anonymous pair of keys. Other types - * deriving from this one add semantics, such as associated collections or - * generating a key bundle via HKDF from another key. - */ -function KeyBundle() { - this._encrypt = null; - this._encryptB64 = null; - this._hmac = null; - this._hmacB64 = null; - this._hmacObj = null; - this._sha256HMACHasher = null; -} -KeyBundle.prototype = { - _encrypt: null, - _encryptB64: null, - _hmac: null, - _hmacB64: null, - _hmacObj: null, - _sha256HMACHasher: null, - - equals: function equals(bundle) { - return bundle && - (bundle.hmacKey == this.hmacKey) && - (bundle.encryptionKey == this.encryptionKey); - }, - - /* - * Accessors for the two keys. - */ - get encryptionKey() { - return this._encrypt; - }, - - set encryptionKey(value) { - if (!value || typeof value != "string") { - throw new Error("Encryption key can only be set to string values."); - } - - if (value.length < 16) { - throw new Error("Encryption key must be at least 128 bits long."); - } - - this._encrypt = value; - this._encryptB64 = btoa(value); - }, - - get encryptionKeyB64() { - return this._encryptB64; - }, - - get hmacKey() { - return this._hmac; - }, - - set hmacKey(value) { - if (!value || typeof value != "string") { - throw new Error("HMAC key can only be set to string values."); - } - - if (value.length < 16) { - throw new Error("HMAC key must be at least 128 bits long."); - } - - this._hmac = value; - this._hmacB64 = btoa(value); - this._hmacObj = value ? Utils.makeHMACKey(value) : null; - this._sha256HMACHasher = value ? Utils.makeHMACHasher( - Ci.nsICryptoHMAC.SHA256, this._hmacObj) : null; - }, - - get hmacKeyB64() { - return this._hmacB64; - }, - - get hmacKeyObject() { - return this._hmacObj; - }, - - get sha256HMACHasher() { - return this._sha256HMACHasher; - }, - - /** - * Populate this key pair with 2 new, randomly generated keys. - */ - generateRandom: function generateRandom() { - let generatedHMAC = Svc.Crypto.generateRandomKey(); - let generatedEncr = Svc.Crypto.generateRandomKey(); - this.keyPairB64 = [generatedEncr, generatedHMAC]; - }, - -}; - -/** - * Represents a KeyBundle associated with a collection. - * - * This is just a KeyBundle with a collection attached. - */ -this.BulkKeyBundle = function BulkKeyBundle(collection) { - let log = Log.repository.getLogger("Sync.BulkKeyBundle"); - log.info("BulkKeyBundle being created for " + collection); - KeyBundle.call(this); - - this._collection = collection; -} - -BulkKeyBundle.prototype = { - __proto__: KeyBundle.prototype, - - get collection() { - return this._collection; - }, - - /** - * Obtain the key pair in this key bundle. - * - * The returned keys are represented as raw byte strings. - */ - get keyPair() { - return [this.encryptionKey, this.hmacKey]; - }, - - set keyPair(value) { - if (!Array.isArray(value) || value.length != 2) { - throw new Error("BulkKeyBundle.keyPair value must be array of 2 keys."); - } - - this.encryptionKey = value[0]; - this.hmacKey = value[1]; - }, - - get keyPairB64() { - return [this.encryptionKeyB64, this.hmacKeyB64]; - }, - - set keyPairB64(value) { - if (!Array.isArray(value) || value.length != 2) { - throw new Error("BulkKeyBundle.keyPairB64 value must be an array of 2 " + - "keys."); - } - - this.encryptionKey = Utils.safeAtoB(value[0]); - this.hmacKey = Utils.safeAtoB(value[1]); - }, -}; - -/** - * Represents a key pair derived from a Sync Key via HKDF. - * - * Instances of this type should be considered immutable. You create an - * instance by specifying the username and 26 character "friendly" Base32 - * encoded Sync Key. The Sync Key is derived at instance creation time. - * - * If the username or Sync Key is invalid, an Error will be thrown. - */ -this.SyncKeyBundle = function SyncKeyBundle(username, syncKey) { - let log = Log.repository.getLogger("Sync.SyncKeyBundle"); - log.info("SyncKeyBundle being created."); - KeyBundle.call(this); - - this.generateFromKey(username, syncKey); -} -SyncKeyBundle.prototype = { - __proto__: KeyBundle.prototype, - - /* - * If we've got a string, hash it into keys and store them. - */ - generateFromKey: function generateFromKey(username, syncKey) { - if (!username || (typeof username != "string")) { - throw new Error("Sync Key cannot be generated from non-string username."); - } - - if (!syncKey || (typeof syncKey != "string")) { - throw new Error("Sync Key cannot be generated from non-string key."); - } - - if (!Utils.isPassphrase(syncKey)) { - throw new Error("Provided key is not a passphrase, cannot derive Sync " + - "Key Bundle."); - } - - // Expand the base32 Sync Key to an AES 256 and 256 bit HMAC key. - let prk = Utils.decodeKeyBase32(syncKey); - let info = HMAC_INPUT + username; - let okm = Utils.hkdfExpand(prk, info, 32 * 2); - this.encryptionKey = okm.slice(0, 32); - this.hmacKey = okm.slice(32, 64); - }, -}; - diff --git a/components/weave/src/sync/main.js b/components/weave/src/sync/main.js deleted file mode 100644 index e8e705e72..000000000 --- a/components/weave/src/sync/main.js +++ /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/. */ - -this.EXPORTED_SYMBOLS = ['Weave']; - -this.Weave = {}; -Components.utils.import("resource://services-sync/constants.js", Weave); -var lazies = { - "jpakeclient.js": ["JPAKEClient", "SendCredentialsController"], - "notifications.js": ["Notifications", "Notification", "NotificationButton"], - "service.js": ["Service"], - "status.js": ["Status"], - "util.js": ['Utils', 'Svc'] -}; - -function lazyImport(module, dest, props) { - function getter(prop) { - return function() { - let ns = {}; - Components.utils.import(module, ns); - delete dest[prop]; - return dest[prop] = ns[prop]; - }; - } - props.forEach(function (prop) { dest.__defineGetter__(prop, getter(prop)); }); -} - -for (let mod in lazies) { - lazyImport("resource://services-sync/" + mod, Weave, lazies[mod]); -} diff --git a/components/weave/src/sync/notifications.js b/components/weave/src/sync/notifications.js deleted file mode 100644 index 5a67a7414..000000000 --- a/components/weave/src/sync/notifications.js +++ /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/. */ - -this.EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/util.js"); - -this.Notifications = { - // Match the referenced values in toolkit/content/widgets/notification.xml. - get PRIORITY_INFO() { return 1; }, // PRIORITY_INFO_LOW - get PRIORITY_WARNING() { return 4; }, // PRIORITY_WARNING_LOW - get PRIORITY_ERROR() { return 7; }, // PRIORITY_CRITICAL_LOW - - // FIXME: instead of making this public, dress the Notifications object - // to behave like an iterator (using generators?) and have callers access - // this array through the Notifications object. - notifications: [], - - _observers: [], - - // XXX Should we have a helper method for adding a simple notification? - // I.e. something like |function notify(title, description, priority)|. - - add: function Notifications_add(notification) { - this.notifications.push(notification); - Observers.notify("weave:notification:added", notification, null); - }, - - remove: function Notifications_remove(notification) { - let index = this.notifications.indexOf(notification); - - if (index != -1) { - this.notifications.splice(index, 1); - Observers.notify("weave:notification:removed", notification, null); - } - }, - - /** - * Replace an existing notification. - */ - replace: function Notifications_replace(oldNotification, newNotification) { - let index = this.notifications.indexOf(oldNotification); - - if (index != -1) - this.notifications.splice(index, 1, newNotification); - else { - this.notifications.push(newNotification); - // XXX Should we throw because we didn't find the existing notification? - // XXX Should we notify observers about weave:notification:added? - } - - // XXX Should we notify observers about weave:notification:replaced? - }, - - /** - * Remove all notifications that match a title. If no title is provided, all - * notifications are removed. - * - * @param title [optional] - * Title of notifications to remove; falsy value means remove all - */ - removeAll: function Notifications_removeAll(title) { - this.notifications.filter(old => (old.title == title || !title)). - forEach(old => { this.remove(old); }, this); - }, - - // replaces all existing notifications with the same title as the new one - replaceTitle: function Notifications_replaceTitle(notification) { - this.removeAll(notification.title); - this.add(notification); - } -}; - - -/** - * A basic notification. Subclass this to create more complex notifications. - */ -this.Notification = -function Notification(title, description, iconURL, priority, buttons, link) { - this.title = title; - this.description = description; - - if (iconURL) - this.iconURL = iconURL; - - if (priority) - this.priority = priority; - - if (buttons) - this.buttons = buttons; - - if (link) - this.link = link; -} - -// We set each prototype property individually instead of redefining -// the entire prototype to avoid blowing away existing properties -// of the prototype like the the "constructor" property, which we use -// to bind notification objects to their XBL representations. -Notification.prototype.priority = Notifications.PRIORITY_INFO; -Notification.prototype.iconURL = null; -Notification.prototype.buttons = []; - -/** - * A button to display in a notification. - */ -this.NotificationButton = - function NotificationButton(label, accessKey, callback) { - function callbackWrapper() { - try { - callback.apply(this, arguments); - } catch (e) { - let logger = Log.repository.getLogger("Sync.Notifications"); - logger.error("An exception occurred: ", e); - logger.info(Utils.stackTrace(e)); - throw e; - } - } - - this.label = label; - this.accessKey = accessKey; - this.callback = callbackWrapper; -} diff --git a/components/weave/src/sync/policies.js b/components/weave/src/sync/policies.js deleted file mode 100644 index 48acbe2e6..000000000 --- a/components/weave/src/sync/policies.js +++ /dev/null @@ -1,967 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "ErrorHandler", - "SyncScheduler", -]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-common/logmanager.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "Status", - "resource://services-sync/status.js"); - -this.SyncScheduler = function SyncScheduler(service) { - this.service = service; - this.init(); -} -SyncScheduler.prototype = { - _log: Log.repository.getLogger("Sync.SyncScheduler"), - - _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME, - LOGIN_FAILED_NO_PASSWORD, - LOGIN_FAILED_NO_PASSPHRASE, - LOGIN_FAILED_INVALID_PASSPHRASE, - LOGIN_FAILED_LOGIN_REJECTED], - - /** - * The nsITimer object that schedules the next sync. See scheduleNextSync(). - */ - syncTimer: null, - - setDefaults: function setDefaults() { - this._log.trace("Setting SyncScheduler policy values to defaults."); - - let service = Cc["@mozilla.org/weave/service;1"] - .getService(Ci.nsISupports) - .wrappedJSObject; - - let prefSDInterval = "scheduler.sync11.singleDeviceInterval"; - this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000; - - this.idleInterval = Svc.Prefs.get("scheduler.idleInterval") * 1000; - this.activeInterval = Svc.Prefs.get("scheduler.activeInterval") * 1000; - this.immediateInterval = Svc.Prefs.get("scheduler.immediateInterval") * 1000; - this.eolInterval = Svc.Prefs.get("scheduler.eolInterval") * 1000; - - // A user is non-idle on startup by default. - this.idle = false; - - this.hasIncomingItems = false; - - this.clearSyncTriggers(); - }, - - // nextSync is in milliseconds, but prefs can't hold that much - get nextSync() { - if (Svc.Prefs) { - return Svc.Prefs.get("nextSync", 0) * 1000 - } - }, - set nextSync(value) { - if (Svc.Prefs) { - Svc.Prefs.set("nextSync", Math.floor(value / 1000)) - } - }, - - get syncInterval() { - if (Svc.Prefs) { - return Svc.Prefs.get("syncInterval", this.singleDeviceInterval) - } - }, - set syncInterval(value) { - if (Svc.Prefs) { - Svc.Prefs.set("syncInterval", value) - } - }, - - get syncThreshold() { - if (Svc.Prefs) { - return Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD) - } - }, - set syncThreshold(value) { - if (Svc.Prefs) { - Svc.Prefs.set("syncThreshold", value) - } - }, - - get globalScore() { - if (Svc.Prefs) { - return Svc.Prefs.get("globalScore", 0) - } - }, - set globalScore(value) { - if (Svc.Prefs) { - Svc.Prefs.set("globalScore", value) - } - }, - - get numClients() { - if (Svc.Prefs) { - return Svc.Prefs.get("numClients", 0) - } - }, - set numClients(value) { - if (Svc.Prefs) { - Svc.Prefs.set("numClients", value) - } - }, - - init: function init() { - this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; - this.setDefaults(); - Svc.Obs.add("weave:engine:score:updated", this); - Svc.Obs.add("network:offline-status-changed", this); - Svc.Obs.add("weave:service:sync:start", this); - Svc.Obs.add("weave:service:sync:finish", this); - Svc.Obs.add("weave:engine:sync:finish", this); - Svc.Obs.add("weave:engine:sync:error", this); - Svc.Obs.add("weave:service:login:error", this); - Svc.Obs.add("weave:service:logout:finish", this); - Svc.Obs.add("weave:service:sync:error", this); - Svc.Obs.add("weave:service:backoff:interval", this); - Svc.Obs.add("weave:service:ready", this); - Svc.Obs.add("weave:engine:sync:applied", this); - Svc.Obs.add("weave:service:setup-complete", this); - Svc.Obs.add("weave:service:start-over", this); - Svc.Obs.add("FxA:hawk:backoff:interval", this); - - if (Status.checkSetup() == STATUS_OK) { - Svc.Obs.add("wake_notification", this); - Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); - } - }, - - observe: function observe(subject, topic, data) { - this._log.trace("Handling " + topic); - switch(topic) { - case "weave:engine:score:updated": - if (Status.login == LOGIN_SUCCEEDED) { - Utils.namedTimer(this.calculateScore, SCORE_UPDATE_DELAY, this, - "_scoreTimer"); - } - break; - case "network:offline-status-changed": - // Whether online or offline, we'll reschedule syncs - this._log.trace("Network offline status change: " + data); - this.checkSyncStatus(); - break; - case "weave:service:sync:start": - // Clear out any potentially pending syncs now that we're syncing - this.clearSyncTriggers(); - - // reset backoff info, if the server tells us to continue backing off, - // we'll handle that later - Status.resetBackoff(); - - this.globalScore = 0; - break; - case "weave:service:sync:finish": - this.nextSync = 0; - this.adjustSyncInterval(); - - if (Status.service == SYNC_FAILED_PARTIAL && this.requiresBackoff) { - this.requiresBackoff = false; - this.handleSyncError(); - return; - } - - let sync_interval; - this._syncErrors = 0; - if (Status.sync == NO_SYNC_NODE_FOUND) { - this._log.trace("Scheduling a sync at interval NO_SYNC_NODE_FOUND."); - sync_interval = NO_SYNC_NODE_INTERVAL; - } - this.scheduleNextSync(sync_interval); - break; - case "weave:engine:sync:finish": - if (data == "clients") { - // Update the client mode because it might change what we sync. - this.updateClientMode(); - } - break; - case "weave:engine:sync:error": - // `subject` is the exception thrown by an engine's sync() method. - let exception = subject; - if (exception.status >= 500 && exception.status <= 504) { - this.requiresBackoff = true; - } - break; - case "weave:service:login:error": - this.clearSyncTriggers(); - - if (Status.login == MASTER_PASSWORD_LOCKED) { - // Try again later, just as if we threw an error... only without the - // error count. - this._log.debug("Couldn't log in: master password is locked."); - this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"); - this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); - } else if (this._fatalLoginStatus.indexOf(Status.login) == -1) { - // Not a fatal login error, just an intermittent network or server - // issue. Keep on syncin'. - this.checkSyncStatus(); - } - break; - case "weave:service:logout:finish": - // Start or cancel the sync timer depending on if - // logged in or logged out - this.checkSyncStatus(); - break; - case "weave:service:sync:error": - // There may be multiple clients but if the sync fails, client mode - // should still be updated so that the next sync has a correct interval. - this.updateClientMode(); - this.adjustSyncInterval(); - this.nextSync = 0; - this.handleSyncError(); - break; - case "FxA:hawk:backoff:interval": - case "weave:service:backoff:interval": - let requested_interval = subject * 1000; - this._log.debug("Got backoff notification: " + requested_interval + "ms"); - // Leave up to 25% more time for the back off. - let interval = requested_interval * (1 + Math.random() * 0.25); - Status.backoffInterval = interval; - Status.minimumNextSync = Date.now() + requested_interval; - this._log.debug("Fuzzed minimum next sync: " + Status.minimumNextSync); - break; - case "weave:service:ready": - // Applications can specify this preference if they want autoconnect - // to happen after a fixed delay. - let delay = Svc.Prefs.get("autoconnectDelay"); - if (delay) { - this.delayedAutoConnect(delay); - } - break; - case "weave:engine:sync:applied": - let numItems = subject.succeeded; - this._log.trace("Engine " + data + " successfully applied " + numItems + - " items."); - if (numItems) { - this.hasIncomingItems = true; - } - break; - case "weave:service:setup-complete": - Services.prefs.savePrefFile(null); - Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); - Svc.Obs.add("wake_notification", this); - break; - case "weave:service:start-over": - this.setDefaults(); - try { - Svc.Idle.removeIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); - } catch (ex if (ex.result == Cr.NS_ERROR_FAILURE)) { - // In all likelihood we didn't have an idle observer registered yet. - // It's all good. - } - break; - case "idle": - this._log.trace("We're idle."); - this.idle = true; - // Adjust the interval for future syncs. This won't actually have any - // effect until the next pending sync (which will happen soon since we - // were just active.) - this.adjustSyncInterval(); - break; - case "active": - this._log.trace("Received notification that we're back from idle."); - this.idle = false; - Utils.namedTimer(function onBack() { - if (this.idle) { - this._log.trace("... and we're idle again. " + - "Ignoring spurious back notification."); - return; - } - - this._log.trace("Genuine return from idle. Syncing."); - // Trigger a sync if we have multiple clients. - if (this.numClients > 1) { - this.scheduleNextSync(0); - } - }, IDLE_OBSERVER_BACK_DELAY, this, "idleDebouncerTimer"); - break; - case "wake_notification": - this._log.debug("Woke from sleep."); - Utils.nextTick(() => { - // Trigger a sync if we have multiple clients. - if (this.numClients > 1) { - this._log.debug("More than 1 client. Syncing."); - this.scheduleNextSync(0); - } - }); - break; - } - }, - - adjustSyncInterval: function adjustSyncInterval() { - if (Status.eol) { - this._log.debug("Server status is EOL; using eolInterval."); - this.syncInterval = this.eolInterval; - return; - } - - if (this.numClients <= 1) { - this._log.trace("Adjusting syncInterval to singleDeviceInterval."); - this.syncInterval = this.singleDeviceInterval; - return; - } - - // Only MULTI_DEVICE clients will enter this if statement - // since SINGLE_USER clients will be handled above. - if (this.idle) { - this._log.trace("Adjusting syncInterval to idleInterval."); - this.syncInterval = this.idleInterval; - return; - } - - if (this.hasIncomingItems) { - this._log.trace("Adjusting syncInterval to immediateInterval."); - this.hasIncomingItems = false; - this.syncInterval = this.immediateInterval; - } else { - this._log.trace("Adjusting syncInterval to activeInterval."); - this.syncInterval = this.activeInterval; - } - }, - - calculateScore: function calculateScore() { - let engines = [this.service.clientsEngine].concat(this.service.engineManager.getEnabled()); - for (let i = 0;i < engines.length;i++) { - this._log.trace(engines[i].name + ": score: " + engines[i].score); - this.globalScore += engines[i].score; - engines[i]._tracker.resetScore(); - } - - this._log.trace("Global score updated: " + this.globalScore); - this.checkSyncStatus(); - }, - - /** - * Process the locally stored clients list to figure out what mode to be in - */ - updateClientMode: function updateClientMode() { - // Nothing to do if it's the same amount - let numClients = this.service.clientsEngine.stats.numClients; - if (this.numClients == numClients) - return; - - this._log.debug("Client count: " + this.numClients + " -> " + numClients); - this.numClients = numClients; - - if (numClients <= 1) { - this._log.trace("Adjusting syncThreshold to SINGLE_USER_THRESHOLD"); - this.syncThreshold = SINGLE_USER_THRESHOLD; - } else { - this._log.trace("Adjusting syncThreshold to MULTI_DEVICE_THRESHOLD"); - this.syncThreshold = MULTI_DEVICE_THRESHOLD; - } - this.adjustSyncInterval(); - }, - - /** - * Check if we should be syncing and schedule the next sync, if it's not scheduled - */ - checkSyncStatus: function checkSyncStatus() { - // Should we be syncing now, if not, cancel any sync timers and return - // if we're in backoff, we'll schedule the next sync. - let ignore = [kSyncBackoffNotMet, kSyncMasterPasswordLocked]; - let skip = this.service._checkSync(ignore); - this._log.trace("_checkSync returned \"" + skip + "\"."); - if (skip) { - this.clearSyncTriggers(); - return; - } - - // Only set the wait time to 0 if we need to sync right away - let wait; - if (this.globalScore > this.syncThreshold) { - this._log.debug("Global Score threshold hit, triggering sync."); - wait = 0; - } - this.scheduleNextSync(wait); - }, - - /** - * Call sync() if Master Password is not locked. - * - * Otherwise, reschedule a sync for later. - */ - syncIfMPUnlocked: function syncIfMPUnlocked() { - // No point if we got kicked out by the master password dialog. - if (Status.login == MASTER_PASSWORD_LOCKED && - Utils.mpLocked()) { - this._log.debug("Not initiating sync: Login status is " + Status.login); - - // If we're not syncing now, we need to schedule the next one. - this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"); - this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); - return; - } - - Utils.nextTick(this.service.sync, this.service); - }, - - /** - * Set a timer for the next sync - */ - scheduleNextSync: function scheduleNextSync(interval) { - // If no interval was specified, use the current sync interval. - if (interval == null) { - interval = this.syncInterval; - } - - // Ensure the interval is set to no less than the backoff. - if (Status.backoffInterval && interval < Status.backoffInterval) { - this._log.trace("Requested interval " + interval + - " ms is smaller than the backoff interval. " + - "Using backoff interval " + - Status.backoffInterval + " ms instead."); - interval = Status.backoffInterval; - } - - if (this.nextSync != 0) { - // There's already a sync scheduled. Don't reschedule if there's already - // a timer scheduled for sooner than requested. - let currentInterval = this.nextSync - Date.now(); - this._log.trace("There's already a sync scheduled in " + - currentInterval + " ms."); - if (currentInterval < interval && this.syncTimer) { - this._log.trace("Ignoring scheduling request for next sync in " + - interval + " ms."); - return; - } - } - - // Start the sync right away if we're already late. - if (interval <= 0) { - this._log.trace("Requested sync should happen right away."); - this.syncIfMPUnlocked(); - return; - } - - this._log.debug("Next sync in " + interval + " ms."); - Utils.namedTimer(this.syncIfMPUnlocked, interval, this, "syncTimer"); - - // Save the next sync time in-case sync is disabled (logout/offline/etc.) - this.nextSync = Date.now() + interval; - }, - - - /** - * Incorporates the backoff/retry logic used in error handling and elective - * non-syncing. - */ - scheduleAtInterval: function scheduleAtInterval(minimumInterval) { - let interval = Utils.calculateBackoff(this._syncErrors, - MINIMUM_BACKOFF_INTERVAL, - Status.backoffInterval); - if (minimumInterval) { - interval = Math.max(minimumInterval, interval); - } - - this._log.debug("Starting client-initiated backoff. Next sync in " + - interval + " ms."); - this.scheduleNextSync(interval); - }, - - /** - * Automatically start syncing after the given delay (in seconds). - * - * Applications can define the `services.sync.autoconnectDelay` preference - * to have this called automatically during start-up with the pref value as - * the argument. Alternatively, they can call it themselves to control when - * Sync should first start to sync. - */ - delayedAutoConnect: function delayedAutoConnect(delay) { - if (this.service._checkSetup() == STATUS_OK) { - Utils.namedTimer(this.autoConnect, delay * 1000, this, "_autoTimer"); - } - }, - - autoConnect: function autoConnect() { - if (this.service._checkSetup() == STATUS_OK && !this.service._checkSync()) { - // Schedule a sync based on when a previous sync was scheduled. - // scheduleNextSync() will do the right thing if that time lies in - // the past. - this.scheduleNextSync(this.nextSync - Date.now()); - } - - // Once autoConnect is called we no longer need _autoTimer. - if (this._autoTimer) { - this._autoTimer.clear(); - } - }, - - _syncErrors: 0, - /** - * Deal with sync errors appropriately - */ - handleSyncError: function handleSyncError() { - this._log.trace("In handleSyncError. Error count: " + this._syncErrors); - this._syncErrors++; - - // Do nothing on the first couple of failures, if we're not in - // backoff due to 5xx errors. - if (!Status.enforceBackoff) { - if (this._syncErrors < MAX_ERROR_COUNT_BEFORE_BACKOFF) { - this.scheduleNextSync(); - return; - } - this._log.debug("Sync error count has exceeded " + - MAX_ERROR_COUNT_BEFORE_BACKOFF + "; enforcing backoff."); - Status.enforceBackoff = true; - } - - this.scheduleAtInterval(); - }, - - - /** - * Remove any timers/observers that might trigger a sync - */ - clearSyncTriggers: function clearSyncTriggers() { - this._log.debug("Clearing sync triggers and the global score."); - this.globalScore = this.nextSync = 0; - - // Clear out any scheduled syncs - if (this.syncTimer) - this.syncTimer.clear(); - }, - - /** - * Prevent new syncs from starting. This is used by the FxA migration code - * where we can't afford to have a sync start partway through the migration. - * To handle the edge-case of a sync starting and not stopping, we store - * this state in a pref, so on the next startup we remain blocked (and thus - * sync will never start) so the migration can complete. - * - * As a safety measure, we only block for some period of time, and after - * that it will automatically unblock. This ensures that if things go - * really pear-shaped and we never end up calling unblockSync() we haven't - * completely broken the world. - */ - blockSync: function(until = null) { - if (!until) { - until = Date.now() + DEFAULT_BLOCK_PERIOD; - } - // until is specified in ms, but Prefs can't hold that much - Svc.Prefs.set("scheduler.blocked-until", Math.floor(until / 1000)); - }, - - unblockSync: function() { - Svc.Prefs.reset("scheduler.blocked-until"); - // the migration code should be ready to roll, so resume normal operations. - this.checkSyncStatus(); - }, - - get isBlocked() { - let until = Svc.Prefs ? Svc.Prefs.get("scheduler.blocked-until") : undefined; - if (until === undefined) { - return false; - } - if (until <= Math.floor(Date.now() / 1000)) { - // we were previously blocked but the time has expired. - Svc.Prefs.reset("scheduler.blocked-until"); - return false; - } - // we remain blocked. - return true; - }, -}; - -this.ErrorHandler = function ErrorHandler(service) { - this.service = service; - this.init(); -} -ErrorHandler.prototype = { - MINIMUM_ALERT_INTERVAL_MSEC: 604800000, // One week. - - /** - * Flag that turns on error reporting for all errors, incl. network errors. - */ - dontIgnoreErrors: false, - - /** - * Flag that indicates if we have already reported a prolonged failure. - * Once set, we don't report it again, meaning this error is only reported - * one per run. - */ - didReportProlongedError: false, - - init: function init() { - Svc.Obs.add("weave:engine:sync:applied", this); - Svc.Obs.add("weave:engine:sync:error", this); - Svc.Obs.add("weave:service:login:error", this); - Svc.Obs.add("weave:service:sync:error", this); - Svc.Obs.add("weave:service:sync:finish", this); - - this.initLogs(); - }, - - initLogs: function initLogs() { - this._log = Log.repository.getLogger("Sync.ErrorHandler"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; - - let root = Log.repository.getLogger("Sync"); - root.level = Log.Level[Svc.Prefs.get("log.rootLogger")]; - - let logs = ["Sync", "FirefoxAccounts", "Hawk", "Common.TokenServerClient", - "Sync.SyncMigration"]; - - this._logManager = new LogManager(Svc.Prefs, logs, "sync"); - }, - - observe: function observe(subject, topic, data) { - this._log.trace("Handling " + topic); - switch(topic) { - case "weave:engine:sync:applied": - if (subject.newFailed) { - // An engine isn't able to apply one or more incoming records. - // We don't fail hard on this, but it usually indicates a bug, - // so for now treat it as sync error (c.f. Service._syncEngine()) - Status.engines = [data, ENGINE_APPLY_FAIL]; - this._log.debug(data + " failed to apply some records."); - } - break; - case "weave:engine:sync:error": - let exception = subject; // exception thrown by engine's sync() method - let engine_name = data; // engine name that threw the exception - - this.checkServerError(exception); - - Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL]; - this._log.debug(engine_name + " failed: ", exception); - break; - case "weave:service:login:error": - this.resetFileLog(this._logManager.REASON_ERROR); - - if (this.shouldReportError()) { - this.notifyOnNextTick("weave:ui:login:error"); - } else { - this.notifyOnNextTick("weave:ui:clear-error"); - } - - this.dontIgnoreErrors = false; - break; - case "weave:service:sync:error": - if (Status.sync == CREDENTIALS_CHANGED) { - this.service.logout(); - } - - this.resetFileLog(this._logManager.REASON_ERROR); - - if (this.shouldReportError()) { - this.notifyOnNextTick("weave:ui:sync:error"); - } else { - this.notifyOnNextTick("weave:ui:sync:finish"); - } - - this.dontIgnoreErrors = false; - break; - case "weave:service:sync:finish": - this._log.trace("Status.service is " + Status.service); - - // Check both of these status codes: in the event of a failure in one - // engine, Status.service will be SYNC_FAILED_PARTIAL despite - // Status.sync being SYNC_SUCCEEDED. - // *facepalm* - if (Status.sync == SYNC_SUCCEEDED && - Status.service == STATUS_OK) { - // Great. Let's clear our mid-sync 401 note. - this._log.trace("Clearing lastSyncReassigned."); - Svc.Prefs.reset("lastSyncReassigned"); - } - - if (Status.service == SYNC_FAILED_PARTIAL) { - this._log.debug("Some engines did not sync correctly."); - this.resetFileLog(this._logManager.REASON_ERROR); - - if (this.shouldReportError()) { - this.dontIgnoreErrors = false; - this.notifyOnNextTick("weave:ui:sync:error"); - break; - } - } else { - this.resetFileLog(this._logManager.REASON_SUCCESS); - } - this.dontIgnoreErrors = false; - this.notifyOnNextTick("weave:ui:sync:finish"); - break; - } - }, - - notifyOnNextTick: function notifyOnNextTick(topic) { - Utils.nextTick(function() { - this._log.trace("Notifying " + topic + - ". Status.login is " + Status.login + - ". Status.sync is " + Status.sync); - Svc.Obs.notify(topic); - }, this); - }, - - /** - * Trigger a sync and don't muffle any errors, particularly network errors. - */ - syncAndReportErrors: function syncAndReportErrors() { - this._log.debug("Beginning user-triggered sync."); - - this.dontIgnoreErrors = true; - Utils.nextTick(this.service.sync, this.service); - }, - - /** - * Generate a log file for the sync that just completed - * and refresh the input & output streams. - * - * @param reason - * A constant from the LogManager that indicates the reason for the - * reset. - */ - resetFileLog: function resetFileLog(reason) { - let onComplete = () => { - Svc.Obs.notify("weave:service:reset-file-log"); - this._log.trace("Notified: " + Date.now()); - }; - // Note we do not return the promise here - the caller doesn't need to wait - // for this to complete. - this._logManager.resetFileLog(reason).then(onComplete, onComplete); - }, - - /** - * Translates server error codes to meaningful strings. - * - * @param code - * server error code as an integer - */ - errorStr: function errorStr(code) { - switch (code.toString()) { - case "1": - return "illegal-method"; - case "2": - return "invalid-captcha"; - case "3": - return "invalid-username"; - case "4": - return "cannot-overwrite-resource"; - case "5": - return "userid-mismatch"; - case "6": - return "json-parse-failure"; - case "7": - return "invalid-password"; - case "8": - return "invalid-record"; - case "9": - return "weak-password"; - default: - return "generic-server-error"; - } - }, - - shouldReportError: function shouldReportError() { - if (Status.login == MASTER_PASSWORD_LOCKED) { - this._log.trace("shouldReportError: false (master password locked)."); - return false; - } - - if (this.dontIgnoreErrors) { - return true; - } - - if (Status.login == LOGIN_FAILED_LOGIN_REJECTED) { - // An explicit LOGIN_REJECTED state is always reported (bug 1081158) - this._log.trace("shouldReportError: true (login was rejected)"); - return true; - } - - let lastSync = Svc.Prefs.get("lastSync"); - if (lastSync && ((Date.now() - Date.parse(lastSync)) > - Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) { - Status.sync = PROLONGED_SYNC_FAILURE; - if (this.didReportProlongedError) { - this._log.trace("shouldReportError: false (prolonged sync failure, but" + - " we've already reported it)."); - return false; - } - this._log.trace("shouldReportError: true (first prolonged sync failure)."); - this.didReportProlongedError = true; - return true; - } - - // We got a 401 mid-sync. Wait for the next sync before actually handling - // an error. This assumes that we'll get a 401 again on a login fetch in - // order to report the error. - if (!this.service.clusterURL) { - this._log.trace("shouldReportError: false (no cluster URL; " + - "possible node reassignment)."); - return false; - } - - return ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 && - [Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1); - }, - - get currentAlertMode() { - return Svc.Prefs ? Svc.Prefs.get("errorhandler.alert.mode") : undefined; - }, - - set currentAlertMode(str) { - return Svc.Prefs ? Svc.Prefs.set("errorhandler.alert.mode", str) : undefined; - }, - - get earliestNextAlert() { - return Svc.Prefs ? Svc.Prefs.get("errorhandler.alert.earliestNext", 0) * 1000 : undefined; - }, - - set earliestNextAlert(msec) { - return Svc.Prefs ? Svc.Prefs.set("errorhandler.alert.earliestNext", msec / 1000) : undefined; - }, - - clearServerAlerts: function () { - // If we have any outstanding alerts, apparently they're no longer relevant. - Svc.Prefs.resetBranch("errorhandler.alert"); - }, - - /** - * X-Weave-Alert headers can include a JSON object: - * - * { - * "code": // One of "hard-eol", "soft-eol". - * "url": // For "Learn more" link. - * "message": // Logged in Sync logs. - * } - */ - handleServerAlert: function (xwa) { - if (!xwa.code) { - this._log.warn("Got structured X-Weave-Alert, but no alert code."); - return; - } - - switch (xwa.code) { - // Gently and occasionally notify the user that this service will be - // shutting down. - case "soft-eol": - // Fall through. - - // Tell the user that this service has shut down, and drop our syncing - // frequency dramatically. - case "hard-eol": - // Note that both of these alerts should be subservient to future "sign - // in with your Firefox Account" storage alerts. - if ((this.currentAlertMode != xwa.code) || - (this.earliestNextAlert < Date.now())) { - Utils.nextTick(function() { - Svc.Obs.notify("weave:eol", xwa); - }, this); - this._log.error("X-Weave-Alert: " + xwa.code + ": " + xwa.message); - this.earliestNextAlert = Date.now() + this.MINIMUM_ALERT_INTERVAL_MSEC; - this.currentAlertMode = xwa.code; - } - break; - default: - this._log.debug("Got unexpected X-Weave-Alert code: " + xwa.code); - } - }, - - /** - * Handle HTTP response results or exceptions and set the appropriate - * Status.* bits. - * - * This method also looks for "side-channel" warnings. - */ - checkServerError: function (resp) { - switch (resp.status) { - case 200: - case 404: - case 513: - let xwa = resp.headers['x-weave-alert']; - - // Only process machine-readable alerts. - if (!xwa || !xwa.startsWith("{")) { - this.clearServerAlerts(); - return; - } - - try { - xwa = JSON.parse(xwa); - } catch (ex) { - this._log.warn("Malformed X-Weave-Alert from server: " + xwa); - return; - } - - this.handleServerAlert(xwa); - break; - - case 400: - if (resp == RESPONSE_OVER_QUOTA) { - Status.sync = OVER_QUOTA; - } - break; - - case 401: - this.service.logout(); - this._log.info("Got 401 response; resetting clusterURL."); - Svc.Prefs.reset("clusterURL"); - - let delay = 0; - if (Svc.Prefs.get("lastSyncReassigned")) { - // We got a 401 in the middle of the previous sync, and we just got - // another. Login must have succeeded in order for us to get here, so - // the password should be correct. - // This is likely to be an intermittent server issue, so back off and - // give it time to recover. - this._log.warn("Last sync also failed for 401. Delaying next sync."); - delay = MINIMUM_BACKOFF_INTERVAL; - } else { - this._log.debug("New mid-sync 401 failure. Making a note."); - Svc.Prefs.set("lastSyncReassigned", true); - } - this._log.info("Attempting to schedule another sync."); - this.service.scheduler.scheduleNextSync(delay); - break; - - case 500: - case 502: - case 503: - case 504: - Status.enforceBackoff = true; - if (resp.status == 503 && resp.headers["retry-after"]) { - let retryAfter = resp.headers["retry-after"]; - this._log.debug("Got Retry-After: " + retryAfter); - if (this.service.isLoggedIn) { - Status.sync = SERVER_MAINTENANCE; - } else { - Status.login = SERVER_MAINTENANCE; - } - Svc.Obs.notify("weave:service:backoff:interval", - parseInt(retryAfter, 10)); - } - break; - } - - switch (resp.result) { - case Cr.NS_ERROR_UNKNOWN_HOST: - case Cr.NS_ERROR_CONNECTION_REFUSED: - case Cr.NS_ERROR_NET_TIMEOUT: - case Cr.NS_ERROR_NET_RESET: - case Cr.NS_ERROR_NET_INTERRUPT: - case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED: - // The constant says it's about login, but in fact it just - // indicates general network error. - if (this.service.isLoggedIn) { - Status.sync = LOGIN_FAILED_NETWORK_ERROR; - } else { - Status.login = LOGIN_FAILED_NETWORK_ERROR; - } - break; - } - }, -}; diff --git a/components/weave/src/sync/record.js b/components/weave/src/sync/record.js deleted file mode 100644 index 5dc1c012c..000000000 --- a/components/weave/src/sync/record.js +++ /dev/null @@ -1,628 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "WBORecord", - "RecordManager", - "CryptoWrapper", - "CollectionKeyManager", - "Collection", -]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -const CRYPTO_COLLECTION = "crypto"; -const KEYS_WBO = "keys"; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/keys.js"); -Cu.import("resource://services-sync/resource.js"); -Cu.import("resource://services-sync/util.js"); - -this.WBORecord = function WBORecord(collection, id) { - this.data = {}; - this.payload = {}; - this.collection = collection; // Optional. - this.id = id; // Optional. -} -WBORecord.prototype = { - _logName: "Sync.Record.WBO", - - get sortindex() { - if (this.data.sortindex) - return this.data.sortindex; - return 0; - }, - - // Get thyself from your URI, then deserialize. - // Set thine 'response' field. - fetch: function fetch(resource) { - if (!(resource instanceof Resource)) { - throw new Error("First argument must be a Resource instance."); - } - - let r = resource.get(); - if (r.success) { - this.deserialize(r); // Warning! Muffles exceptions! - } - this.response = r; - return this; - }, - - upload: function upload(resource) { - if (!(resource instanceof Resource)) { - throw new Error("First argument must be a Resource instance."); - } - - return resource.put(this); - }, - - // Take a base URI string, with trailing slash, and return the URI of this - // WBO based on collection and ID. - uri: function(base) { - if (this.collection && this.id) { - let url = Utils.makeURI(base + this.collection + "/" + this.id); - url.QueryInterface(Ci.nsIURL); - return url; - } - return null; - }, - - deserialize: function deserialize(json) { - this.data = json.constructor.toString() == String ? JSON.parse(json) : json; - - try { - // The payload is likely to be JSON, but if not, keep it as a string - this.payload = JSON.parse(this.payload); - } catch(ex) {} - }, - - toJSON: function toJSON() { - // Copy fields from data to be stringified, making sure payload is a string - let obj = {}; - for (let [key, val] of Object.entries(this.data)) - obj[key] = key == "payload" ? JSON.stringify(val) : val; - if (this.ttl) - obj.ttl = this.ttl; - return obj; - }, - - toString: function toString() { - return "{ " + - "id: " + this.id + " " + - "index: " + this.sortindex + " " + - "modified: " + this.modified + " " + - "ttl: " + this.ttl + " " + - "payload: " + JSON.stringify(this.payload) + - " }"; - } -}; - -Utils.deferGetSet(WBORecord, "data", ["id", "modified", "sortindex", "payload"]); - -this.CryptoWrapper = function CryptoWrapper(collection, id) { - this.cleartext = {}; - WBORecord.call(this, collection, id); - this.ciphertext = null; - this.id = id; -} -CryptoWrapper.prototype = { - __proto__: WBORecord.prototype, - _logName: "Sync.Record.CryptoWrapper", - - ciphertextHMAC: function ciphertextHMAC(keyBundle) { - let hasher = keyBundle.sha256HMACHasher; - if (!hasher) { - throw "Cannot compute HMAC without an HMAC key."; - } - - return Utils.bytesAsHex(Utils.digestUTF8(this.ciphertext, hasher)); - }, - - /* - * Don't directly use the sync key. Instead, grab a key for this - * collection, which is decrypted with the sync key. - * - * Cache those keys; invalidate the cache if the time on the keys collection - * changes, or other auth events occur. - * - * Optional key bundle overrides the collection key lookup. - */ - encrypt: function encrypt(keyBundle) { - if (!keyBundle) { - throw new Error("A key bundle must be supplied to encrypt."); - } - - this.IV = Svc.Crypto.generateRandomIV(); - this.ciphertext = Svc.Crypto.encrypt(JSON.stringify(this.cleartext), - keyBundle.encryptionKeyB64, this.IV); - this.hmac = this.ciphertextHMAC(keyBundle); - this.cleartext = null; - }, - - // Optional key bundle. - decrypt: function decrypt(keyBundle) { - if (!this.ciphertext) { - throw "No ciphertext: nothing to decrypt?"; - } - - if (!keyBundle) { - throw new Error("A key bundle must be supplied to decrypt."); - } - - // Authenticate the encrypted blob with the expected HMAC - let computedHMAC = this.ciphertextHMAC(keyBundle); - - if (computedHMAC != this.hmac) { - Utils.throwHMACMismatch(this.hmac, computedHMAC); - } - - // Handle invalid data here. Elsewhere we assume that cleartext is an object. - let cleartext = Svc.Crypto.decrypt(this.ciphertext, - keyBundle.encryptionKeyB64, this.IV); - let json_result = JSON.parse(cleartext); - - if (json_result && (json_result instanceof Object)) { - this.cleartext = json_result; - this.ciphertext = null; - } else { - throw "Decryption failed: result is <" + json_result + ">, not an object."; - } - - // Verify that the encrypted id matches the requested record's id. - if (this.cleartext.id != this.id) - throw "Record id mismatch: " + this.cleartext.id + " != " + this.id; - - return this.cleartext; - }, - - toString: function toString() { - let payload = this.deleted ? "DELETED" : JSON.stringify(this.cleartext); - - return "{ " + - "id: " + this.id + " " + - "index: " + this.sortindex + " " + - "modified: " + this.modified + " " + - "ttl: " + this.ttl + " " + - "payload: " + payload + " " + - "collection: " + (this.collection || "undefined") + - " }"; - }, - - // The custom setter below masks the parent's getter, so explicitly call it :( - get id() WBORecord.prototype.__lookupGetter__("id").call(this), - - // Keep both plaintext and encrypted versions of the id to verify integrity - set id(val) { - WBORecord.prototype.__lookupSetter__("id").call(this, val); - return this.cleartext.id = val; - }, -}; - -Utils.deferGetSet(CryptoWrapper, "payload", ["ciphertext", "IV", "hmac"]); -Utils.deferGetSet(CryptoWrapper, "cleartext", "deleted"); - -/** - * An interface and caching layer for records. - */ -this.RecordManager = function RecordManager(service) { - this.service = service; - - this._log = Log.repository.getLogger(this._logName); - this._records = {}; -} -RecordManager.prototype = { - _recordType: CryptoWrapper, - _logName: "Sync.RecordManager", - - import: function RecordMgr_import(url) { - this._log.trace("Importing record: " + (url.spec ? url.spec : url)); - try { - // Clear out the last response with empty object if GET fails - this.response = {}; - this.response = this.service.resource(url).get(); - - // Don't parse and save the record on failure - if (!this.response.success) - return null; - - let record = new this._recordType(url); - record.deserialize(this.response); - - return this.set(url, record); - } catch(ex) { - this._log.debug("Failed to import record: ", ex); - return null; - } - }, - - get: function RecordMgr_get(url) { - // Use a url string as the key to the hash - let spec = url.spec ? url.spec : url; - if (spec in this._records) - return this._records[spec]; - return this.import(url); - }, - - set: function RecordMgr_set(url, record) { - let spec = url.spec ? url.spec : url; - return this._records[spec] = record; - }, - - contains: function RecordMgr_contains(url) { - if ((url.spec || url) in this._records) - return true; - return false; - }, - - clearCache: function recordMgr_clearCache() { - this._records = {}; - }, - - del: function RecordMgr_del(url) { - delete this._records[url]; - } -}; - -/** - * Keeps track of mappings between collection names ('tabs') and KeyBundles. - * - * You can update this thing simply by giving it /info/collections. It'll - * use the last modified time to bring itself up to date. - */ -this.CollectionKeyManager = function CollectionKeyManager() { - this.lastModified = 0; - this._collections = {}; - this._default = null; - - this._log = Log.repository.getLogger("Sync.CollectionKeyManager"); -} - -// TODO: persist this locally as an Identity. Bug 610913. -// Note that the last modified time needs to be preserved. -CollectionKeyManager.prototype = { - - // Return information about old vs new keys: - // * same: true if two collections are equal - // * changed: an array of collection names that changed. - _compareKeyBundleCollections: function _compareKeyBundleCollections(m1, m2) { - let changed = []; - - function process(m1, m2) { - for (let k1 in m1) { - let v1 = m1[k1]; - let v2 = m2[k1]; - if (!(v1 && v2 && v1.equals(v2))) - changed.push(k1); - } - } - - // Diffs both ways. - process(m1, m2); - process(m2, m1); - - // Return a sorted, unique array. - changed.sort(); - let last; - changed = changed.filter(x => (x != last) && (last = x)); - return {same: changed.length == 0, - changed: changed}; - }, - - get isClear() { - return !this._default; - }, - - clear: function clear() { - this._log.info("Clearing collection keys..."); - this.lastModified = 0; - this._collections = {}; - this._default = null; - }, - - keyForCollection: function(collection) { - if (collection && this._collections[collection]) - return this._collections[collection]; - - return this._default; - }, - - /** - * If `collections` (an array of strings) is provided, iterate - * over it and generate random keys for each collection. - * Create a WBO for the given data. - */ - _makeWBO: function(collections, defaultBundle) { - let wbo = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); - let c = {}; - for (let k in collections) { - c[k] = collections[k].keyPairB64; - } - wbo.cleartext = { - "default": defaultBundle ? defaultBundle.keyPairB64 : null, - "collections": c, - "collection": CRYPTO_COLLECTION, - "id": KEYS_WBO - }; - return wbo; - }, - - /** - * Create a WBO for the current keys. - */ - asWBO: function(collection, id) - this._makeWBO(this._collections, this._default), - - /** - * Compute a new default key, and new keys for any specified collections. - */ - newKeys: function(collections) { - let newDefaultKey = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME); - newDefaultKey.generateRandom(); - - let newColls = {}; - if (collections) { - collections.forEach(function (c) { - let b = new BulkKeyBundle(c); - b.generateRandom(); - newColls[c] = b; - }); - } - return [newDefaultKey, newColls]; - }, - - /** - * Generates new keys, but does not replace our local copy. Use this to - * verify an upload before storing. - */ - generateNewKeysWBO: function(collections) { - let newDefaultKey, newColls; - [newDefaultKey, newColls] = this.newKeys(collections); - - return this._makeWBO(newColls, newDefaultKey); - }, - - // Take the fetched info/collections WBO, checking the change - // time of the crypto collection. - updateNeeded: function(info_collections) { - - this._log.info("Testing for updateNeeded. Last modified: " + this.lastModified); - - // No local record of modification time? Need an update. - if (!this.lastModified) - return true; - - // No keys on the server? We need an update, though our - // update handling will be a little more drastic... - if (!(CRYPTO_COLLECTION in info_collections)) - return true; - - // Otherwise, we need an update if our modification time is stale. - return (info_collections[CRYPTO_COLLECTION] > this.lastModified); - }, - - // - // Set our keys and modified time to the values fetched from the server. - // Returns one of three values: - // - // * If the default key was modified, return true. - // * If the default key was not modified, but per-collection keys were, - // return an array of such. - // * Otherwise, return false -- we were up-to-date. - // - setContents: function setContents(payload, modified) { - - if (!modified) - throw "No modified time provided to setContents."; - - let self = this; - - this._log.info("Setting collection keys contents. Our last modified: " + - this.lastModified + ", input modified: " + modified + "."); - - if (!payload) - throw "No payload in CollectionKeyManager.setContents()."; - - if (!payload.default) { - this._log.warn("No downloaded default key: this should not occur."); - this._log.warn("Not clearing local keys."); - throw "No default key in CollectionKeyManager.setContents(). Cannot proceed."; - } - - // Process the incoming default key. - let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME); - b.keyPairB64 = payload.default; - let newDefault = b; - - // Process the incoming collections. - let newCollections = {}; - if ("collections" in payload) { - this._log.info("Processing downloaded per-collection keys."); - let colls = payload.collections; - for (let k in colls) { - let v = colls[k]; - if (v) { - let keyObj = new BulkKeyBundle(k); - keyObj.keyPairB64 = v; - if (keyObj) { - newCollections[k] = keyObj; - } - } - } - } - - // Check to see if these are already our keys. - let sameDefault = (this._default && this._default.equals(newDefault)); - let collComparison = this._compareKeyBundleCollections(newCollections, this._collections); - let sameColls = collComparison.same; - - if (sameDefault && sameColls) { - self._log.info("New keys are the same as our old keys! Bumped local modified time."); - self.lastModified = modified; - return false; - } - - // Make sure things are nice and tidy before we set. - this.clear(); - - this._log.info("Saving downloaded keys."); - this._default = newDefault; - this._collections = newCollections; - - // Always trust the server. - self._log.info("Bumping last modified to " + modified); - self.lastModified = modified; - - return sameDefault ? collComparison.changed : true; - }, - - updateContents: function updateContents(syncKeyBundle, storage_keys) { - let log = this._log; - log.info("Updating collection keys..."); - - // storage_keys is a WBO, fetched from storage/crypto/keys. - // Its payload is the default key, and a map of collections to keys. - // We lazily compute the key objects from the strings we're given. - - let payload; - try { - payload = storage_keys.decrypt(syncKeyBundle); - } catch (ex) { - log.warn("Got exception \"" + ex + "\" decrypting storage keys with sync key."); - log.info("Aborting updateContents. Rethrowing."); - throw ex; - } - - let r = this.setContents(payload, storage_keys.modified); - log.info("Collection keys updated."); - return r; - } -} - -this.Collection = function Collection(uri, recordObj, service) { - if (!service) { - throw new Error("Collection constructor requires a service."); - } - - Resource.call(this, uri); - - // This is a bit hacky, but gets the job done. - let res = service.resource(uri); - this.authenticator = res.authenticator; - - this._recordObj = recordObj; - this._service = service; - - this._full = false; - this._ids = null; - this._limit = 0; - this._older = 0; - this._newer = 0; - this._data = []; -} -Collection.prototype = { - __proto__: Resource.prototype, - _logName: "Sync.Collection", - - _rebuildURL: function Coll__rebuildURL() { - // XXX should consider what happens if it's not a URL... - this.uri.QueryInterface(Ci.nsIURL); - - let args = []; - if (this.older) - args.push('older=' + this.older); - else if (this.newer) { - args.push('newer=' + this.newer); - } - if (this.full) - args.push('full=1'); - if (this.sort) - args.push('sort=' + this.sort); - if (this.ids != null) - args.push("ids=" + this.ids); - if (this.limit > 0 && this.limit != Infinity) - args.push("limit=" + this.limit); - - this.uri.query = (args.length > 0)? '?' + args.join('&') : ''; - }, - - // get full items - get full() { return this._full; }, - set full(value) { - this._full = value; - this._rebuildURL(); - }, - - // Apply the action to a certain set of ids - get ids() this._ids, - set ids(value) { - this._ids = value; - this._rebuildURL(); - }, - - // Limit how many records to get - get limit() this._limit, - set limit(value) { - this._limit = value; - this._rebuildURL(); - }, - - // get only items modified before some date - get older() { return this._older; }, - set older(value) { - this._older = value; - this._rebuildURL(); - }, - - // get only items modified since some date - get newer() { return this._newer; }, - set newer(value) { - this._newer = value; - this._rebuildURL(); - }, - - // get items sorted by some criteria. valid values: - // oldest (oldest first) - // newest (newest first) - // index - get sort() { return this._sort; }, - set sort(value) { - this._sort = value; - this._rebuildURL(); - }, - - pushData: function Coll_pushData(data) { - this._data.push(data); - }, - - clearRecords: function Coll_clearRecords() { - this._data = []; - }, - - set recordHandler(onRecord) { - // Save this because onProgress is called with this as the ChannelListener - let coll = this; - - // Switch to newline separated records for incremental parsing - coll.setHeader("Accept", "application/newlines"); - - this._onProgress = function() { - let newline; - while ((newline = this._data.indexOf("\n")) > 0) { - // Split the json record from the rest of the data - let json = this._data.slice(0, newline); - this._data = this._data.slice(newline + 1); - - // Deserialize a record from json and give it to the callback - let record = new coll._recordObj(); - record.deserialize(json); - onRecord(record); - } - }; - }, -}; diff --git a/components/weave/src/sync/resource.js b/components/weave/src/sync/resource.js deleted file mode 100644 index bc1d639f6..000000000 --- a/components/weave/src/sync/resource.js +++ /dev/null @@ -1,678 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = [ - "AsyncResource", - "Resource" -]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Async.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/util.js"); - -const DEFAULT_LOAD_FLAGS = - // Always validate the cache: - Ci.nsIRequest.LOAD_BYPASS_CACHE | - Ci.nsIRequest.INHIBIT_CACHING | - // Don't send user cookies over the wire (Bug 644734). - Ci.nsIRequest.LOAD_ANONYMOUS; - -/* - * AsyncResource represents a remote network resource, identified by a URI. - * Create an instance like so: - * - * let resource = new AsyncResource("http://foobar.com/path/to/resource"); - * - * The 'resource' object has the following methods to issue HTTP requests - * of the corresponding HTTP methods: - * - * get(callback) - * put(data, callback) - * post(data, callback) - * delete(callback) - * - * 'callback' is a function with the following signature: - * - * function callback(error, result) {...} - * - * 'error' will be null on successful requests. Likewise, result will not be - * passed (=undefined) when an error occurs. Note that this is independent of - * the status of the HTTP response. - */ -this.AsyncResource = function AsyncResource(uri) { - this._log = Log.repository.getLogger(this._logName); - this._log.level = - Log.Level[Svc.Prefs.get("log.logger.network.resources")]; - this.uri = uri; - this._headers = {}; - this._onComplete = Utils.bind2(this, this._onComplete); -} -AsyncResource.prototype = { - _logName: "Sync.AsyncResource", - - // ** {{{ AsyncResource.serverTime }}} ** - // - // Caches the latest server timestamp (X-Weave-Timestamp header). - serverTime: null, - - /** - * Callback to be invoked at request time to add authentication details. - * - * By default, a global authenticator is provided. If this is set, it will - * be used instead of the global one. - */ - authenticator: null, - - // The string to use as the base User-Agent in Sync requests. - // These strings will look something like - // - // Firefox/4.0 FxSync/1.8.0.20100101.mobile - // - // or - // - // Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop - // - _userAgent: - Services.appinfo.name + "/" + Services.appinfo.version + // Product. - " FxSync/" + WEAVE_VERSION + "." + // Sync. - Services.appinfo.appBuildID + ".", // Build. - - // Wait 5 minutes before killing a request. - ABORT_TIMEOUT: 300000, - - // ** {{{ AsyncResource.headers }}} ** - // - // Headers to be included when making a request for the resource. - // Note: Header names should be all lower case, there's no explicit - // check for duplicates due to case! - get headers() { - return this._headers; - }, - set headers(value) { - this._headers = value; - }, - setHeader: function Res_setHeader(header, value) { - this._headers[header.toLowerCase()] = value; - }, - get headerNames() { - return Object.keys(this.headers); - }, - - // ** {{{ AsyncResource.uri }}} ** - // - // URI representing this resource. - get uri() { - return this._uri; - }, - set uri(value) { - if (typeof value == 'string') - this._uri = CommonUtils.makeURI(value); - else - this._uri = value; - }, - - // ** {{{ AsyncResource.spec }}} ** - // - // Get the string representation of the URI. - get spec() { - if (this._uri) - return this._uri.spec; - return null; - }, - - // ** {{{ AsyncResource.data }}} ** - // - // Get and set the data encapulated in the resource. - _data: null, - get data() this._data, - set data(value) { - this._data = value; - }, - - // ** {{{ AsyncResource._createRequest }}} ** - // - // This method returns a new IO Channel for requests to be made - // through. It is never called directly, only {{{_doRequest}}} uses it - // to obtain a request channel. - // - _createRequest: function Res__createRequest(method) { - let channel = Services.io.newChannel2(this.spec, - null, - null, - null, // aLoadingNode - Services.scriptSecurityManager.getSystemPrincipal(), - null, // aTriggeringPrincipal - Ci.nsILoadInfo.SEC_NORMAL, - Ci.nsIContentPolicy.TYPE_OTHER) - .QueryInterface(Ci.nsIRequest) - .QueryInterface(Ci.nsIHttpChannel); - - channel.loadFlags |= DEFAULT_LOAD_FLAGS; - - // Setup a callback to handle channel notifications. - let listener = new ChannelNotificationListener(this.headerNames); - channel.notificationCallbacks = listener; - - // Compose a UA string fragment from the various available identifiers. - if (Svc.Prefs.get("sendVersionInfo", true)) { - let ua = this._userAgent + Svc.Prefs.get("client.type", "desktop"); - channel.setRequestHeader("user-agent", ua, false); - } - - let headers = this.headers; - - if (this.authenticator) { - let result = this.authenticator(this, method); - if (result && result.headers) { - for (let [k, v] of Object.entries(result.headers)) { - headers[k.toLowerCase()] = v; - } - } - } else { - this._log.debug("No authenticator found."); - } - - for (let [key, value] of Object.entries(headers)) { - if (key == 'authorization') - this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); - else - this._log.trace("HTTP Header " + key + ": " + headers[key]); - channel.setRequestHeader(key, headers[key], false); - } - return channel; - }, - - _onProgress: function Res__onProgress(channel) {}, - - _doRequest: function _doRequest(action, data, callback) { - this._log.trace("In _doRequest."); - this._callback = callback; - let channel = this._createRequest(action); - - if ("undefined" != typeof(data)) - this._data = data; - - // PUT and POST are treated differently because they have payload data. - if ("PUT" == action || "POST" == action) { - // Convert non-string bodies into JSON - if (this._data.constructor.toString() != String) - this._data = JSON.stringify(this._data); - - this._log.debug(action + " Length: " + this._data.length); - this._log.trace(action + " Body: " + this._data); - - let type = ('content-type' in this._headers) ? - this._headers['content-type'] : 'text/plain'; - - let stream = Cc["@mozilla.org/io/string-input-stream;1"]. - createInstance(Ci.nsIStringInputStream); - stream.setData(this._data, this._data.length); - - channel.QueryInterface(Ci.nsIUploadChannel); - channel.setUploadStream(stream, type, this._data.length); - } - - // Setup a channel listener so that the actual network operation - // is performed asynchronously. - let listener = new ChannelListener(this._onComplete, this._onProgress, - this._log, this.ABORT_TIMEOUT); - channel.requestMethod = action; - try { - channel.asyncOpen(listener, null); - } catch (ex) { - // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. - this._log.warn("Caught an error in asyncOpen: ", ex); - CommonUtils.nextTick(callback.bind(this, ex)); - } - }, - - _onComplete: function _onComplete(error, data, channel) { - this._log.trace("In _onComplete. Error is " + error + "."); - - if (error) { - this._callback(error); - return; - } - - this._data = data; - let action = channel.requestMethod; - - this._log.trace("Channel: " + channel); - this._log.trace("Action: " + action); - - // Process status and success first. This way a problem with headers - // doesn't fail to include accurate status information. - let status = 0; - let success = false; - - try { - status = channel.responseStatus; - success = channel.requestSucceeded; // HTTP status. - - this._log.trace("Status: " + status); - this._log.trace("Success: " + success); - - // Log the status of the request. - let mesg = [action, success ? "success" : "fail", status, - channel.URI.spec].join(" "); - this._log.debug("mesg: " + mesg); - - if (mesg.length > 200) - mesg = mesg.substr(0, 200) + "…"; - this._log.debug(mesg); - - // Additionally give the full response body when Trace logging. - if (this._log.level <= Log.Level.Trace) - this._log.trace(action + " body: " + data); - - } catch(ex) { - // Got a response, but an exception occurred during processing. - // This shouldn't occur. - this._log.warn("Caught unexpected exception in _onComplete. ", ex); - this._log.debug(CommonUtils.stackTrace(ex)); - } - - // Process headers. They can be empty, or the call can otherwise fail, so - // put this in its own try block. - let headers = {}; - try { - this._log.trace("Processing response headers."); - - // Read out the response headers if available. - channel.visitResponseHeaders({ - visitHeader: function visitHeader(header, value) { - headers[header.toLowerCase()] = value; - } - }); - - // This is a server-side safety valve to allow slowing down - // clients without hurting performance. - if (headers["x-weave-backoff"]) { - let backoff = headers["x-weave-backoff"]; - this._log.debug("Got X-Weave-Backoff: " + backoff); - Observers.notify("weave:service:backoff:interval", - parseInt(backoff, 10)); - } - - if (success && headers["x-weave-quota-remaining"]) { - Observers.notify("weave:service:quota:remaining", - parseInt(headers["x-weave-quota-remaining"], 10)); - } - - let contentLength = headers["content-length"]; - if (success && contentLength && data && - contentLength != data.length) { - this._log.warn("The response body's length of: " + data.length + - " doesn't match the header's content-length of: " + - contentLength + "."); - } - } catch (ex) { - this._log.debug("Caught exception visiting headers in _onComplete", ex); - this._log.debug(CommonUtils.stackTrace(ex)); - } - - let ret = new String(data); - ret.status = status; - ret.success = success; - ret.headers = headers; - - // Make a lazy getter to convert the json response into an object. - // Note that this can cause a parse error to be thrown far away from the - // actual fetch, so be warned! - XPCOMUtils.defineLazyGetter(ret, "obj", function() { - try { - return JSON.parse(ret); - } catch (ex) { - this._log.warn("Got exception parsing response body", ex); - // Stringify to avoid possibly printing non-printable characters. - this._log.debug("Parse fail: Response body starts: \"" + - JSON.stringify((ret + "").slice(0, 100)) + - "\"."); - throw ex; - } - }.bind(this)); - - this._callback(null, ret); - }, - - get: function get(callback) { - this._doRequest("GET", undefined, callback); - }, - - put: function put(data, callback) { - if (typeof data == "function") - [data, callback] = [undefined, data]; - this._doRequest("PUT", data, callback); - }, - - post: function post(data, callback) { - if (typeof data == "function") - [data, callback] = [undefined, data]; - this._doRequest("POST", data, callback); - }, - - delete: function delete_(callback) { - this._doRequest("DELETE", undefined, callback); - } -}; - - -/* - * Represent a remote network resource, identified by a URI, with a - * synchronous API. - * - * 'Resource' is not recommended for new code. Use the asynchronous API of - * 'AsyncResource' instead. - */ -this.Resource = function Resource(uri) { - AsyncResource.call(this, uri); -} -Resource.prototype = { - - __proto__: AsyncResource.prototype, - - _logName: "Sync.Resource", - - // ** {{{ Resource._request }}} ** - // - // Perform a particular HTTP request on the resource. This method - // is never called directly, but is used by the high-level - // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. - _request: function Res__request(action, data) { - let cb = Async.makeSyncCallback(); - function callback(error, ret) { - if (error) - cb.throw(error); - else - cb(ret); - } - - // The channel listener might get a failure code - try { - this._doRequest(action, data, callback); - return Async.waitForSyncCallback(cb); - } catch(ex) { - // Combine the channel stack with this request stack. Need to create - // a new error object for that. - let error = Error(ex.message); - error.result = ex.result; - let chanStack = []; - if (ex.stack) - chanStack = ex.stack.trim().split(/\n/).slice(1); - let requestStack = error.stack.split(/\n/).slice(1); - - // Strip out the args for the last 2 frames because they're usually HUGE! - for (let i = 0; i <= 1; i++) - requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@"); - - error.stack = chanStack.concat(requestStack).join("\n"); - throw error; - } - }, - - // ** {{{ Resource.get }}} ** - // - // Perform an asynchronous HTTP GET for this resource. - get: function Res_get() { - return this._request("GET"); - }, - - // ** {{{ Resource.put }}} ** - // - // Perform a HTTP PUT for this resource. - put: function Res_put(data) { - return this._request("PUT", data); - }, - - // ** {{{ Resource.post }}} ** - // - // Perform a HTTP POST for this resource. - post: function Res_post(data) { - return this._request("POST", data); - }, - - // ** {{{ Resource.delete }}} ** - // - // Perform a HTTP DELETE for this resource. - delete: function Res_delete() { - return this._request("DELETE"); - } -}; - -// = ChannelListener = -// -// This object implements the {{{nsIStreamListener}}} interface -// and is called as the network operation proceeds. -function ChannelListener(onComplete, onProgress, logger, timeout) { - this._onComplete = onComplete; - this._onProgress = onProgress; - this._log = logger; - this._timeout = timeout; - this.delayAbort(); -} -ChannelListener.prototype = { - - onStartRequest: function Channel_onStartRequest(channel) { - this._log.trace("onStartRequest called for channel " + channel + "."); - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); - channel.cancel(Cr.NS_BINDING_ABORTED); - return; - } - - // Save the latest server timestamp when possible. - try { - AsyncResource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0; - } - catch(ex) {} - - this._log.trace("onStartRequest: " + channel.requestMethod + " " + - channel.URI.spec); - this._data = ''; - this.delayAbort(); - }, - - onStopRequest: function Channel_onStopRequest(channel, context, status) { - // Clear the abort timer now that the channel is done. - this.abortTimer.clear(); - - if (!this._onComplete) { - this._log.error("Unexpected error: _onComplete not defined in onStopRequest."); - this._onProgress = null; - return; - } - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); - - this._onComplete(ex, this._data, channel); - this._onComplete = this._onProgress = null; - return; - } - - let statusSuccess = Components.isSuccessCode(status); - let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; - this._log.trace("Channel for " + channel.requestMethod + " " + uri + ": " + - "isSuccessCode(" + status + ")? " + statusSuccess); - - if (this._data == '') { - this._data = null; - } - - // Pass back the failure code and stop execution. Use Components.Exception() - // instead of Error() so the exception is QI-able and can be passed across - // XPCOM borders while preserving the status code. - if (!statusSuccess) { - let message = Components.Exception("", status).name; - let error = Components.Exception(message, status); - - this._onComplete(error, undefined, channel); - this._onComplete = this._onProgress = null; - return; - } - - this._log.trace("Channel: flags = " + channel.loadFlags + - ", URI = " + uri + - ", HTTP success? " + channel.requestSucceeded); - this._onComplete(null, this._data, channel); - this._onComplete = this._onProgress = null; - }, - - onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { - let siStream; - try { - siStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); - siStream.init(stream); - } catch (ex) { - this._log.warn("Exception creating nsIScriptableInputStream", ex); - this._log.debug("Parameters: " + req.URI.spec + ", " + stream + ", " + off + ", " + count); - // Cannot proceed, so rethrow and allow the channel to cancel itself. - throw ex; - } - - try { - this._data += siStream.read(count); - } catch (ex) { - this._log.warn("Exception thrown reading " + count + " bytes from " + siStream + "."); - throw ex; - } - - try { - this._onProgress(); - } catch (ex) { - this._log.warn("Got exception calling onProgress handler during fetch of " - + req.URI.spec, ex); - this._log.trace("Rethrowing; expect a failure code from the HTTP channel."); - throw ex; - } - - this.delayAbort(); - }, - - /** - * Create or push back the abort timer that kills this request. - */ - delayAbort: function delayAbort() { - try { - CommonUtils.namedTimer(this.abortRequest, this._timeout, this, "abortTimer"); - } catch (ex) { - this._log.warn("Got exception extending abort timer: ", ex); - } - }, - - abortRequest: function abortRequest() { - // Ignore any callbacks if we happen to get any now - this.onStopRequest = function() {}; - let error = Components.Exception("Aborting due to channel inactivity.", - Cr.NS_ERROR_NET_TIMEOUT); - if (!this._onComplete) { - this._log.error("Unexpected error: _onComplete not defined in " + - "abortRequest."); - return; - } - this._onComplete(error); - } -}; - -/** - * This class handles channel notification events. - * - * An instance of this class is bound to each created channel. - * - * Optionally pass an array of header names. Each header named - * in this array will be copied between the channels in the - * event of a redirect. - */ -function ChannelNotificationListener(headersToCopy) { - this._headersToCopy = headersToCopy; - - this._log = Log.repository.getLogger(this._logName); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.network.resources")]; -} -ChannelNotificationListener.prototype = { - _logName: "Sync.Resource", - - getInterface: function(aIID) { - return this.QueryInterface(aIID); - }, - - QueryInterface: function(aIID) { - if (aIID.equals(Ci.nsIBadCertListener2) || - aIID.equals(Ci.nsIInterfaceRequestor) || - aIID.equals(Ci.nsISupports) || - aIID.equals(Ci.nsIChannelEventSink)) - return this; - - throw Cr.NS_ERROR_NO_INTERFACE; - }, - - notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) { - let log = Log.repository.getLogger("Sync.CertListener"); - log.warn("Invalid HTTPS certificate encountered!"); - - // This suppresses the UI warning only. The request is still cancelled. - return true; - }, - - asyncOnChannelRedirect: - function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { - - let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>"; - let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>"; - this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags); - - this._log.debug("Ensuring load flags are set."); - newChannel.loadFlags |= DEFAULT_LOAD_FLAGS; - - // For internal redirects, copy the headers that our caller set. - try { - if ((flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL) && - newChannel.URI.equals(oldChannel.URI)) { - this._log.debug("Copying headers for safe internal redirect."); - - // QI the channel so we can set headers on it. - try { - newChannel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); - throw ex; - } - - for (let header of this._headersToCopy) { - let value = oldChannel.getRequestHeader(header); - if (value) { - let printed = (header == "authorization") ? "****" : value; - this._log.debug("Header: " + header + " = " + printed); - newChannel.setRequestHeader(header, value, false); - } else { - this._log.warn("No value for header " + header); - } - } - } - } catch (ex) { - this._log.error("Error copying headers: ", ex); - } - - // We let all redirects proceed. - try { - callback.onRedirectVerifyCallback(Cr.NS_OK); - } catch (ex) { - this._log.error("onRedirectVerifyCallback threw!" + CommonUtils.exceptionStr(ex)); - } - } -}; diff --git a/components/weave/src/sync/rest.js b/components/weave/src/sync/rest.js deleted file mode 100644 index 106ece222..000000000 --- a/components/weave/src/sync/rest.js +++ /dev/null @@ -1,106 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-sync/util.js"); -Cu.import("resource://services-sync/constants.js"); - -this.EXPORTED_SYMBOLS = ["SyncStorageRequest"]; - -const STORAGE_REQUEST_TIMEOUT = 5 * 60; // 5 minutes - -/** - * RESTRequest variant for use against a Sync storage server. - */ -this.SyncStorageRequest = function SyncStorageRequest(uri) { - RESTRequest.call(this, uri); - - this.authenticator = null; -} -SyncStorageRequest.prototype = { - - __proto__: RESTRequest.prototype, - - _logName: "Sync.StorageRequest", - - /** - * The string to use as the base User-Agent in Sync requests. - * These strings will look something like - * - * Firefox/4.0 FxSync/1.8.0.20100101.mobile - * - * or - * - * Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop - */ - userAgent: - Services.appinfo.name + "/" + Services.appinfo.version + // Product. - " FxSync/" + WEAVE_VERSION + "." + // Sync. - Services.appinfo.appBuildID + ".", // Build. - - /** - * Wait 5 minutes before killing a request. - */ - timeout: STORAGE_REQUEST_TIMEOUT, - - dispatch: function dispatch(method, data, onComplete, onProgress) { - // Compose a UA string fragment from the various available identifiers. - if (Svc.Prefs.get("sendVersionInfo", true)) { - let ua = this.userAgent + Svc.Prefs.get("client.type", "desktop"); - this.setHeader("user-agent", ua); - } - - if (this.authenticator) { - this.authenticator(this); - } else { - this._log.debug("No authenticator found."); - } - - return RESTRequest.prototype.dispatch.apply(this, arguments); - }, - - onStartRequest: function onStartRequest(channel) { - RESTRequest.prototype.onStartRequest.call(this, channel); - if (this.status == this.ABORTED) { - return; - } - - let headers = this.response.headers; - // Save the latest server timestamp when possible. - if (headers["x-weave-timestamp"]) { - SyncStorageRequest.serverTime = parseFloat(headers["x-weave-timestamp"]); - } - - // This is a server-side safety valve to allow slowing down - // clients without hurting performance. - if (headers["x-weave-backoff"]) { - Svc.Obs.notify("weave:service:backoff:interval", - parseInt(headers["x-weave-backoff"], 10)); - } - - if (this.response.success && headers["x-weave-quota-remaining"]) { - Svc.Obs.notify("weave:service:quota:remaining", - parseInt(headers["x-weave-quota-remaining"], 10)); - } - }, - - onStopRequest: function onStopRequest(channel, context, statusCode) { - if (this.status != this.ABORTED) { - let resp = this.response; - let contentLength = resp.headers ? resp.headers["content-length"] : ""; - - if (resp.success && contentLength && - contentLength != resp.body.length) { - this._log.warn("The response body's length of: " + resp.body.length + - " doesn't match the header's content-length of: " + - contentLength + "."); - } - } - - RESTRequest.prototype.onStopRequest.apply(this, arguments); - } -}; diff --git a/components/weave/src/sync/service.js b/components/weave/src/sync/service.js deleted file mode 100644 index 1ced636d1..000000000 --- a/components/weave/src/sync/service.js +++ /dev/null @@ -1,1586 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["Service"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -// How long before refreshing the cluster -const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes - -// How long a key to generate from an old passphrase. -const PBKDF2_KEY_BYTES = 16; - -const CRYPTO_COLLECTION = "crypto"; -const KEYS_WBO = "keys"; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://services-sync/engines.js"); -Cu.import("resource://services-sync/engines/clients.js"); -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://services-sync/policies.js"); -Cu.import("resource://services-sync/record.js"); -Cu.import("resource://services-sync/resource.js"); -Cu.import("resource://services-sync/rest.js"); -Cu.import("resource://services-sync/stages/enginesync.js"); -Cu.import("resource://services-sync/stages/declined.js"); -Cu.import("resource://services-sync/status.js"); -Cu.import("resource://services-sync/userapi.js"); -Cu.import("resource://services-sync/util.js"); - -const ENGINE_MODULES = { - Addons: "addons.js", - Bookmarks: "bookmarks.js", - Form: "forms.js", - History: "history.js", - Password: "passwords.js", - Prefs: "prefs.js", - Tab: "tabs.js", -}; - -const STORAGE_INFO_TYPES = [INFO_COLLECTIONS, - INFO_COLLECTION_USAGE, - INFO_COLLECTION_COUNTS, - INFO_QUOTA]; - -function Sync11Service() { - this._notify = Utils.notify("weave:service:"); -} -Sync11Service.prototype = { - - _lock: Utils.lock, - _locked: false, - _loggedIn: false, - - infoURL: null, - storageURL: null, - metaURL: null, - cryptoKeyURL: null, - - get serverURL() Svc.Prefs.get("serverURL"), - set serverURL(value) { - if (!value.endsWith("/")) { - value += "/"; - } - - // Only do work if it's actually changing - if (value == this.serverURL) - return; - - // A new server most likely uses a different cluster, so clear that - Svc.Prefs.set("serverURL", value); - Svc.Prefs.reset("clusterURL"); - }, - - get clusterURL() Svc.Prefs.get("clusterURL", ""), - set clusterURL(value) { - Svc.Prefs.set("clusterURL", value); - this._updateCachedURLs(); - }, - - get miscAPI() { - // Append to the serverURL if it's a relative fragment - let misc = Svc.Prefs.get("miscURL"); - if (misc.indexOf(":") == -1) - misc = this.serverURL + misc; - return misc + MISC_API_VERSION + "/"; - }, - - /** - * The URI of the User API service. - * - * This is the base URI of the service as applicable to all users up to - * and including the server version path component, complete with trailing - * forward slash. - */ - get userAPIURI() { - // Append to the serverURL if it's a relative fragment. - let url = Svc.Prefs.get("userURL"); - if (!url.includes(":")) { - url = this.serverURL + url; - } - - return url + USER_API_VERSION + "/"; - }, - - get pwResetURL() { - return this.serverURL + "weave-password-reset"; - }, - - get syncID() { - // Generate a random syncID id we don't have one - let syncID = Svc.Prefs.get("client.syncID", ""); - return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; - }, - set syncID(value) { - Svc.Prefs.set("client.syncID", value); - }, - - get isLoggedIn() { return this._loggedIn; }, - - get locked() { return this._locked; }, - lock: function lock() { - if (this._locked) - return false; - this._locked = true; - return true; - }, - unlock: function unlock() { - this._locked = false; - }, - - // A specialized variant of Utils.catch. - // This provides a more informative error message when we're already syncing: - // see Bug 616568. - _catch: function _catch(func) { - function lockExceptions(ex) { - if (Utils.isLockException(ex)) { - // This only happens if we're syncing already. - this._log.info("Cannot start sync: already syncing?"); - } - } - - return Utils.catch.call(this, func, lockExceptions); - }, - - get userBaseURL() { - if (!this._clusterManager) { - return null; - } - return this._clusterManager.getUserBaseURL(); - }, - - _updateCachedURLs: function _updateCachedURLs() { - // Nothing to cache yet if we don't have the building blocks - if (!this.clusterURL || !this.identity.username) - return; - - this._log.debug("Caching URLs under storage user base: " + this.userBaseURL); - - // Generate and cache various URLs under the storage API for this user - this.infoURL = this.userBaseURL + "info/collections"; - this.storageURL = this.userBaseURL + "storage/"; - this.metaURL = this.storageURL + "meta/global"; - this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO; - }, - - _checkCrypto: function _checkCrypto() { - let ok = false; - - try { - let iv = Svc.Crypto.generateRandomIV(); - if (iv.length == 24) - ok = true; - - } catch (e) { - this._log.debug("Crypto check failed: " + e); - } - - return ok; - }, - - /** - * Here is a disgusting yet reasonable way of handling HMAC errors deep in - * the guts of Sync. The astute reader will note that this is a hacky way of - * implementing something like continuable conditions. - * - * A handler function is glued to each engine. If the engine discovers an - * HMAC failure, we fetch keys from the server and update our keys, just as - * we would on startup. - * - * If our key collection changed, we signal to the engine (via our return - * value) that it should retry decryption. - * - * If our key collection did not change, it means that we already had the - * correct keys... and thus a different client has the wrong ones. Reupload - * the bundle that we fetched, which will bump the modified time on the - * server and (we hope) prompt a broken client to fix itself. - * - * We keep track of the time at which we last applied this reasoning, because - * thrashing doesn't solve anything. We keep a reasonable interval between - * these remedial actions. - */ - lastHMACEvent: 0, - - /* - * Returns whether to try again. - */ - handleHMACEvent: function handleHMACEvent() { - let now = Date.now(); - - // Leave a sizable delay between HMAC recovery attempts. This gives us - // time for another client to fix themselves if we touch the record. - if ((now - this.lastHMACEvent) < HMAC_EVENT_INTERVAL) - return false; - - this._log.info("Bad HMAC event detected. Attempting recovery " + - "or signaling to other clients."); - - // Set the last handled time so that we don't act again. - this.lastHMACEvent = now; - - // Fetch keys. - let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); - try { - let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; - - // Save out the ciphertext for when we reupload. If there's a bug in - // CollectionKeyManager, this will prevent us from uploading junk. - let cipherText = cryptoKeys.ciphertext; - - if (!cryptoResp.success) { - this._log.warn("Failed to download keys."); - return false; - } - - let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, - cryptoKeys, true); - if (keysChanged) { - // Did they change? If so, carry on. - this._log.info("Suggesting retry."); - return true; // Try again. - } - - // If not, reupload them and continue the current sync. - cryptoKeys.ciphertext = cipherText; - cryptoKeys.cleartext = null; - - let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL)); - if (uploadResp.success) - this._log.info("Successfully re-uploaded keys. Continuing sync."); - else - this._log.warn("Got error response re-uploading keys. " + - "Continuing sync; let's try again later."); - - return false; // Don't try again: same keys. - - } catch (ex) { - this._log.warn("Got exception \"" + ex + "\" fetching and handling " + - "crypto keys. Will try again later."); - return false; - } - }, - - handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) { - // Don't want to wipe if we're just starting up! - let wasBlank = this.collectionKeys.isClear; - let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys); - - if (keysChanged && !wasBlank) { - this._log.debug("Keys changed: " + JSON.stringify(keysChanged)); - - if (!skipReset) { - this._log.info("Resetting client to reflect key change."); - - if (keysChanged.length) { - // Collection keys only. Reset individual engines. - this.resetClient(keysChanged); - } - else { - // Default key changed: wipe it all. - this.resetClient(); - } - - this._log.info("Downloaded new keys, client reset. Proceeding."); - } - return true; - } - return false; - }, - - /** - * Prepare to initialize the rest of Weave after waiting a little bit - */ - onStartup: function onStartup() { - this._migratePrefs(); - - // Status is instantiated before us and is the first to grab an instance of - // the IdentityManager. We use that instance because IdentityManager really - // needs to be a singleton. Ideally, the longer-lived object would spawn - // this service instance. - if (!Status || !Status._authManager) { - throw new Error("Status or Status._authManager not initialized."); - } - - this.status = Status; - this.identity = Status._authManager; - this.collectionKeys = new CollectionKeyManager(); - - this.errorHandler = new ErrorHandler(this); - - this._log = Log.repository.getLogger("Sync.Service"); - this._log.level = - Log.Level[Svc.Prefs.get("log.logger.service.main")]; - - this._log.info("Loading Weave " + WEAVE_VERSION); - - this._clusterManager = this.identity.createClusterManager(this); - this.recordManager = new RecordManager(this); - - this.enabled = true; - - this._registerEngines(); - - let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. - getService(Ci.nsIHttpProtocolHandler).userAgent; - this._log.info(ua); - - if (!this._checkCrypto()) { - this.enabled = false; - this._log.info("Could not load the Weave crypto component. Disabling " + - "Weave, since it will not work correctly."); - } - - Svc.Obs.add("weave:service:setup-complete", this); - Svc.Prefs.observe("engine.", this); - - this.scheduler = new SyncScheduler(this); - - if (!this.enabled) { - this._log.info("Firefox Sync disabled."); - } - - this._updateCachedURLs(); - - let status = this._checkSetup(); - if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) { - Svc.Obs.notify("weave:engine:start-tracking"); - } - - // Send an event now that Weave service is ready. We don't do this - // synchronously so that observers can import this module before - // registering an observer. - Utils.nextTick(function onNextTick() { - this.status.ready = true; - - // UI code uses the flag on the XPCOM service so it doesn't have - // to load a bunch of modules. - let xps = Cc["@mozilla.org/weave/service;1"] - .getService(Ci.nsISupports) - .wrappedJSObject; - xps.ready = true; - - Svc.Obs.notify("weave:service:ready"); - }.bind(this)); - }, - - _checkSetup: function _checkSetup() { - if (!this.enabled) { - return this.status.service = STATUS_DISABLED; - } - return this.status.checkSetup(); - }, - - _migratePrefs: function _migratePrefs() { - // Migrate old debugLog prefs. - let logLevel = Svc.Prefs.get("log.appender.debugLog"); - if (logLevel) { - Svc.Prefs.set("log.appender.file.level", logLevel); - Svc.Prefs.reset("log.appender.debugLog"); - } - if (Svc.Prefs.get("log.appender.debugLog.enabled")) { - Svc.Prefs.set("log.appender.file.logOnSuccess", true); - Svc.Prefs.reset("log.appender.debugLog.enabled"); - } - - // Migrate old extensions.weave.* prefs if we haven't already tried. - if (Svc.Prefs.get("migrated", false)) - return; - - // Grab the list of old pref names - let oldPrefBranch = "extensions.weave."; - let oldPrefNames = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefService). - getBranch(oldPrefBranch). - getChildList("", {}); - - // Map each old pref to the current pref branch - let oldPref = new Preferences(oldPrefBranch); - for (let pref of oldPrefNames) - Svc.Prefs.set(pref, oldPref.get(pref)); - - // Remove all the old prefs and remember that we've migrated - oldPref.resetBranch(""); - Svc.Prefs.set("migrated", true); - }, - - /** - * Register the built-in engines for certain applications - */ - _registerEngines: function _registerEngines() { - this.engineManager = new EngineManager(this); - - let engines = []; - // Applications can provide this preference (comma-separated list) - // to specify which engines should be registered on startup. - let pref = Svc.Prefs.get("registerEngines"); - if (pref) { - engines = pref.split(","); - } - - let declined = []; - pref = Svc.Prefs.get("declinedEngines"); - if (pref) { - declined = pref.split(","); - } - - this.clientsEngine = new ClientEngine(this); - - for (let name of engines) { - if (!(name in ENGINE_MODULES)) { - this._log.info("Do not know about engine: " + name); - continue; - } - - let ns = {}; - try { - Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns); - - let engineName = name + "Engine"; - if (!(engineName in ns)) { - this._log.warn("Could not find exported engine instance: " + engineName); - continue; - } - - this.engineManager.register(ns[engineName]); - } catch (ex) { - this._log.warn("Could not register engine " + name + ": ", ex); - } - } - - this.engineManager.setDeclined(declined); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - // nsIObserver - - observe: function observe(subject, topic, data) { - switch (topic) { - case "weave:service:setup-complete": - let status = this._checkSetup(); - if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) - Svc.Obs.notify("weave:engine:start-tracking"); - break; - case "nsPref:changed": - if (this._ignorePrefObserver) - return; - let engine = data.slice((PREFS_BRANCH + "engine.").length); - this._handleEngineStatusChanged(engine); - break; - } - }, - - _handleEngineStatusChanged: function handleEngineDisabled(engine) { - this._log.trace("Status for " + engine + " engine changed."); - if (Svc.Prefs.get("engineStatusChanged." + engine, false)) { - // The enabled status being changed back to what it was before. - Svc.Prefs.reset("engineStatusChanged." + engine); - } else { - // Remember that the engine status changed locally until the next sync. - Svc.Prefs.set("engineStatusChanged." + engine, true); - } - }, - - /** - * Obtain a Resource instance with authentication credentials. - */ - resource: function resource(url) { - let res = new Resource(url); - res.authenticator = this.identity.getResourceAuthenticator(); - - return res; - }, - - /** - * Obtain a SyncStorageRequest instance with authentication credentials. - */ - getStorageRequest: function getStorageRequest(url) { - let request = new SyncStorageRequest(url); - request.authenticator = this.identity.getRESTRequestAuthenticator(); - - return request; - }, - - /** - * Perform the info fetch as part of a login or key fetch, or - * inside engine sync. - */ - _fetchInfo: function (url) { - let infoURL = url || this.infoURL; - - this._log.trace("In _fetchInfo: " + infoURL); - let info; - try { - info = this.resource(infoURL).get(); - } catch (ex) { - this.errorHandler.checkServerError(ex); - throw ex; - } - - // Always check for errors; this is also where we look for X-Weave-Alert. - this.errorHandler.checkServerError(info); - if (!info.success) { - throw "Aborting sync: failed to get collections."; - } - return info; - }, - - verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) { - - this._log.debug("Fetching and verifying -- or generating -- symmetric keys."); - - // Don't allow empty/missing passphrase. - // Furthermore, we assume that our sync key is already upgraded, - // and fail if that assumption is invalidated. - - if (!this.identity.syncKey) { - this.status.login = LOGIN_FAILED_NO_PASSPHRASE; - this.status.sync = CREDENTIALS_CHANGED; - return false; - } - - let syncKeyBundle = this.identity.syncKeyBundle; - if (!syncKeyBundle) { - this._log.error("Sync Key Bundle not set. Invalid Sync Key?"); - - this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; - this.status.sync = CREDENTIALS_CHANGED; - return false; - } - - try { - if (!infoResponse) - infoResponse = this._fetchInfo(); // Will throw an exception on failure. - - // This only applies when the server is already at version 4. - if (infoResponse.status != 200) { - this._log.warn("info/collections returned non-200 response. Failing key fetch."); - this.status.login = LOGIN_FAILED_SERVER_ERROR; - this.errorHandler.checkServerError(infoResponse); - return false; - } - - let infoCollections = infoResponse.obj; - - this._log.info("Testing info/collections: " + JSON.stringify(infoCollections)); - - if (this.collectionKeys.updateNeeded(infoCollections)) { - this._log.info("collection keys reports that a key update is needed."); - - // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this. - - // Fetch storage/crypto/keys. - let cryptoKeys; - - if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) { - try { - cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); - let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; - - if (cryptoResp.success) { - let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys); - return true; - } - else if (cryptoResp.status == 404) { - // On failure, ask to generate new keys and upload them. - // Fall through to the behavior below. - this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating."); - cryptoKeys = null; - } - else { - // Some other problem. - this.status.login = LOGIN_FAILED_SERVER_ERROR; - this.errorHandler.checkServerError(cryptoResp); - this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys."); - return false; - } - } - catch (ex) { - this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys."); - // TODO: Um, what exceptions might we get here? Should we re-throw any? - - // One kind of exception: HMAC failure. - if (Utils.isHMACMismatch(ex)) { - this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; - this.status.sync = CREDENTIALS_CHANGED; - } - else { - // In the absence of further disambiguation or more precise - // failure constants, just report failure. - this.status.login = LOGIN_FAILED; - } - return false; - } - } - else { - this._log.info("... 'crypto' is not a reported collection. Generating new keys."); - } - - if (!cryptoKeys) { - this._log.info("No keys! Generating new ones."); - - // Better make some and upload them, and wipe the server to ensure - // consistency. This is all achieved via _freshStart. - // If _freshStart fails to clear the server or upload keys, it will - // throw. - this._freshStart(); - return true; - } - - // Last-ditch case. - return false; - } - else { - // No update needed: we're good! - return true; - } - - } catch (ex) { - // This means no keys are present, or there's a network error. - this._log.debug("Failed to fetch and verify keys: ", ex); - this.errorHandler.checkServerError(ex); - return false; - } - }, - - verifyLogin: function verifyLogin(allow40XRecovery = true) { - // If the identity isn't ready it might not know the username... - if (!this.identity.readyToAuthenticate) { - this._log.info("Not ready to authenticate in verifyLogin."); - this.status.login = LOGIN_FAILED_NOT_READY; - return false; - } - - if (!this.identity.username) { - this._log.warn("No username in verifyLogin."); - this.status.login = LOGIN_FAILED_NO_USERNAME; - return false; - } - - // Attaching auth credentials to a request requires access to - // passwords, which means that Resource.get can throw MP-related - // exceptions! - // So we ask the identity to verify the login state after unlocking the - // master password (ie, this call is expected to prompt for MP unlock - // if necessary) while we still have control. - let cb = Async.makeSpinningCallback(); - this.identity.unlockAndVerifyAuthState().then( - result => cb(null, result), - cb - ); - let unlockedState = cb.wait(); - this._log.debug("Fetching unlocked auth state returned " + unlockedState); - if (unlockedState != STATUS_OK) { - this.status.login = unlockedState; - return false; - } - - try { - // Make sure we have a cluster to verify against. - // This is a little weird, if we don't get a node we pretend - // to succeed, since that probably means we just don't have storage. - if (this.clusterURL == "" && !this._clusterManager.setCluster()) { - this.status.sync = NO_SYNC_NODE_FOUND; - return true; - } - - // Fetch collection info on every startup. - let test = this.resource(this.infoURL).get(); - - switch (test.status) { - case 200: - // The user is authenticated. - - // We have no way of verifying the passphrase right now, - // so wait until remoteSetup to do so. - // Just make the most trivial checks. - if (!this.identity.syncKey) { - this._log.warn("No passphrase in verifyLogin."); - this.status.login = LOGIN_FAILED_NO_PASSPHRASE; - return false; - } - - // Go ahead and do remote setup, so that we can determine - // conclusively that our passphrase is correct. - if (this._remoteSetup(test)) { - // Username/password verified. - this.status.login = LOGIN_SUCCEEDED; - return true; - } - - this._log.warn("Remote setup failed."); - // Remote setup must have failed. - return false; - - case 401: - this._log.warn("401: login failed."); - // Fall through to the 404 case. - - case 404: - // Check that we're verifying with the correct cluster - if (allow40XRecovery && this._clusterManager.setCluster()) { - return this.verifyLogin(false); - } - - // We must have the right cluster, but the server doesn't expect us - this.status.login = LOGIN_FAILED_LOGIN_REJECTED; - return false; - - default: - // Server didn't respond with something that we expected - this.status.login = LOGIN_FAILED_SERVER_ERROR; - this.errorHandler.checkServerError(test); - return false; - } - } catch (ex) { - // Must have failed on some network issue - this._log.debug("verifyLogin failed: ", ex); - this.status.login = LOGIN_FAILED_NETWORK_ERROR; - this.errorHandler.checkServerError(ex); - return false; - } - }, - - generateNewSymmetricKeys: function generateNewSymmetricKeys() { - this._log.info("Generating new keys WBO..."); - let wbo = this.collectionKeys.generateNewKeysWBO(); - this._log.info("Encrypting new key bundle."); - wbo.encrypt(this.identity.syncKeyBundle); - - this._log.info("Uploading..."); - let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL)); - if (uploadRes.status != 200) { - this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!"); - this.errorHandler.checkServerError(uploadRes); - throw new Error("Unable to upload symmetric keys."); - } - this._log.info("Got status " + uploadRes.status + " uploading keys."); - let serverModified = uploadRes.obj; // Modified timestamp according to server. - this._log.debug("Server reports crypto modified: " + serverModified); - - // Now verify that info/collections shows them! - this._log.debug("Verifying server collection records."); - let info = this._fetchInfo(); - this._log.debug("info/collections is: " + info); - - if (info.status != 200) { - this._log.warn("Non-200 info/collections response. Aborting."); - throw new Error("Unable to upload symmetric keys."); - } - - info = info.obj; - if (!(CRYPTO_COLLECTION in info)) { - this._log.error("Consistency failure: info/collections excludes " + - "crypto after successful upload."); - throw new Error("Symmetric key upload failed."); - } - - // Can't check against local modified: clock drift. - if (info[CRYPTO_COLLECTION] < serverModified) { - this._log.error("Consistency failure: info/collections crypto entry " + - "is stale after successful upload."); - throw new Error("Symmetric key upload failed."); - } - - // Doesn't matter if the timestamp is ahead. - - // Download and install them. - let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); - let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; - if (cryptoResp.status != 200) { - this._log.warn("Failed to download keys."); - throw new Error("Symmetric key download failed."); - } - let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, - cryptoKeys, true); - if (keysChanged) { - this._log.info("Downloaded keys differed, as expected."); - } - }, - - changePassword: function changePassword(newPassword) { - let client = new UserAPI10Client(this.userAPIURI); - let cb = Async.makeSpinningCallback(); - client.changePassword(this.identity.username, - this.identity.basicPassword, newPassword, cb); - - try { - cb.wait(); - } catch (ex) { - this._log.debug("Password change failed: ", ex); - return false; - } - - // Save the new password for requests and login manager. - this.identity.basicPassword = newPassword; - this.persistLogin(); - return true; - }, - - changePassphrase: function changePassphrase(newphrase) { - return this._catch(function doChangePasphrase() { - /* Wipe. */ - this.wipeServer(); - - this.logout(); - - /* Set this so UI is updated on next run. */ - this.identity.syncKey = newphrase; - this.persistLogin(); - - /* We need to re-encrypt everything, so reset. */ - this.resetClient(); - this.collectionKeys.clear(); - - /* Login and sync. This also generates new keys. */ - this.sync(); - - Svc.Obs.notify("weave:service:change-passphrase", true); - - return true; - })(); - }, - - startOver: function startOver() { - this._log.trace("Invoking Service.startOver."); - Svc.Obs.notify("weave:engine:stop-tracking"); - this.status.resetSync(); - - // Deletion doesn't make sense if we aren't set up yet! - if (this.clusterURL != "") { - // Clear client-specific data from the server, including disabled engines. - for (let engine of [this.clientsEngine].concat(this.engineManager.getAll())) { - try { - engine.removeClientData(); - } catch(ex) { - this._log.warn("Deleting client data for " + engine.name + " failed:", ex); - } - } - this._log.debug("Finished deleting client data."); - } else { - this._log.debug("Skipping client data removal: no cluster URL."); - } - - // We want let UI consumers of the following notification know as soon as - // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now - // by emptying the passphrase (we still need the password). - this._log.info("Service.startOver dropping sync key and logging out."); - this.identity.resetSyncKey(); - this.status.login = LOGIN_FAILED_NO_PASSPHRASE; - this.logout(); - Svc.Obs.notify("weave:service:start-over"); - - // Reset all engines and clear keys. - this.resetClient(); - this.collectionKeys.clear(); - this.status.resetBackoff(); - - // Reset Weave prefs. - this._ignorePrefObserver = true; - Svc.Prefs.resetBranch(""); - this._ignorePrefObserver = false; - - Svc.Prefs.set("lastversion", WEAVE_VERSION); - - this.identity.deleteSyncCredentials(); - - // If necessary, reset the identity manager, then re-initialize it so the - // FxA manager is used. This is configurable via a pref - mainly for tests. - let keepIdentity = false; - try { - keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity"); - } catch (_) { /* no such pref */ } - if (keepIdentity) { - Svc.Obs.notify("weave:service:start-over:finish"); - return; - } - - this.identity.finalize().then( - () => { - // an observer so the FxA migration code can take some action before - // the new identity is created. - Svc.Obs.notify("weave:service:start-over:init-identity"); - this.identity.username = ""; - this.status.__authManager = null; - this.identity = Status._authManager; - this._clusterManager = this.identity.createClusterManager(this); - Svc.Obs.notify("weave:service:start-over:finish"); - } - ).then(null, - err => { - this._log.error("startOver failed to re-initialize the identity manager: " + err); - // Still send the observer notification so the current state is - // reflected in the UI. - Svc.Obs.notify("weave:service:start-over:finish"); - } - ); - }, - - persistLogin: function persistLogin() { - try { - this.identity.persistCredentials(true); - } catch (ex) { - this._log.info("Unable to persist credentials: " + ex); - } - }, - - login: function login(username, password, passphrase) { - function onNotify() { - this._loggedIn = false; - if (Services.io.offline) { - this.status.login = LOGIN_FAILED_NETWORK_ERROR; - throw "Application is offline, login should not be called"; - } - - let initialStatus = this._checkSetup(); - if (username) { - this.identity.username = username; - } - if (password) { - this.identity.basicPassword = password; - } - if (passphrase) { - this.identity.syncKey = passphrase; - } - - if (this._checkSetup() == CLIENT_NOT_CONFIGURED) { - throw "Aborting login, client not configured."; - } - - // Ask the identity manager to explicitly login now. - let cb = Async.makeSpinningCallback(); - this.identity.ensureLoggedIn().then(cb, cb); - - // Just let any errors bubble up - they've more context than we do! - cb.wait(); - - // Calling login() with parameters when the client was - // previously not configured means setup was completed. - if (initialStatus == CLIENT_NOT_CONFIGURED - && (username || password || passphrase)) { - Svc.Obs.notify("weave:service:setup-complete"); - } - this._log.info("Logging in the user."); - this._updateCachedURLs(); - - if (!this.verifyLogin()) { - // verifyLogin sets the failure states here. - throw "Login failed: " + this.status.login; - } - - this._loggedIn = true; - - return true; - } - - let notifier = this._notify("login", "", onNotify.bind(this)); - return this._catch(this._lock("service.js: login", notifier))(); - }, - - logout: function logout() { - // If we failed during login, we aren't going to have this._loggedIn set, - // but we still want to ask the identity to logout, so it doesn't try and - // reuse any old credentials next time we sync. - this._log.info("Logging out"); - this.identity.logout(); - this._loggedIn = false; - - Svc.Obs.notify("weave:service:logout:finish"); - }, - - checkAccount: function checkAccount(account) { - let client = new UserAPI10Client(this.userAPIURI); - let cb = Async.makeSpinningCallback(); - - let username = this.identity.usernameFromAccount(account); - client.usernameExists(username, cb); - - try { - let exists = cb.wait(); - return exists ? "notAvailable" : "available"; - } catch (ex) { - // TODO fix API convention. - return this.errorHandler.errorStr(ex); - } - }, - - createAccount: function createAccount(email, password, - captchaChallenge, captchaResponse) { - let client = new UserAPI10Client(this.userAPIURI); - - // Hint to server to allow scripted user creation or otherwise - // ignore captcha. - if (Svc.Prefs.isSet("admin-secret")) { - client.adminSecret = Svc.Prefs.get("admin-secret", ""); - } - - let cb = Async.makeSpinningCallback(); - - client.createAccount(email, password, captchaChallenge, captchaResponse, - cb); - - try { - cb.wait(); - return null; - } catch (ex) { - return this.errorHandler.errorStr(ex.body); - } - }, - - // Stuff we need to do after login, before we can really do - // anything (e.g. key setup). - _remoteSetup: function _remoteSetup(infoResponse) { - let reset = false; - - this._log.debug("Fetching global metadata record"); - let meta = this.recordManager.get(this.metaURL); - - // Checking modified time of the meta record. - if (infoResponse && - (infoResponse.obj.meta != this.metaModified) && - (!meta || !meta.isNew)) { - - // Delete the cached meta record... - this._log.debug("Clearing cached meta record. metaModified is " + - JSON.stringify(this.metaModified) + ", setting to " + - JSON.stringify(infoResponse.obj.meta)); - - this.recordManager.del(this.metaURL); - - // ... fetch the current record from the server, and COPY THE FLAGS. - let newMeta = this.recordManager.get(this.metaURL); - - // If we got a 401, we do not want to create a new meta/global - we - // should be able to get the existing meta after we get a new node. - if (this.recordManager.response.status == 401) { - this._log.debug("Fetching meta/global record on the server returned 401."); - this.errorHandler.checkServerError(this.recordManager.response); - return false; - } - - if (!this.recordManager.response.success || !newMeta) { - this._log.debug("No meta/global record on the server. Creating one."); - newMeta = new WBORecord("meta", "global"); - newMeta.payload.syncID = this.syncID; - newMeta.payload.storageVersion = STORAGE_VERSION; - newMeta.payload.declined = this.engineManager.getDeclined(); - - newMeta.isNew = true; - - this.recordManager.set(this.metaURL, newMeta); - if (!newMeta.upload(this.resource(this.metaURL)).success) { - this._log.warn("Unable to upload new meta/global. Failing remote setup."); - return false; - } - } else { - // If newMeta, then it stands to reason that meta != null. - newMeta.isNew = meta.isNew; - newMeta.changed = meta.changed; - } - - // Switch in the new meta object and record the new time. - meta = newMeta; - this.metaModified = infoResponse.obj.meta; - } - - let remoteVersion = (meta && meta.payload.storageVersion)? - meta.payload.storageVersion : ""; - - this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", - STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); - - // Check for cases that require a fresh start. When comparing remoteVersion, - // we need to convert it to a number as older clients used it as a string. - if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || - STORAGE_VERSION > parseFloat(remoteVersion)) { - - this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed."); - - // abort the server wipe if the GET status was anything other than 404 or 200 - let status = this.recordManager.response.status; - if (status != 200 && status != 404) { - this.status.sync = METARECORD_DOWNLOAD_FAIL; - this.errorHandler.checkServerError(this.recordManager.response); - this._log.warn("Unknown error while downloading metadata record. " + - "Aborting sync."); - return false; - } - - if (!meta) - this._log.info("No metadata record, server wipe needed"); - if (meta && !meta.payload.syncID) - this._log.warn("No sync id, server wipe needed"); - - reset = true; - - this._log.info("Wiping server data"); - this._freshStart(); - - if (status == 404) - this._log.info("Metadata record not found, server was wiped to ensure " + - "consistency."); - else // 200 - this._log.info("Wiped server; incompatible metadata: " + remoteVersion); - - return true; - } - else if (remoteVersion > STORAGE_VERSION) { - this.status.sync = VERSION_OUT_OF_DATE; - this._log.warn("Upgrade required to access newer storage version."); - return false; - } - else if (meta.payload.syncID != this.syncID) { - - this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID); - this.resetClient(); - this.collectionKeys.clear(); - this.syncID = meta.payload.syncID; - this._log.debug("Clear cached values and take syncId: " + this.syncID); - - if (!this.upgradeSyncKey(meta.payload.syncID)) { - this._log.warn("Failed to upgrade sync key. Failing remote setup."); - return false; - } - - if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { - this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); - return false; - } - - // bug 545725 - re-verify creds and fail sanely - if (!this.verifyLogin()) { - this.status.sync = CREDENTIALS_CHANGED; - this._log.info("Credentials have changed, aborting sync and forcing re-login."); - return false; - } - - return true; - } - else { - if (!this.upgradeSyncKey(meta.payload.syncID)) { - this._log.warn("Failed to upgrade sync key. Failing remote setup."); - return false; - } - - if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { - this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); - return false; - } - - return true; - } - }, - - /** - * Return whether we should attempt login at the start of a sync. - * - * Note that this function has strong ties to _checkSync: callers - * of this function should typically use _checkSync to verify that - * any necessary login took place. - */ - _shouldLogin: function _shouldLogin() { - return this.enabled && - !Services.io.offline && - !this.isLoggedIn; - }, - - /** - * Determine if a sync should run. - * - * @param ignore [optional] - * array of reasons to ignore when checking - * - * @return Reason for not syncing; not-truthy if sync should run - */ - _checkSync: function _checkSync(ignore) { - let reason = ""; - if (!this.enabled) - reason = kSyncWeaveDisabled; - else if (Services.io.offline) - reason = kSyncNetworkOffline; - else if (this.status.minimumNextSync > Date.now()) - reason = kSyncBackoffNotMet; - else if ((this.status.login == MASTER_PASSWORD_LOCKED) && - Utils.mpLocked()) - reason = kSyncMasterPasswordLocked; - else if (Svc.Prefs.get("firstSync") == "notReady") - reason = kFirstSyncChoiceNotMade; - - if (ignore && ignore.indexOf(reason) != -1) - return ""; - - return reason; - }, - - sync: function sync() { - let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT); - this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent); - this._log.info("Starting sync at " + dateStr); - this._catch(function () { - // Make sure we're logged in. - if (this._shouldLogin()) { - this._log.debug("In sync: should login."); - if (!this.login()) { - this._log.debug("Not syncing: login returned false."); - return; - } - } - else { - this._log.trace("In sync: no need to login."); - } - return this._lockedSync.apply(this, arguments); - })(); - }, - - /** - * Sync up engines with the server. - */ - _lockedSync: function _lockedSync() { - return this._lock("service.js: sync", - this._notify("sync", "", function onNotify() { - - let synchronizer = new EngineSynchronizer(this); - let cb = Async.makeSpinningCallback(); - synchronizer.onComplete = cb; - - synchronizer.sync(); - // wait() throws if the first argument is truthy, which is exactly what - // we want. - let result = cb.wait(); - - // We successfully synchronized. - // Check if the identity wants to pre-fetch a migration sentinel from - // the server. - // If we have no clusterURL, we are probably doing a node reassignment - // so don't attempt to get it in that case. - //if (this.clusterURL) { - // this.identity.prefetchMigrationSentinel(this); - //} - - // Now let's update our declined engines. - let meta = this.recordManager.get(this.metaURL); - if (!meta) { - this._log.warn("No meta/global; can't update declined state."); - return; - } - - let declinedEngines = new DeclinedEngines(this); - let didChange = declinedEngines.updateDeclined(meta, this.engineManager); - if (!didChange) { - this._log.info("No change to declined engines. Not reuploading meta/global."); - return; - } - - this.uploadMetaGlobal(meta); - }))(); - }, - - /** - * Upload meta/global, throwing the response on failure. - */ - uploadMetaGlobal: function (meta) { - this._log.debug("Uploading meta/global: " + JSON.stringify(meta)); - - // 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. - let res = this.resource(this.metaURL); - let response = res.put(meta); - if (!response.success) { - throw response; - } - this.recordManager.set(this.metaURL, meta); - }, - - /** - * If we have a passphrase, rather than a 25-alphadigit sync key, - * use the provided sync ID to bootstrap it using PBKDF2. - * - * Store the new 'passphrase' back into the identity manager. - * - * We can check this as often as we want, because once it's done the - * check will no longer succeed. It only matters that it happens after - * we decide to bump the server storage version. - */ - upgradeSyncKey: function upgradeSyncKey(syncID) { - let p = this.identity.syncKey; - - if (!p) { - return false; - } - - // Check whether it's already a key that we generated. - if (Utils.isPassphrase(p)) { - this._log.info("Sync key is up-to-date: no need to upgrade."); - return true; - } - - // Otherwise, let's upgrade it. - // N.B., we persist the sync key without testing it first... - - let s = btoa(syncID); // It's what WeaveCrypto expects. *sigh* - let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES); // Base 32. - - if (!k) { - this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade."); - return false; - } - - this._log.info("Upgrading sync key..."); - this.identity.syncKey = k; - this._log.info("Saving upgraded sync key..."); - this.persistLogin(); - this._log.info("Done saving."); - return true; - }, - - _freshStart: function _freshStart() { - this._log.info("Fresh start. Resetting client and considering key upgrade."); - this.resetClient(); - this.collectionKeys.clear(); - this.upgradeSyncKey(this.syncID); - - // Wipe the server. - let wipeTimestamp = this.wipeServer(); - - // Upload a new meta/global record. - let meta = new WBORecord("meta", "global"); - meta.payload.syncID = this.syncID; - meta.payload.storageVersion = STORAGE_VERSION; - meta.payload.declined = this.engineManager.getDeclined(); - meta.isNew = true; - - // uploadMetaGlobal throws on failure -- including race conditions. - // If we got into a race condition, we'll abort the sync this way, too. - // That's fine. We'll just wait till the next sync. The client that we're - // racing is probably busy uploading stuff right now anyway. - this.uploadMetaGlobal(meta); - - // Wipe everything we know about except meta because we just uploaded it - let engines = [this.clientsEngine].concat(this.engineManager.getAll()); - let collections = engines.map(engine => engine.name); - // TODO: there's a bug here. We should be calling resetClient, no? - - // Generate, upload, and download new keys. Do this last so we don't wipe - // them... - this.generateNewSymmetricKeys(); - }, - - /** - * Wipe user data from the server. - * - * @param collections [optional] - * Array of collections to wipe. If not given, all collections are - * wiped by issuing a DELETE request for `storageURL`. - * - * @return the server's timestamp of the (last) DELETE. - */ - wipeServer: function wipeServer(collections) { - let response; - if (!collections) { - // Strip the trailing slash. - let res = this.resource(this.storageURL.slice(0, -1)); - res.setHeader("X-Confirm-Delete", "1"); - try { - response = res.delete(); - } catch (ex) { - this._log.debug("Failed to wipe server: ", ex); - throw ex; - } - if (response.status != 200 && response.status != 404) { - this._log.debug("Aborting wipeServer. Server responded with " + - response.status + " response for " + this.storageURL); - throw response; - } - return response.headers["x-weave-timestamp"]; - } - - let timestamp; - for (let name of collections) { - let url = this.storageURL + name; - try { - response = this.resource(url).delete(); - } catch (ex) { - this._log.debug("Failed to wipe '" + name + "' collection: ", ex); - throw ex; - } - - if (response.status != 200 && response.status != 404) { - this._log.debug("Aborting wipeServer. Server responded with " + - response.status + " response for " + url); - throw response; - } - - if ("x-weave-timestamp" in response.headers) { - timestamp = response.headers["x-weave-timestamp"]; - } - } - - return timestamp; - }, - - /** - * Wipe all local user data. - * - * @param engines [optional] - * Array of engine names to wipe. If not given, all engines are used. - */ - wipeClient: function wipeClient(engines) { - // If we don't have any engines, reset the service and wipe all engines - if (!engines) { - // Clear out any service data - this.resetService(); - - engines = [this.clientsEngine].concat(this.engineManager.getAll()); - } - // Convert the array of names into engines - else { - engines = this.engineManager.get(engines); - } - - // Fully wipe each engine if it's able to decrypt data - for each (let engine in engines) { - if (engine.canDecrypt()) { - engine.wipeClient(); - } - } - - // Save the password/passphrase just in-case they aren't restored by sync - this.persistLogin(); - }, - - /** - * Wipe all remote user data by wiping the server then telling each remote - * client to wipe itself. - * - * @param engines [optional] - * Array of engine names to wipe. If not given, all engines are used. - */ - wipeRemote: function wipeRemote(engines) { - try { - // Make sure stuff gets uploaded. - this.resetClient(engines); - - // Clear out any server data. - this.wipeServer(engines); - - // Only wipe the engines provided. - if (engines) { - engines.forEach(function(e) { - this.clientsEngine.sendCommand("wipeEngine", [e]); - }, this); - } - // Tell the remote machines to wipe themselves. - else { - this.clientsEngine.sendCommand("wipeAll", []); - } - - // Make sure the changed clients get updated. - this.clientsEngine.sync(); - } catch (ex) { - this.errorHandler.checkServerError(ex); - throw ex; - } - }, - - /** - * Reset local service information like logs, sync times, caches. - */ - resetService: function resetService() { - this._catch(function reset() { - this._log.info("Service reset."); - - // Pretend we've never synced to the server and drop cached data - this.syncID = ""; - this.recordManager.clearCache(); - })(); - }, - - /** - * Reset the client by getting rid of any local server data and client data. - * - * @param engines [optional] - * Array of engine names to reset. If not given, all engines are used. - */ - resetClient: function resetClient(engines) { - this._catch(function doResetClient() { - // If we don't have any engines, reset everything including the service - if (!engines) { - // Clear out any service data - this.resetService(); - - engines = [this.clientsEngine].concat(this.engineManager.getAll()); - } - // Convert the array of names into engines - else { - engines = this.engineManager.get(engines); - } - - // Have each engine drop any temporary meta data - for (let engine of engines) { - engine.resetClient(); - } - })(); - }, - - /** - * Fetch storage info from the server. - * - * @param type - * String specifying what info to fetch from the server. Must be one - * of the INFO_* values. See Sync Storage Server API spec for details. - * @param callback - * Callback function with signature (error, data) where `data' is - * the return value from the server already parsed as JSON. - * - * @return RESTRequest instance representing the request, allowing callers - * to cancel the request. - */ - getStorageInfo: function getStorageInfo(type, callback) { - if (STORAGE_INFO_TYPES.indexOf(type) == -1) { - throw "Invalid value for 'type': " + type; - } - - let info_type = "info/" + type; - this._log.trace("Retrieving '" + info_type + "'..."); - let url = this.userBaseURL + info_type; - return this.getStorageRequest(url).get(function onComplete(error) { - // Note: 'this' is the request. - if (error) { - this._log.debug("Failed to retrieve '" + info_type + "': ", error); - return callback(error); - } - if (this.response.status != 200) { - this._log.debug("Failed to retrieve '" + info_type + - "': server responded with HTTP" + - this.response.status); - return callback(this.response); - } - - let result; - try { - result = JSON.parse(this.response.body); - } catch (ex) { - this._log.debug("Server returned invalid JSON for '" + info_type + - "': " + this.response.body); - return callback(ex); - } - this._log.trace("Successfully retrieved '" + info_type + "'."); - return callback(null, result); - }); - }, -}; - -this.Service = new Sync11Service(); -Service.onStartup(); diff --git a/components/weave/src/sync/status.js b/components/weave/src/sync/status.js deleted file mode 100644 index 27243de0d..000000000 --- a/components/weave/src/sync/status.js +++ /dev/null @@ -1,142 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.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 = ["Status"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Async.jsm"); - -this.Status = { - _log: Log.repository.getLogger("Sync.Status"), - __authManager: null, - ready: false, - - get _authManager() { - if (this.__authManager) { - return this.__authManager; - } - let service = Components.classes["@mozilla.org/weave/service;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - let idClass = IdentityManager; - this.__authManager = new idClass(); - // .initialize returns a promise, so we need to spin until it resolves. - let cb = Async.makeSpinningCallback(); - this.__authManager.initialize().then(cb, cb); - cb.wait(); - return this.__authManager; - }, - - get service() { - return this._service; - }, - - set service(code) { - this._log.debug("Status.service: " + (this._service || undefined) + " => " + code); - this._service = code; - }, - - get login() { - return this._login; - }, - - set login(code) { - this._log.debug("Status.login: " + this._login + " => " + code); - this._login = code; - - if (code == LOGIN_FAILED_NO_USERNAME || - code == LOGIN_FAILED_NO_PASSWORD || - code == LOGIN_FAILED_NO_PASSPHRASE) { - this.service = CLIENT_NOT_CONFIGURED; - } else if (code != LOGIN_SUCCEEDED) { - this.service = LOGIN_FAILED; - } else { - this.service = STATUS_OK; - } - }, - - get sync() { - return this._sync; - }, - - set sync(code) { - this._log.debug("Status.sync: " + this._sync + " => " + code); - this._sync = code; - this.service = code == SYNC_SUCCEEDED ? STATUS_OK : SYNC_FAILED; - }, - - get eol() { - let modePref = PREFS_BRANCH + "errorhandler.alert.mode"; - try { - return Services.prefs.getCharPref(modePref) == "hard-eol"; - } catch (ex) { - return false; - } - }, - - get engines() { - return this._engines; - }, - - set engines([name, code]) { - this._log.debug("Status for engine " + name + ": " + code); - this._engines[name] = code; - - if (code != ENGINE_SUCCEEDED) { - this.service = SYNC_FAILED_PARTIAL; - } - }, - - // Implement toString because adding a logger introduces a cyclic object - // value, so we can't trivially debug-print Status as JSON. - toString: function toString() { - return "<Status" + - ": login: " + Status.login + - ", service: " + Status.service + - ", sync: " + Status.sync + ">"; - }, - - checkSetup: function checkSetup() { - let result = this._authManager.currentAuthState; - if (result == STATUS_OK) { - Status.service = result; - return result; - } - - Status.login = result; - return Status.service; - }, - - resetBackoff: function resetBackoff() { - this.enforceBackoff = false; - this.backoffInterval = 0; - this.minimumNextSync = 0; - }, - - resetSync: function resetSync() { - // Logger setup. - let logPref = PREFS_BRANCH + "log.logger.status"; - let logLevel = Services.prefs.getCharPref(logPref, "Trace"); - this._log.level = Log.Level[logLevel]; - - this._log.info("Resetting Status."); - this.service = STATUS_OK; - this._login = LOGIN_SUCCEEDED; - this._sync = SYNC_SUCCEEDED; - this._engines = {}; - this.partial = false; - } -}; - -// Initialize various status values. -Status.resetBackoff(); -Status.resetSync(); diff --git a/components/weave/src/sync/userapi.js b/components/weave/src/sync/userapi.js deleted file mode 100644 index 4c3cdd43f..000000000 --- a/components/weave/src/sync/userapi.js +++ /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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "UserAPI10Client", -]; - -var {utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://services-sync/identity.js"); -Cu.import("resource://services-sync/util.js"); - -/** - * A generic client for the user API 1.0 service. - * - * http://docs.services.mozilla.com/reg/apis.html - * - * Instances are constructed with the base URI of the service. - */ -this.UserAPI10Client = function UserAPI10Client(baseURI) { - this._log = Log.repository.getLogger("Sync.UserAPI"); - this._log.level = Log.Level[Svc.Prefs.get("log.logger.userapi")]; - - this.baseURI = baseURI; -} -UserAPI10Client.prototype = { - USER_CREATE_ERROR_CODES: { - 2: "Incorrect or missing captcha.", - 4: "User exists.", - 6: "JSON parse failure.", - 7: "Missing password field.", - 9: "Requested password not strong enough.", - 12: "No email address on file.", - }, - - /** - * Determine whether a specified username exists. - * - * Callback receives the following arguments: - * - * (Error) Describes error that occurred or null if request was - * successful. - * (boolean) True if user exists. False if not. null if there was an error. - */ - usernameExists: function usernameExists(username, cb) { - if (typeof(cb) != "function") { - throw new Error("cb must be a function."); - } - - let url = this.baseURI + username; - let request = new RESTRequest(url); - request.get(this._onUsername.bind(this, cb, request)); - }, - - /** - * Obtain the Weave (Sync) node for a specified user. - * - * The callback receives the following arguments: - * - * (Error) Describes error that occurred or null if request was successful. - * (string) Username request is for. - * (string) URL of user's node. If null and there is no error, no node could - * be assigned at the time of the request. - */ - getWeaveNode: function getWeaveNode(username, password, cb) { - if (typeof(cb) != "function") { - throw new Error("cb must be a function."); - } - - let request = this._getRequest(username, "/node/weave", password); - request.get(this._onWeaveNode.bind(this, cb, request)); - }, - - /** - * Change a password for the specified user. - * - * @param username - * (string) The username whose password to change. - * @param oldPassword - * (string) The old, current password. - * @param newPassword - * (string) The new password to switch to. - */ - changePassword: function changePassword(username, oldPassword, newPassword, cb) { - let request = this._getRequest(username, "/password", oldPassword); - request.onComplete = this._onChangePassword.bind(this, cb, request); - request.post(CommonUtils.encodeUTF8(newPassword)); - }, - - createAccount: function createAccount(email, password, captchaChallenge, - captchaResponse, cb) { - let username = IdentityManager.prototype.usernameFromAccount(email); - let body = JSON.stringify({ - "email": email, - "password": Utils.encodeUTF8(password), - "captcha-challenge": captchaChallenge, - "captcha-response": captchaResponse - }); - - let url = this.baseURI + username; - let request = new RESTRequest(url); - - if (this.adminSecret) { - request.setHeader("X-Weave-Secret", this.adminSecret); - } - - request.onComplete = this._onCreateAccount.bind(this, cb, request); - request.put(body); - }, - - _getRequest: function _getRequest(username, path, password=null) { - let url = this.baseURI + username + path; - let request = new RESTRequest(url); - - if (password) { - let up = username + ":" + password; - request.setHeader("authorization", "Basic " + btoa(up)); - } - - return request; - }, - - _onUsername: function _onUsername(cb, request, error) { - if (error) { - cb(error, null); - return; - } - - let body = request.response.body; - if (body == "0") { - cb(null, false); - return; - } else if (body == "1") { - cb(null, true); - return; - } else { - cb(new Error("Unknown response from server: " + body), null); - return; - } - }, - - _onWeaveNode: function _onWeaveNode(cb, request, error) { - if (error) { - cb.network = true; - cb(error, null); - return; - } - - let response = request.response; - - if (response.status == 200) { - let body = response.body; - if (body == "null") { - cb(null, null); - return; - } - - cb(null, body); - return; - } - - error = new Error("Sync node retrieval failed."); - switch (response.status) { - case 400: - error.denied = true; - break; - case 404: - error.notFound = true; - break; - default: - error.message = "Unexpected response code: " + response.status; - } - - cb(error, null); - return; - }, - - _onChangePassword: function _onChangePassword(cb, request, error) { - this._log.info("Password change response received: " + - request.response.status); - if (error) { - cb(error); - return; - } - - let response = request.response; - if (response.status != 200) { - cb(new Error("Password changed failed: " + response.body)); - return; - } - - cb(null); - }, - - _onCreateAccount: function _onCreateAccount(cb, request, error) { - let response = request.response; - - this._log.info("Create account response: " + response.status + " " + - response.body); - - if (error) { - cb(new Error("HTTP transport error."), null); - return; - } - - if (response.status == 200) { - cb(null, response.body); - return; - } - - error = new Error("Could not create user."); - error.body = response.body; - - cb(error, null); - return; - }, -}; -Object.freeze(UserAPI10Client.prototype); diff --git a/components/weave/src/sync/util.js b/components/weave/src/sync/util.js deleted file mode 100644 index b26d0837e..000000000 --- a/components/weave/src/sync/util.js +++ /dev/null @@ -1,693 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.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 = ["XPCOMUtils", "Services", "Utils", "Async", "Svc", "Str"]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://services-common/stringbundle.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://gre/modules/Async.jsm", this); -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-sync/constants.js"); -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm", this); -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -Cu.import("resource://gre/modules/osfile.jsm", this); -Cu.import("resource://gre/modules/Task.jsm", this); - -/* - * Utility functions - */ - -this.Utils = { - // Alias in functions from CommonUtils. These previously were defined here. - // In the ideal world, references to these would be removed. - nextTick: CommonUtils.nextTick, - namedTimer: CommonUtils.namedTimer, - exceptionStr: CommonUtils.exceptionStr, - stackTrace: CommonUtils.stackTrace, - makeURI: CommonUtils.makeURI, - encodeUTF8: CommonUtils.encodeUTF8, - decodeUTF8: CommonUtils.decodeUTF8, - safeAtoB: CommonUtils.safeAtoB, - byteArrayToString: CommonUtils.byteArrayToString, - bytesAsHex: CommonUtils.bytesAsHex, - hexToBytes: CommonUtils.hexToBytes, - encodeBase32: CommonUtils.encodeBase32, - decodeBase32: CommonUtils.decodeBase32, - - // Aliases from CryptoUtils. - generateRandomBytes: CryptoUtils.generateRandomBytes, - computeHTTPMACSHA1: CryptoUtils.computeHTTPMACSHA1, - digestUTF8: CryptoUtils.digestUTF8, - digestBytes: CryptoUtils.digestBytes, - sha1: CryptoUtils.sha1, - sha1Base32: CryptoUtils.sha1Base32, - makeHMACKey: CryptoUtils.makeHMACKey, - makeHMACHasher: CryptoUtils.makeHMACHasher, - hkdfExpand: CryptoUtils.hkdfExpand, - pbkdf2Generate: CryptoUtils.pbkdf2Generate, - deriveKeyFromPassphrase: CryptoUtils.deriveKeyFromPassphrase, - getHTTPMACSHA1Header: CryptoUtils.getHTTPMACSHA1Header, - - /** - * Wrap a function to catch all exceptions and log them - * - * @usage MyObj._catch = Utils.catch; - * MyObj.foo = function() { this._catch(func)(); } - * - * Optionally pass a function which will be called if an - * exception occurs. - */ - catch: function Utils_catch(func, exceptionCallback) { - let thisArg = this; - return function WrappedCatch() { - try { - return func.call(thisArg); - } - catch(ex) { - thisArg._log.debug("Exception: ", ex); - if (exceptionCallback) { - return exceptionCallback.call(thisArg, ex); - } - return null; - } - }; - }, - - /** - * Wrap a function to call lock before calling the function then unlock. - * - * @usage MyObj._lock = Utils.lock; - * MyObj.foo = function() { this._lock(func)(); } - */ - lock: function lock(label, func) { - let thisArg = this; - return function WrappedLock() { - if (!thisArg.lock()) { - throw "Could not acquire lock. Label: \"" + label + "\"."; - } - - try { - return func.call(thisArg); - } - finally { - thisArg.unlock(); - } - }; - }, - - isLockException: function isLockException(ex) { - return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0; - }, - - /** - * Wrap functions to notify when it starts and finishes executing or if it - * threw an error. - * - * The message is a combination of a provided prefix, the local name, and - * the event. Possible events are: "start", "finish", "error". The subject - * is the function's return value on "finish" or the caught exception on - * "error". The data argument is the predefined data value. - * - * Example: - * - * @usage function MyObj(name) { - * this.name = name; - * this._notify = Utils.notify("obj:"); - * } - * MyObj.prototype = { - * foo: function() this._notify("func", "data-arg", function () { - * //... - * }(), - * }; - */ - notify: function Utils_notify(prefix) { - return function NotifyMaker(name, data, func) { - let thisArg = this; - let notify = function(state, subject) { - let mesg = prefix + name + ":" + state; - thisArg._log.trace("Event: " + mesg); - Observers.notify(mesg, subject, data); - }; - - return function WrappedNotify() { - try { - notify("start", null); - let ret = func.call(thisArg); - notify("finish", ret); - return ret; - } - catch(ex) { - notify("error", ex); - throw ex; - } - }; - }; - }, - - /** - * GUIDs are 9 random bytes encoded with base64url (RFC 4648). - * That makes them 12 characters long with 72 bits of entropy. - */ - makeGUID: function makeGUID() { - return CommonUtils.encodeBase64URL(Utils.generateRandomBytes(9)); - }, - - _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i, - checkGUID: function checkGUID(guid) { - return !!guid && this._base64url_regex.test(guid); - }, - - /** - * Add a simple getter/setter to an object that defers access of a property - * to an inner property. - * - * @param obj - * Object to add properties to defer in its prototype - * @param defer - * Property of obj to defer to - * @param prop - * Property name to defer (or an array of property names) - */ - deferGetSet: function Utils_deferGetSet(obj, defer, prop) { - if (Array.isArray(prop)) - return prop.map(prop => Utils.deferGetSet(obj, defer, prop)); - - let prot = obj.prototype; - - // Create a getter if it doesn't exist yet - if (!prot.__lookupGetter__(prop)) { - prot.__defineGetter__(prop, function () { - return this[defer][prop]; - }); - } - - // Create a setter if it doesn't exist yet - if (!prot.__lookupSetter__(prop)) { - prot.__defineSetter__(prop, function (val) { - this[defer][prop] = val; - }); - } - }, - - lazyStrings: function Weave_lazyStrings(name) { - let bundle = "chrome://weave/locale/" + name + ".properties"; - return () => new StringBundle(bundle); - }, - - deepEquals: function eq(a, b) { - // If they're triple equals, then it must be equals! - if (a === b) - return true; - - // If they weren't equal, they must be objects to be different - if (typeof a != "object" || typeof b != "object") - return false; - - // But null objects won't have properties to compare - if (a === null || b === null) - return false; - - // Make sure all of a's keys have a matching value in b - for (let k in a) - if (!eq(a[k], b[k])) - return false; - - // Do the same for b's keys but skip those that we already checked - for (let k in b) - if (!(k in a) && !eq(a[k], b[k])) - return false; - - return true; - }, - - // Generator and discriminator for HMAC exceptions. - // Split these out in case we want to make them richer in future, and to - // avoid inevitable confusion if the message changes. - throwHMACMismatch: function throwHMACMismatch(shouldBe, is) { - throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is; - }, - - isHMACMismatch: function isHMACMismatch(ex) { - const hmacFail = "Record SHA256 HMAC mismatch: "; - return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0); - }, - - /** - * Turn RFC 4648 base32 into our own user-friendly version. - * ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 - * becomes - * abcdefghijk8mn9pqrstuvwxyz234567 - */ - base32ToFriendly: function base32ToFriendly(input) { - return input.toLowerCase() - .replace("l", '8', "g") - .replace("o", '9', "g"); - }, - - base32FromFriendly: function base32FromFriendly(input) { - return input.toUpperCase() - .replace("8", 'L', "g") - .replace("9", 'O', "g"); - }, - - /** - * Key manipulation. - */ - - // Return an octet string in friendly base32 *with no trailing =*. - encodeKeyBase32: function encodeKeyBase32(keyData) { - return Utils.base32ToFriendly( - Utils.encodeBase32(keyData)) - .slice(0, SYNC_KEY_ENCODED_LENGTH); - }, - - decodeKeyBase32: function decodeKeyBase32(encoded) { - return Utils.decodeBase32( - Utils.base32FromFriendly( - Utils.normalizePassphrase(encoded))) - .slice(0, SYNC_KEY_DECODED_LENGTH); - }, - - base64Key: function base64Key(keyData) { - return btoa(keyData); - }, - - /** - * N.B., salt should be base64 encoded, even though we have to decode - * it later! - */ - derivePresentableKeyFromPassphrase : function derivePresentableKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { - let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength, - forceJS); - return Utils.encodeKeyBase32(k); - }, - - /** - * N.B., salt should be base64 encoded, even though we have to decode - * it later! - */ - deriveEncodedKeyFromPassphrase : function deriveEncodedKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { - let k = CryptoUtils.deriveKeyFromPassphrase(passphrase, salt, keyLength, - forceJS); - return Utils.base64Key(k); - }, - - /** - * Take a base64-encoded 128-bit AES key, returning it as five groups of five - * uppercase alphanumeric characters, separated by hyphens. - * A.K.A. base64-to-base32 encoding. - */ - presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) { - return Utils.encodeKeyBase32(atob(encodedKey)); - }, - - /** - * Load a JSON file from disk in the profile directory. - * - * @param filePath - * JSON file path load from profile. Loaded file will be - * <profile>/<filePath>.json. i.e. Do not specify the ".json" - * extension. - * @param that - * Object to use for logging and "this" for callback. - * @param callback - * Function to process json object as its first argument. If the file - * could not be loaded, the first argument will be undefined. - */ - jsonLoad: Task.async(function*(filePath, that, callback) { - let path; - try { - path = OS.Path.normalize(OS.Path.join(OS.Constants.Path.profileDir, "weave", filePath + ".json")); - } catch (e) { - if (that._log) { - that._log.debug("Path join error: " + e); - } - } - - if (that._log) { - that._log.trace("Loading json from disk: " + path); - } - - let json; - - try { - json = yield CommonUtils.readJSON(path); - } catch (e) { - if (e instanceof OS.File.Error && e.becauseNoSuchFile) { - // Ignore non-existent files, but explicitly return null. - json = null; - } else { - if (that._log) { - that._log.debug("Failed to load json", e); - } - } - } - if (callback) { - callback.call(that, json); - } - }), - - /** - * Save a json-able object to disk in the profile directory. - * - * @param filePath - * JSON file path save to <filePath>.json - * @param that - * Object to use for logging and "this" for callback - * @param obj - * Function to provide json-able object to save. If this isn't a - * function, it'll be used as the object to make a json string. - * @param callback - * Function called when the write has been performed. Optional. - * The first argument will be a Components.results error - * constant on error or null if no error was encountered (and - * the file saved successfully). - */ - jsonSave: Task.async(function*(filePath, that, obj, callback) { - let path = OS.Path.join(OS.Constants.Path.profileDir, "weave", - ...(filePath + ".json").split("/")); - let dir = OS.Path.dirname(path); - let error = null; - - try { - yield OS.File.makeDir(dir, { from: OS.Constants.Path.profileDir }); - - if (that._log) { - that._log.trace("Saving json to disk: " + path); - } - - let json = typeof obj == "function" ? obj.call(that) : obj; - - yield CommonUtils.writeJSON(json, path); - } catch (e) { - error = e - } - - if (typeof callback == "function") { - callback.call(that, error); - } - }), - - getErrorString: function Utils_getErrorString(error, args) { - try { - return Str.errors.get(error, args || null); - } catch (e) {} - - // basically returns "Unknown Error" - return Str.errors.get("error.reason.unknown"); - }, - - /** - * Generate 26 characters. - */ - generatePassphrase: function generatePassphrase() { - // Note that this is a different base32 alphabet to the one we use for - // other tasks. It's lowercase, uses different letters, and needs to be - // decoded with decodeKeyBase32, not just decodeBase32. - return Utils.encodeKeyBase32(CryptoUtils.generateRandomBytes(16)); - }, - - /** - * The following are the methods supported for UI use: - * - * * isPassphrase: - * determines whether a string is either a normalized or presentable - * passphrase. - * * hyphenatePassphrase: - * present a normalized passphrase for display. This might actually - * perform work beyond just hyphenation; sorry. - * * hyphenatePartialPassphrase: - * present a fragment of a normalized passphrase for display. - * * normalizePassphrase: - * take a presentable passphrase and reduce it to a normalized - * representation for storage. normalizePassphrase can safely be called - * on normalized input. - * * normalizeAccount: - * take user input for account/username, cleaning up appropriately. - */ - - isPassphrase: function(s) { - if (s) { - return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(Utils.normalizePassphrase(s)); - } - return false; - }, - - /** - * Hyphenate a passphrase (26 characters) into groups. - * abbbbccccddddeeeeffffggggh - * => - * a-bbbbc-cccdd-ddeee-effff-ggggh - */ - hyphenatePassphrase: function hyphenatePassphrase(passphrase) { - // For now, these are the same. - return Utils.hyphenatePartialPassphrase(passphrase, true); - }, - - hyphenatePartialPassphrase: function hyphenatePartialPassphrase(passphrase, omitTrailingDash) { - if (!passphrase) - return null; - - // Get the raw data input. Just base32. - let data = passphrase.toLowerCase().replace(/[^abcdefghijkmnpqrstuvwxyz23456789]/g, ""); - - // This is the neatest way to do this. - if ((data.length == 1) && !omitTrailingDash) - return data + "-"; - - // Hyphenate it. - let y = data.substr(0,1); - let z = data.substr(1).replace(/(.{1,5})/g, "-$1"); - - // Correct length? We're done. - if ((z.length == 30) || omitTrailingDash) - return y + z; - - // Add a trailing dash if appropriate. - return (y + z.replace(/([^-]{5})$/, "$1-")).substr(0, SYNC_KEY_HYPHENATED_LENGTH); - }, - - normalizePassphrase: function normalizePassphrase(pp) { - // Short var name... have you seen the lines below?! - // Allow leading and trailing whitespace. - pp = pp.trim().toLowerCase(); - - // 20-char sync key. - if (pp.length == 23 && - [5, 11, 17].every(function(i) pp[i] == '-')) { - - return pp.slice(0, 5) + pp.slice(6, 11) - + pp.slice(12, 17) + pp.slice(18, 23); - } - - // "Modern" 26-char key. - if (pp.length == 31 && - [1, 7, 13, 19, 25].every(function(i) pp[i] == '-')) { - - return pp.slice(0, 1) + pp.slice(2, 7) - + pp.slice(8, 13) + pp.slice(14, 19) - + pp.slice(20, 25) + pp.slice(26, 31); - } - - // Something else -- just return. - return pp; - }, - - normalizeAccount: function normalizeAccount(acc) { - return acc.trim(); - }, - - /** - * Create an array like the first but without elements of the second. Reuse - * arrays if possible. - */ - arraySub: function arraySub(minuend, subtrahend) { - if (!minuend.length || !subtrahend.length) - return minuend; - return minuend.filter(i => subtrahend.indexOf(i) == -1); - }, - - /** - * Build the union of two arrays. Reuse arrays if possible. - */ - arrayUnion: function arrayUnion(foo, bar) { - if (!foo.length) - return bar; - if (!bar.length) - return foo; - return foo.concat(Utils.arraySub(bar, foo)); - }, - - bind2: function Async_bind2(object, method) { - return function innerBind() { return method.apply(object, arguments); }; - }, - - /** - * Is there a master password configured, regardless of current lock state? - */ - mpEnabled: function mpEnabled() { - let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"] - .getService(Ci.nsIPKCS11ModuleDB); - let sdrSlot = modules.findSlotByName(""); - let status = sdrSlot.status; - let slots = Ci.nsIPKCS11Slot; - - return status != slots.SLOT_UNINITIALIZED && status != slots.SLOT_READY; - }, - - /** - * Is there a master password configured and currently locked? - */ - mpLocked: function mpLocked() { - let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"] - .getService(Ci.nsIPKCS11ModuleDB); - let sdrSlot = modules.findSlotByName(""); - let status = sdrSlot.status; - let slots = Ci.nsIPKCS11Slot; - - if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN - || status == slots.SLOT_UNINITIALIZED) - return false; - - if (status == slots.SLOT_NOT_LOGGED_IN) - return true; - - // something wacky happened, pretend MP is locked - return true; - }, - - // If Master Password is enabled and locked, present a dialog to unlock it. - // Return whether the system is unlocked. - ensureMPUnlocked: function ensureMPUnlocked() { - if (!Utils.mpLocked()) { - return true; - } - let sdr = Cc["@mozilla.org/security/sdr;1"] - .getService(Ci.nsISecretDecoderRing); - try { - sdr.encryptString("bacon"); - return true; - } catch(e) {} - return false; - }, - - /** - * Return a value for a backoff interval. Maximum is eight hours, unless - * Status.backoffInterval is higher. - * - */ - calculateBackoff: function calculateBackoff(attempts, baseInterval, - statusInterval) { - let backoffInterval = attempts * - (Math.floor(Math.random() * baseInterval) + - baseInterval); - return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), - statusInterval); - }, - - /** - * Return a set of hostnames (including the protocol) which may have - * credentials for sync itself stored in the login manager. - * - * In general, these hosts will not have their passwords synced, will be - * reset when we drop sync credentials, etc. - */ - getSyncCredentialsHosts: function() { - let result = new Set(this.getSyncCredentialsHostsLegacy()); - return result; - }, - - /* - * Get the "legacy" identity hosts. - */ - getSyncCredentialsHostsLegacy: function() { - // the legacy sync host - return new Set([PWDMGR_HOST]); - }, - - getDefaultDeviceName() { - // 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") || - Svc.Prefs.get("account") || Svc.Prefs.get("username"); - // A little hack for people using the the moz-build environment on Windows - // which sets USER to the literal "%USERNAME%" (yes, really) - if (user == "%USERNAME%" && env.get("USERNAME")) { - user = env.get("USERNAME"); - } - - let brand = new StringBundle("chrome://branding/locale/brand.properties"); - let brandName = brand.get("brandShortName"); - - let appName; - try { - let syncStrings = new StringBundle("chrome://weave/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 Str.sync.get("client.name2", [user, appName, system]); - } -}; - -XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() { - let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] - .createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - return converter; -}); - -/* - * Commonly-used services - */ -this.Svc = {}; -Svc.Prefs = new Preferences(PREFS_BRANCH); -Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true}); -Svc.Obs = Observers; - -var _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ? - "@mozilla.org/suite/sessionstore;1" : - "@mozilla.org/browser/sessionstore;1"; - -[ - ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], - ["Session", _sessionCID, "nsISessionStore"] -].forEach(function([name, contract, iface]) { - XPCOMUtils.defineLazyServiceGetter(Svc, name, contract, iface); -}); - -XPCOMUtils.defineLazyModuleGetter(Svc, "FormHistory", "resource://gre/modules/FormHistory.jsm"); - -Svc.__defineGetter__("Crypto", function() { - let cryptoSvc; - let ns = {}; - Cu.import("resource://services-crypto/WeaveCrypto.js", ns); - cryptoSvc = new ns.WeaveCrypto(); - delete Svc.Crypto; - return Svc.Crypto = cryptoSvc; -}); - -this.Str = {}; -["errors", "sync"].forEach(function(lazy) { - XPCOMUtils.defineLazyGetter(Str, lazy, Utils.lazyStrings(lazy)); -}); - -Svc.Obs.add("xpcom-shutdown", function () { - for (let name in Svc) - delete Svc[name]; -}); diff --git a/components/weave/weave-prefs.js b/components/weave/weave-prefs.js deleted file mode 100644 index dfce84767..000000000 --- a/components/weave/weave-prefs.js +++ /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/. */ - -pref("services.sync.serverURL", "https://pmsync.palemoon.org/sync/index.php/"); -pref("services.sync.userURL", "user/"); -pref("services.sync.miscURL", "misc/"); -pref("services.sync.termsURL", "http://www.palemoon.org/sync/terms.shtml"); -pref("services.sync.privacyURL", "http://www.palemoon.org/sync/privacy.shtml"); -pref("services.sync.statusURL", "https://pmsync.palemoon.org/status/"); -pref("services.sync.syncKeyHelpURL", "http://www.palemoon.org/sync/keyhelp.shtml"); - -pref("services.sync.lastversion", "firstrun"); -pref("services.sync.sendVersionInfo", true); - -pref("services.sync.scheduler.eolInterval", 604800); // 1 week -pref("services.sync.scheduler.idleInterval", 3600); // 1 hour -pref("services.sync.scheduler.activeInterval", 600); // 10 minutes -pref("services.sync.scheduler.immediateInterval", 90); // 1.5 minutes -pref("services.sync.scheduler.idleTime", 300); // 5 minutes - -pref("services.sync.scheduler.fxa.singleDeviceInterval", 3600); // 1 hour -pref("services.sync.scheduler.sync11.singleDeviceInterval", 86400); // 1 day - -pref("services.sync.errorhandler.networkFailureReportTimeout", 1209600); // 2 weeks - -// Our engines. -pref("services.sync.engine.addons", false); -pref("services.sync.engine.bookmarks", true); -pref("services.sync.engine.history", true); -pref("services.sync.engine.passwords", true); -pref("services.sync.engine.prefs", true); -pref("services.sync.engine.tabs", true); -pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*|blob:.*)$"); - -pref("services.sync.jpake.serverURL", "https://keyserver.palemoon.org/"); -pref("services.sync.jpake.pollInterval", 1000); -pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes -pref("services.sync.jpake.lastMsgMaxTries", 300); // 5 minutes -pref("services.sync.jpake.maxTries", 10); - -// Allow add-ons to be synced from non-trusted sources. -pref("services.sync.addons.ignoreRepositoryChecking", true); - -// If true, add-on sync ignores changes to the user-enabled flag. This -// allows people to have the same set of add-ons installed across all -// profiles while maintaining different enabled states. -pref("services.sync.addons.ignoreUserEnabledChanges", false); - -// Comma-delimited list of hostnames to trust for add-on install. -pref("services.sync.addons.trustedSourceHostnames", "addons.palemoon.org,addons.mozilla.org"); - -pref("services.sync.log.appender.console", "Warn"); -pref("services.sync.log.appender.dump", "Error"); -pref("services.sync.log.appender.file.level", "Trace"); -pref("services.sync.log.appender.file.logOnError", true); -pref("services.sync.log.appender.file.logOnSuccess", false); -pref("services.sync.log.appender.file.maxErrorAge", 864000); // 10 days -pref("services.sync.log.rootLogger", "Debug"); -pref("services.sync.log.logger.addonutils", "Debug"); -pref("services.sync.log.logger.declined", "Debug"); -pref("services.sync.log.logger.service.main", "Debug"); -pref("services.sync.log.logger.status", "Debug"); -pref("services.sync.log.logger.authenticator", "Debug"); -pref("services.sync.log.logger.network.resources", "Debug"); -pref("services.sync.log.logger.service.jpakeclient", "Debug"); -pref("services.sync.log.logger.engine.bookmarks", "Debug"); -pref("services.sync.log.logger.engine.clients", "Debug"); -pref("services.sync.log.logger.engine.forms", "Debug"); -pref("services.sync.log.logger.engine.history", "Debug"); -pref("services.sync.log.logger.engine.passwords", "Debug"); -pref("services.sync.log.logger.engine.prefs", "Debug"); -pref("services.sync.log.logger.engine.tabs", "Debug"); -pref("services.sync.log.logger.engine.addons", "Debug"); -pref("services.sync.log.logger.engine.apps", "Debug"); -pref("services.sync.log.logger.identity", "Debug"); -pref("services.sync.log.logger.userapi", "Debug"); -pref("services.sync.log.cryptoDebug", false); - -pref("services.sync.tokenServerURI", "https://token.services.mozilla.com/1.0/sync/1.5"); - -pref("services.sync.fxa.termsURL", "https://accounts.firefox.com/legal/terms"); -pref("services.sync.fxa.privacyURL", "https://accounts.firefox.com/legal/privacy"); diff --git a/system/network/build/nsNetModule.cpp b/system/network/build/nsNetModule.cpp index 8fd48d6e2..72be830cb 100644 --- a/system/network/build/nsNetModule.cpp +++ b/system/network/build/nsNetModule.cpp @@ -1131,10 +1131,6 @@ static const mozilla::Module::ContractIDEntry kNeckoContracts[] = { { NS_ABOUT_MODULE_CONTRACTID_PREFIX "profiles", &kNS_ABOUT_REDIRECTOR_MODULE_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "srcdoc", &kNS_ABOUT_REDIRECTOR_MODULE_CID }, { NS_ABOUT_MODULE_CONTRACTID_PREFIX "support", &kNS_ABOUT_REDIRECTOR_MODULE_CID }, -#ifdef MOZ_SERVICES_SYNC - { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-progress", &kNS_ABOUT_REDIRECTOR_MODULE_CID }, - { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_ABOUT_REDIRECTOR_MODULE_CID }, -#endif { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "socks", &kNS_SOCKSSOCKETPROVIDER_CID }, { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "socks4", &kNS_SOCKS4SOCKETPROVIDER_CID }, { NS_NETWORK_SOCKET_CONTRACTID_PREFIX "udp", &kNS_UDPSOCKETPROVIDER_CID }, diff --git a/system/network/protocol/about/nsAboutRedirector.cpp b/system/network/protocol/about/nsAboutRedirector.cpp index 72a76d8ca..35c3ef13c 100644 --- a/system/network/protocol/about/nsAboutRedirector.cpp +++ b/system/network/protocol/about/nsAboutRedirector.cpp @@ -165,18 +165,6 @@ static RedirEntry kRedirMap[] = { "chrome://global/content/aboutSupport.xhtml", nsIAboutModule::ALLOW_SCRIPT }, -#ifdef MOZ_SERVICES_SYNC - { - "sync-progress", - "chrome://weave/content/progress.xhtml", - nsIAboutModule::ALLOW_SCRIPT - }, - { - "sync-tabs", - "chrome://weave/content/aboutSyncTabs.xul", - nsIAboutModule::ALLOW_SCRIPT - }, -#endif }; static const int kRedirTotal = mozilla::ArrayLength(kRedirMap); |