diff options
Diffstat (limited to 'comm/suite/components/sync/content')
16 files changed, 3614 insertions, 0 deletions
diff --git a/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml b/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml new file mode 100644 index 0000000000..6365bf55fc --- /dev/null +++ b/comm/suite/components/sync/content/aboutSyncTabs-bindings.xml @@ -0,0 +1,46 @@ +<?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> + <xul:image class="tabIcon" + xbl:inherits="src=icon"/> + </xul:vbox> + <xul:vbox 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 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/comm/suite/components/sync/content/aboutSyncTabs.css b/comm/suite/components/sync/content/aboutSyncTabs.css new file mode 100644 index 0000000000..83782b4d5f --- /dev/null +++ b/comm/suite/components/sync/content/aboutSyncTabs.css @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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://communicator/content/aboutSyncTabs-bindings.xml#tab-listing); +} + +richlistitem[type="client"] { + -moz-binding: url(chrome://communicator/content/aboutSyncTabs-bindings.xml#client-listing); +} diff --git a/comm/suite/components/sync/content/aboutSyncTabs.js b/comm/suite/components/sync/content/aboutSyncTabs.js new file mode 100644 index 0000000000..e13c2be685 --- /dev/null +++ b/comm/suite/components/sync/content/aboutSyncTabs.js @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 {Weave} = ChromeUtils.import("resource://services-sync/main.js"); +const {PlacesUIUtils} = ChromeUtils.import("resource:///modules/PlacesUIUtils.jsm"); +const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm"); +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var RemoteTabViewer = { + _tabsList: null, + + init: function () { + Services.obs.addObserver(this, "weave:service:login:finish"); + Services.obs.addObserver(this, "weave:engine:sync:finish"); + + 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"); + }, + + buildList: function(force) { + if (!Weave.Service.isLoggedIn || !this._refetchTabs(force)) + return; + //XXXzpao We should say something about not being logged in & not having data + // or tell the appropriate condition. (bug 583344) + + this._generateTabList(); + }, + + 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.url; + + return item; + }, + + filterTabs: function(event) { + let val = event.target.value.toLowerCase(); + let numTabs = this._tabsList.getRowCount(); + let client = null; + for (let i = 0; i < numTabs; i++) { + let item = this._tabsList.getItemAtIndex(i); + let hide = false; // By default, make sure the item is visible. + switch (item.getAttribute("type")) { + case "tab": + if (item.getAttribute("url").toLowerCase().indexOf(val) == -1 && + item.getAttribute("title").toLowerCase().indexOf(val) == -1) + hide = true; + else + client = null; // This client should not be hidden. + break; + case "client": + if (client) + client.hidden = true; // Hide the last client, it had no visible tabs. + client = item; + break; + } + item.hidden = hide; + } + if (client) + client.hidden = true; // Hide the last client, it had no visible tabs. + }, + + 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().getBrowser().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.showMinimalAddBookmarkUI(uri, title); + }, + + bookmarkSelectedTabs: function() { + let items = this._tabsList.selectedItems; + let URIs = []; + let titles = []; + 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); + titles.push(items[i].getAttribute("title")); + } + } + if (URIs.length) + PlacesUIUtils.showMinimalAddMultiBookmarkUI(URIs, titles); + }, + + 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; + }, + + _generateTabList: function() { + let engine = Weave.Service.engineManager.get("tabs"); + 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); + } + + let seenURLs = new Set(); + let localURLs = engine.getOpenURLs(); + + for (let [guid, client] of Object.entries(engine.getAllClients())) { + 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"; + else // menuseparator + el.hidden = mode == "all"; + el = el.nextSibling; + } + }, + + _refetchTabs: function(force) { + if (!force) { + // Don't bother refetching tabs if we already did so recently + let lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch", 0); + let now = Math.floor(Date.now() / 1000); + if (now - lastFetch < 30) + return false; + } + + // if Clients hasn't synced yet this session, 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._generateTabList(); + break; + } + }, + + handleClick: function(event) { + if (event.target.getAttribute("type") == "tab" && event.button == 1) { + let url = event.target.getAttribute("url"); + openUILink(url, event); + let index = this._tabsList.getIndexOfItem(event.target); + this._tabsList.removeItemAt(index); + } + } +}; + +var EventDirector = { + handleEvent: function(event) { + switch (event.type) { + case "click": + RemoteTabViewer.handleClick(event); + break; + case "contextmenu": + RemoteTabViewer.adjustContextMenu(event); + break; + case "command": + switch (event.target.id) { + case "openSingleTab": + case "openSelectedTabs": + RemoteTabViewer.openSelected(); + break; + case "bookmarkSingleTab": + RemoteTabViewer.bookmarkSingleTab(); + break; + case "bookmarkSelectedTabs": + RemoteTabViewer.bookmarkSelectedTabs(); + break; + case "buildList": + RemoteTabViewer.buildList(); + break; + case "filterTabs": + RemoteTabViewer.filterTabs(event); + break; + } + break; + } + } +}; + +window.onload = function() { + RemoteTabViewer.init(); + + let tabsList = document.getElementById("tabsList"); + tabsList.addEventListener("click", EventDirector); + tabsList.addEventListener("contextmenu", EventDirector); + + document.getElementById("tabListContext") + .addEventListener("command", EventDirector); + document.getElementById("filterTabs") + .addEventListener("command", EventDirector); +} + +window.onunload = function() { + RemoteTabViewer.uninit(); +} diff --git a/comm/suite/components/sync/content/aboutSyncTabs.xul b/comm/suite/components/sync/content/aboutSyncTabs.xul new file mode 100644 index 0000000000..b0355557de --- /dev/null +++ b/comm/suite/components/sync/content/aboutSyncTabs.xul @@ -0,0 +1,69 @@ +<?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://communicator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/aboutSyncTabs.css" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/content/aboutSyncTabs.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://communicator/locale/aboutSyncTabs.dtd"> + %aboutSyncTabsDTD; +]> + +<window id="tabs-display" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&tabs.otherComputers.label;"> + + <script src="chrome://communicator/content/aboutSyncTabs.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + + <html:head> + <html:link rel="icon" href="chrome://communicator/skin/sync/sync-16.png"/> + </html:head> + + <popupset id="contextmenus"> + <menupopup id="tabListContext"> + <menuitem id="openSingleTab" + label="&tabs.context.openTab.label;" + accesskey="&tabs.context.openTab.accesskey;" + showFor="single"/> + <menuitem id="bookmarkSingleTab" + label="&tabs.context.bookmarkSingleTab.label;" + accesskey="&tabs.context.bookmarkSingleTab.accesskey;" + showFor="single"/> + <menuitem id="openSelectedTabs" + label="&tabs.context.openMultipleTabs.label;" + accesskey="&tabs.context.openMultipleTabs.accesskey;" + showFor="multiple"/> + <menuitem id="bookmarkSelectedTabs" + label="&tabs.context.bookmarkMultipleTabs.label;" + accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;" + showFor="multiple"/> + <menuseparator/> + <menuitem id="buildList" + label="&tabs.context.refreshList.label;" + accesskey="&tabs.context.refreshList.accesskey;" + showFor="all"/> + </menupopup> + </popupset> + <richlistbox id="tabsList" + context="tabListContext" + seltype="multiple" + class="plain" + align="center" + flex="1"> + <hbox id="headers" align="center"> + <label id="tabsListHeading" + value="&tabs.otherComputers.label;"/> + <spacer flex="1"/> + <textbox id="filterTabs" + type="search" + aria-controls="tabsList" + emptytext="&tabs.searchText.label;"/> + </hbox> + </richlistbox> +</window> diff --git a/comm/suite/components/sync/content/syncAddDevice.js b/comm/suite/components/sync/content/syncAddDevice.js new file mode 100644 index 0000000000..d986b54f00 --- /dev/null +++ b/comm/suite/components/sync/content/syncAddDevice.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 {Weave} = ChromeUtils.import("resource://services-sync/main.js"); +var {XPCOMUtils} = ChromeUtils.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 init() { + this.nextFocusEl = { pin1: this.pin2, + pin2: this.pin3, + pin3: this.wizard.getButton("next") }; + + this.throbber = document.getElementById("add-device-throbber"); + this.errorRow = document.getElementById("errorRow"); + }, + + onPageShow: function onPageShow() { + 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 onWizardAdvance() { + switch (this.wizard.pageIndex) { + case ADD_DEVICE_PAGE: + this.startTransfer(); + return false; + case DEVICE_CONNECTED_PAGE: + window.close(); + return false; + } + return true; + }, + + startTransfer: function startTransfer() { + 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 onPaired() { + 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 onComplete() { + 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 onAbort(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 onWizardBack() { + if (this.wizard.pageIndex != SYNC_KEY_PAGE) + return true; + + this.wizard.pageIndex = ADD_DEVICE_PAGE; + return false; + }, + + onWizardCancel: function onWizardCancel() { + if (this._jpakeclient) { + this._jpakeclient.abort(); + delete this._jpakeclient; + } + return true; + }, + + onTextBoxInput: function onTextBoxInput(textbox) { + this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH && + this.pin2.value.length == PIN_PART_LENGTH && + this.pin3.value.length == PIN_PART_LENGTH); + if (textbox && textbox.value.length == PIN_PART_LENGTH) + this.nextFocusEl[textbox.id].focus(); + }, + + goToSyncKeyPage: function goToSyncKeyPage() { + 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/comm/suite/components/sync/content/syncAddDevice.xul b/comm/suite/components/sync/content/syncAddDevice.xul new file mode 100644 index 0000000000..e7a30d2aa3 --- /dev/null +++ b/comm/suite/components/sync/content/syncAddDevice.xul @@ -0,0 +1,128 @@ +<?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://communicator/skin/sync/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/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="&addDevice.title.label;" + windowtype="Sync:AddDevice" + persist="screenX screenY" + onwizardnext="return gSyncAddDevice.onWizardAdvance();" + onwizardback="return gSyncAddDevice.onWizardBack();" + onwizardcancel="gSyncAddDevice.onWizardCancel();" + onload="gSyncAddDevice.init();"> + + <script src="chrome://communicator/content/sync/syncAddDevice.js"/> + <script src="chrome://communicator/content/sync/syncUtils.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + <script src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&addDevice.title.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &addDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <spacer flex="1"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <spacer flex="1"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + size="4" + maxlength="4" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();"/> + <textbox id="pin2" + class="pin" + size="4" + maxlength="4" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();"/> + <textbox id="pin3" + class="pin" + size="4" + maxlength="4" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();"/> + </vbox> + <spacer flex="1"/> + <vbox id="add-device-throbber" 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/comm/suite/components/sync/content/syncGenericChange.js b/comm/suite/components/sync/content/syncGenericChange.js new file mode 100644 index 0000000000..13cab89811 --- /dev/null +++ b/comm/suite/components/sync/content/syncGenericChange.js @@ -0,0 +1,232 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 {Weave} = ChromeUtils.import("resource://services-sync/main.js"); +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var Change = { + _dialog: null, + _dialogType: null, + _status: null, + _statusIcon: null, + _firstBox: null, + _secondBox: null, + _passphraseBox: null, + + get _currentPasswordInvalid() { + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + }, + + get _updatingPassphrase() { + return this._dialogType == "UpdatePassphrase"; + }, + + onLoad: function Change_onLoad() { + /* Load labels */ + let introText = document.getElementById("introText"); + 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._passphraseBox = document.getElementById("passphraseBox"); + + this._dialog.getButton("finish").disabled = true; + this._dialog.getButton("back").hidden = true; + + this._stringBundle = + Services.strings.createBundle("chrome://communicator/locale/sync/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; + this._passphraseBox.readOnly = true; + 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.recoverykey.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 _clearStatus() { + this._status.textContent = ""; + this._statusIcon.removeAttribute("status"); + }, + + _updateStatus: function Change__updateStatus(str, state) { + this._updateStatusWithString(this._str(str), state); + }, + + _updateStatusWithString: function Change__updateStatusWithString(string, state) { + this._statusRow.hidden = false; + this._status.textContent = 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._clearStatus(); + this._dialog.getButton("finish").disabled = false; + }, + + doChangePassphrase: function Change_doChangePassphrase() { + 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(); + } + 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 Change_doChangePassword() { + 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 () { + 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 { + if (!this._updatingPassphrase) + return; + + valid = this._passphraseBox.value != ""; + } + + if (errorString == "") + this._clearStatus(); + else + this._updateStatusWithString(errorString, "error"); + + this._statusRow.hidden = valid; + this._dialog.getButton("finish").disabled = !valid; + }, + + _str: function Change__string(str) { + try { + return this._stringBundle.GetStringFromName(str); + } catch (e) { + Cu.reportError("Missing string: " + str); + throw e; + } + } +}; diff --git a/comm/suite/components/sync/content/syncGenericChange.xul b/comm/suite/components/sync/content/syncGenericChange.xul new file mode 100644 index 0000000000..aac4a004fa --- /dev/null +++ b/comm/suite/components/sync/content/syncGenericChange.xul @@ -0,0 +1,116 @@ +<?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://communicator/skin/sync/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/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 src="chrome://communicator/content/sync/syncGenericChange.js"/> + <script src="chrome://communicator/content/sync/syncUtils.js"/> + <script src="chrome://global/content/printUtils.js"/> + + <wizardpage id="change-page" + label=""> + + <description id="introText"/> + + <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="&recoveryGenerateNewKey.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="https://services.mozilla.com/sync/help/manual-setup"> + &addDevice.showMeHow.label; + </label> + </description> + </vbox> + + <spacer id="passphraseSpacer" flex="1" hidden="true"/> + + <description id="warningText" class="data"/> + + <spacer flex="1"/> + </wizardpage> +</wizard> diff --git a/comm/suite/components/sync/content/syncKey.xhtml b/comm/suite/components/sync/content/syncKey.xhtml new file mode 100644 index 0000000000..6ce35fa1cc --- /dev/null +++ b/comm/suite/components/sync/content/syncKey.xhtml @@ -0,0 +1,49 @@ +<?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://communicator/locale/sync/syncBrand.dtd"> + %syncBrandDTD; + <!ENTITY % syncKeyDTD SYSTEM "chrome://communicator/locale/sync/syncKey.dtd"> + %syncKeyDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&syncKey.page.title;</title> + <meta name="robots" content="noindex"/> + <style type="text/css"> + #synckey { font-size: 1.5em; } + footer { font-size: 0.67em; } + </style> +</head> + +<body> +<h1>&syncKey.page.title;</h1> + +<p id="synckey">SYNCKEY</p> + +<p>&syncKey.page.description;</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.keepItSafe4.description;</p> +</div> + +<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</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/comm/suite/components/sync/content/syncNotification.xml b/comm/suite/components/sync/content/syncNotification.xml new file mode 100644 index 0000000000..a33a06c030 --- /dev/null +++ b/comm/suite/components/sync/content/syncNotification.xml @@ -0,0 +1,93 @@ +<?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"> + + <binding id="notificationbox" extends="chrome://global/content/bindings/notification.xml#notificationbox"> + <implementation> + <constructor><![CDATA[ + let localScope = {}; + ChromeUtils.import("resource://services-common/observers.js", localScope); + ChromeUtils.import("resource://services-sync/notifications.js", localScope); + + localScope.Observers.add("weave:notification:added", this.onNotificationAdded, this); + localScope.Observers.add("weave:notification:removed", this.onNotificationRemoved, this); + localScope.Notifications.notifications.forEach(this._appendNotification, this); + ]]></constructor> + + <destructor><![CDATA[ + let localScope = {}; + ChromeUtils.import("resource://services-common/observers.js", localScope); + localScope.Observers.remove("weave:notification:added", this.onNotificationAdded, this); + localScope.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 (let notification of 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"> + <implementation> + <method name="close"> + <body><![CDATA[ + let localScope = {}; + ChromeUtils.import("resource://services-sync/notifications.js", localScope); + localScope.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/comm/suite/components/sync/content/syncQuota.js b/comm/suite/components/sync/content/syncQuota.js new file mode 100644 index 0000000000..c4d2d03676 --- /dev/null +++ b/comm/suite/components/sync/content/syncQuota.js @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 {Weave} = ChromeUtils.import("resource://services-sync/main.js"); +const {DownloadUtils} = ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm"); + +var gSyncQuota = { + + init: function init() { + this.bundle = document.getElementById("quotaStrings"); + let caption = document.getElementById("treeCaption"); + caption.textContent = this.bundle.getString("quota.treeCaption.label"); + + gUsageTreeView.init(); + this.tree = document.getElementById("usageTree"); + this.tree.view = gUsageTreeView; + + this.loadData(); + }, + + loadData: function loadData() { + 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)); + }); + }, + + uninit: function uninit() { + if (this._usage_req) + this._usage_req.abort(); + if (this._quota_req) + this._quota_req.abort(); + }, + + onAccept: function onAccept() { + let engines = gUsageTreeView.getEnginesToDisable(); + for (let engine of 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 true; + }, + + convertKB: function convertKB(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 init() { + let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label"); + for (let engine of 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 _collectionTitle(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 displayUsageData(data) { + for (let coll of this._collections) { + coll.size = 0; + // If we couldn't retrieve any data, just blank out the label. + if (!data) { + coll.sizeLabel = ""; + continue; + } + + for (let engineName of 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 onTreeClick(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 toggle(row) { + // Update the tree + let collection = this._collections[row]; + collection.enabled = !collection.enabled; + this.treeBox.invalidateRow(row); + + // Display which ones will be removed + let freeup = 0; + let toremove = []; + for (let collection of this._collections) { + if (collection.enabled) + continue; + toremove.push(collection.name); + freeup += collection.size; + } + + let caption = document.getElementById("treeCaption"); + if (!toremove.length) { + caption.className = ""; + caption.textContent = gSyncQuota.bundle.getString("quota.treeCaption.label"); + return; + } + + toremove = toremove.map(coll => this._byname[coll].title); + toremove = toremove.join(gSyncQuota.bundle.getString("quota.list.separator")); + caption.textContent = gSyncQuota.bundle.getFormattedString( + "quota.removal.label", [toremove]); + if (freeup) + caption.textContent += gSyncQuota.bundle.getFormattedString( + "quota.freeup.label", gSyncQuota.convertKB(freeup)); + caption.className = "captionWarning"; + }, + + /* + * Return a list of engines (or rather their pref names) that should be + * disabled. + */ + getEnginesToDisable: function getEnginesToDisable() { + return this._collections.filter(coll => !coll.enabled).map(coll => coll.name); + }, + + // 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 getCellText(row, col) { + let collection = this._collections[row]; + switch (col.id) { + case "collection": + return collection.title; + case "size": + return collection.sizeLabel; + default: + return ""; + } + }, + + setTree: function setTree(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) {}, +}; diff --git a/comm/suite/components/sync/content/syncQuota.xul b/comm/suite/components/sync/content/syncQuota.xul new file mode 100644 index 0000000000..f7b1fd7aa4 --- /dev/null +++ b/comm/suite/components/sync/content/syncQuota.xul @@ -0,0 +1,62 @@ +<?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://communicator/skin/sync/syncQuota.css" type="text/css"?> + +<!DOCTYPE dialog [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd"> +<!ENTITY % syncQuotaDTD SYSTEM "chrome://communicator/locale/sync/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();" + onunload="gSyncQuota.uninit();" + buttons="accept,cancel" + title=""a.dialogTitle.label;" + ondialogaccept="return gSyncQuota.onAccept();"> + + <script src="chrome://communicator/content/sync/syncQuota.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="quotaStrings" + src="chrome://communicator/locale/sync/syncQuota.properties"/> + </stringbundleset> + + <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/> + </tree> + <separator/> + <description id="treeCaption"/> + +</dialog> diff --git a/comm/suite/components/sync/content/syncSetup.js b/comm/suite/components/sync/content/syncSetup.js new file mode 100644 index 0000000000..c648948568 --- /dev/null +++ b/comm/suite/components/sync/content/syncSetup.js @@ -0,0 +1,961 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// page consts + +const INTRO_PAGE = 0; +const NEW_ACCOUNT_START_PAGE = 1; +const NEW_ACCOUNT_PP_PAGE = 2; +const NEW_ACCOUNT_CAPTCHA_PAGE = 3; +const EXISTING_ACCOUNT_CONNECT_PAGE = 4; +const EXISTING_ACCOUNT_LOGIN_PAGE = 5; +const OPTIONS_PAGE = 6; +const OPTIONS_CONFIRM_PAGE = 7; +const SETUP_SUCCESS_PAGE = 8; + +// 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 {Weave} = ChromeUtils.import("resource://services-sync/main.js"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {PlacesUtils} = ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm"); +const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); + +var gSyncSetup = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + captchaBrowser: null, + wizard: null, + _disabledSites: [], + + status: { + password: false, + email: false, + server: true + }, + + get _remoteSites() { + return [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:changepassphrase", "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", () => addRem(false)); + + setTimeout(function () { + // Force Service to be loaded so that engines are registered. + // See Bug 670082. + Weave.Service; + }, 0); + + this.captchaBrowser = document.getElementById("captcha"); + this.wizard = document.getElementById("accountSetup"); + + if (window.arguments && window.arguments[0] == true) { + // we're resetting sync + this._resettingSync = true; + this.wizard.pageIndex = OPTIONS_PAGE; + } + else { + this.wizard.canAdvance = false; + this.captchaBrowser.addProgressListener(this); + Weave.Svc.Prefs.set("firstSync", "notReady"); + } + + 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; + this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE; + }, + + resetPassphrase: function resetPassphrase() { + // 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].includes(Weave.Status.login)) + 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); + }, + + 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").value; + 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"), false)) + return true; + } + return false; + } + // Default, e.g. wizard's special page -1 etc. + return true; + }, + + 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 valid, str; + if (password.value == document.getElementById("weavePassphrase").value) { + // xxxmpc - hack, sigh + valid = false; + str = Weave.Utils.getErrorString("change.password.pwSameAsRecoveryKey"); + } + else { + let pwconfirm = document.getElementById("weavePasswordConfirm"); + [valid, str] = gSyncUtils.validatePassword(password, pwconfirm); + } + + let feedback = document.getElementById("passwordFeedbackRow"); + this._setFeedback(feedback, valid, str); + + this.status.password = valid; + this.checkFields(); + }, + + onPassphraseGenerate: function () { + let passphrase = Weave.Utils.generatePassphrase(); + Weave.Service.identity.syncKey = passphrase; + let el = document.getElementById("weavePassphrase"); + el.value = Weave.Utils.hyphenatePassphrase(passphrase); + }, + + onPageShow: function() { + switch (this.wizard.pageIndex) { + case INTRO_PAGE: + this.wizard.getButton("next").hidden = true; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("extra1").hidden = true; + break; + case NEW_ACCOUNT_PP_PAGE: + document.getElementById("saveSyncKeyButton").focus(); + let el = document.getElementById("weavePassphrase"); + if (!el.value) + this.onPassphraseGenerate(); + 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.wizard.canRewind = true; + this.checkFields(); + break; + case EXISTING_ACCOUNT_CONNECT_PAGE: + 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.canRewind = true; + this.checkFields(); + break; + case SETUP_SUCCESS_PAGE: + this.wizard.canRewind = false; + this.wizard.canAdvance = true; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("next").hidden = true; + this.wizard.getButton("cancel").hidden = true; + this.wizard.getButton("finish").hidden = false; + this._handleSuccess(); + 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; + + if (!this.wizard.pageIndex) + return true; + + switch (this.wizard.pageIndex) { + 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(); + return this.wizard.canAdvance; + case NEW_ACCOUNT_CAPTCHA_PAGE: + 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"); + feedback.hidden = false; + + 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; + this._handleNoScript(false); + this.wizard.pageIndex = SETUP_SUCCESS_PAGE; + return false; + } + + image.setAttribute("status", "error"); + label.value = Weave.Utils.getErrorString(error); + return false; + case NEW_ACCOUNT_PP_PAGE: + // Time to load the captcha. + // First check for NoScript and whitelist the right sites. + this._handleNoScript(true); + this.captchaBrowser.loadURI(Weave.Service.miscAPI + "captcha_html"); + break; + 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.wizard.pageIndex = SETUP_SUCCESS_PAGE; + 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.onWizardFinish(); + window.close(); + 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 OPTIONS_CONFIRM_PAGE: + // Backing up from the confirmation page = resetting first sync to merge. + document.getElementById("mergeChoiceRadio").selectedIndex = 0; + return this.returnFromOptions(); + } + return true; + }, + + onWizardFinish: function () { + this.setupInitialSync(); + + 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])); + } + 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"); + if (this._settingUpNew) + gSyncUtils.openFirstClientFirstrun(); + else + gSyncUtils.openAddedClientFirstrun(); + } + + if (!Weave.Service.isLoggedIn) + Weave.Service.login(); + + Weave.Utils.nextTick(Weave.Service.sync, Weave.Service); + }, + + onWizardCancel: function () { + if (this._resettingSync) + return; + + if (this.wizard.pageIndex == SETUP_SUCCESS_PAGE) { + this.onWizardFinish(); + 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; + }, + + 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 displayPIN(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 onPairingStart() {}, + + onPaired: function onPaired() {}, + + onComplete: function onComplete(credentials) { + Weave.Service.identity.account = credentials.account; + Weave.Service.identity.basicPassword = credentials.password; + Weave.Service.identity.syncKey = credentials.synckey; + Weave.Service.serverURL = credentials.serverURL; + self.wizard.pageIndex = SETUP_SUCCESS_PAGE; + }, + + onAbort: function onAbort(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. + if (!("@maone.net/noscript-service;1" in Cc)) + return; + + let ns = Cc["@maone.net/noscript-service;1"].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 = []; + } + }, + + _updateControl: function(controlId) { + let control = document.getElementById(controlId); + if (control.selectedIndex == 0) { + control.editable = false; + Weave.Svc.Prefs.reset("serverURL"); + } else { + // Prevent double selection upon using down key. + control.activeChild = null; + control.editable = true; + control.value = ""; + } + // Force a style flush to ensure that the binding is attached. + control.clientTop; + control.focus(); + return control; + }, + + onExistingServerCommand: function () { + this._updateControl("existingServer"); + 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 () { + document.getElementById("TOSRow").hidden = !this._usingMainServers; + let control = this._updateControl("server"); + if (control.selectedIndex != 0) { + // checkServer() will call checkAccount() and checkFields(). + this.checkServer(); + return; + } + 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, true); + 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(); + }, + + // xxxmpc - checkRemote is a hack, we can't verify a minimal server is live + // without auth, so we won't validate in the existing-server case. + _validateServer: function (element, checkRemote) { + 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 && checkRemote) { + 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); + } + 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; + }, + + _handleSuccess: function() { + let self = this; + function fill(id, string) { + document.getElementById(id).textContent = + string ? self._stringBundle.GetStringFromName(string) : ""; + } + + fill("firstSyncAction", ""); + fill("firstSyncActionWarning", ""); + if (this._settingUpNew) { + fill("firstSyncAction", "newAccount.action.label"); + fill("firstSyncActionChange", "newAccount.change.label"); + return; + } + fill("firstSyncActionChange", "existingAccount.change.label"); + let action = document.getElementById("mergeChoiceRadio").value; + let id = action == "resetClient" ? "firstSyncAction" : "firstSyncActionWarning"; + fill(id, action + ".change.label"); + }, + + _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; + } + + let addonsEngine = Weave.Service.engineManager.get("addons"); + if (addonsEngine.enabled) { + let ids = addonsEngine._store.getAllIDs(); + let blessedcount = Object.keys(ids).filter(id => ids[id]).length; + // 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; + } + + if (!Weave.Service.engineManager.get("prefs").enabled) { + document.getElementById("prefsWipe").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 (let name of 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); + }, + + 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 the captcha, assume it's not needed and move on + if (request.QueryInterface(Ci.nsIHttpChannel).responseStatus == 404) + this.onWizardAdvance(); + }, + onProgressChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function() {}, + onLocationChange: function () {} +}; + +// onWizardAdvance() and onPageShow() are run before init(), so we'll set +// wizard & _stringBundle up as lazy getters. +XPCOMUtils.defineLazyGetter(gSyncSetup, "wizard", function() { + return document.getElementById("accountSetup"); +}); +XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() { + return Services.strings.createBundle("chrome://communicator/locale/sync/syncSetup.properties"); +}); diff --git a/comm/suite/components/sync/content/syncSetup.xul b/comm/suite/components/sync/content/syncSetup.xul new file mode 100644 index 0000000000..2df682bb82 --- /dev/null +++ b/comm/suite/components/sync/content/syncSetup.xul @@ -0,0 +1,482 @@ +<?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://communicator/skin/sync/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/sync/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://communicator/locale/sync/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://communicator/locale/sync/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard id="accountSetup" 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();" + onwizardfinish="gSyncSetup.onWizardFinish();" + onwizardcancel="gSyncSetup.onWizardCancel();" + onload="gSyncSetup.init();"> + + <script src="chrome://communicator/content/sync/syncSetup.js"/> + <script src="chrome://communicator/content/sync/syncUtils.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + <script src="chrome://global/content/printUtils.js"/> + + <wizardpage id="pickSetupType" + label="&syncBrand.fullName.label;" + onpageshow="gSyncSetup.onPageShow();"> + <vbox align="center" flex="1"> + <description id="pickSetupDesc"> + &setup.pickSetupType.description; + </description> + <spacer flex="1"/> + <button id="newAccount" + class="accountChoiceButton" + label="&button.createNewAccount.label;" + oncommand="gSyncSetup.startNewAccountSetup();" + align="center"/> + <spacer flex="3"/> + </vbox> + <separator class="groove"/> + <vbox align="center" flex="1"> + <spacer flex="3"/> + <label value="&setup.haveAccount.label;"/> + <spacer flex="1"/> + <button id="existingAccount" + class="accountChoiceButton" + label="&button.connect.label;" + oncommand="gSyncSetup.useExistingAccount();"/> + <spacer flex="3"/> + </vbox> + </wizardpage> + + <wizardpage id="newAccountStart" + label="&setup.newAccountDetailsPage.title.label;" + 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="&signIn.password.label;" + accesskey="&signIn.password.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" + accesskey="&server.accesskey;" + value="&server.label;"/> + <menulist id="server" + oncommand="gSyncSetup.onServerCommand();" + oninput="gSyncSetup.onServerInput();"> + <menupopup> + <menuitem label="&serverType.main.label;"/> + <menuitem label="&serverType.custom2.label;"/> + </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> + </wizardpage> + + <wizardpage id="newSyncKey" + label="&setup.newRecoveryKeyPage.title.label;" + onextra1="gSyncSetup.onSyncOptions();" + onpageshow="gSyncSetup.onPageShow();"> + <description> + &setup.newRecoveryKeyPage.description.label; + </description> + <spacer/> + + <groupbox> + <label value="&recoveryKeyEntry.label;" + accesskey="&recoveryKeyEntry.accesskey;" + control="weavePassphrase"/> + <textbox id="weavePassphrase" + readonly="true" + onfocus="this.select();"/> + </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="captchaEntry" + label="&setup.captchaPage2.title.label;" + onextra1="gSyncSetup.onSyncOptions();"> + <vbox flex="1" align="center"> + <browser height="150" + width="450" + id="captcha" + type="content" + disablehistory="true"/> + <spacer flex="1"/> + <hbox id="captchaFeedback" hidden="true"> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + <spacer flex="3"/> + </vbox> + </wizardpage> + + <wizardpage id="addDevice" + label="&addDevice.title.label;" + onextra1="gSyncSetup.onSyncOptions();" + onpageshow="gSyncSetup.onPageShow();"> + <description> + &addDevice.setup.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/easy-setup"/> + </description> + <description>&addDevice.setup.enterCode.label;</description> + <spacer flex="1"/> + <vbox align="center" flex="1"> + <textbox id="easySetupPIN1" + class="pin" + value="" + size="4" + disabled="true"/> + <textbox id="easySetupPIN2" + class="pin" + value="" + size="4" + disabled="true"/> + <textbox id="easySetupPIN3" + class="pin" + value="" + size="4" + disabled="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="existingAccountName"/> + <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" + accesskey="&server.accesskey;" + value="&server.label;"/> + <menulist id="existingServer" + oncommand="gSyncSetup.onExistingServerCommand();" + oninput="gSyncSetup.onExistingServerInput();"> + <menupopup> + <menuitem label="&serverType.main.label;"/> + <menuitem label="&serverType.custom2.label;"/> + </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="https://services.mozilla.com/sync/help/manual-setup"> + &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 class="inputColumn" flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&syncComputerName.label;" + accesskey="&syncComputerName.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="true"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + id="engine.bookmarks" + checked="true"/> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + id="engine.history" + 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.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 value="resetClient" + label="&choice2.merge.main.label;" + class="mergeChoiceButton"/> + <label value="&choice2.merge.recommended.label;" class="recommended"/> + </row> + <row align="center"> + <radio value="wipeClient" + label="&choice2.client.main.label;" + class="mergeChoiceButton"/> + </row> + <row align="center"> + <radio value="wipeRemote" + label="&choice2.server.main.label;" + class="mergeChoiceButton"/> + </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.merge.label; + </description> + </vbox> + <vbox id="chosenActionWipeClient" class="confirm"> + <description class="normal"> + &confirm.client2.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.client.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> + + <wizardpage id="successfulSetup" + label="&setup.successPage.title;" + onextra1="gSyncSetup.onSyncOptions();" + onpageshow="gSyncSetup.onPageShow();"> + <vbox align="center"> + <image id="successPageIcon"/> + </vbox> + <separator/> + <description class="normal"> + <html:span id="firstSyncAction"/> + <html:strong id="firstSyncActionWarning"/> + <html:span id="firstSyncActionChange"/> + </description> + <description> + &continueUsing.label; + </description> + </wizardpage> +</wizard> diff --git a/comm/suite/components/sync/content/syncUI.js b/comm/suite/components/sync/content/syncUI.js new file mode 100644 index 0000000000..668447c46d --- /dev/null +++ b/comm/suite/components/sync/content/syncUI.js @@ -0,0 +1,454 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 gSyncUI = { + _obs: ["weave:notification:added", + "weave:service:sync:start", + "weave:service:sync:delayed", + "weave:service:quota:remaining", + "weave:service:setup-complete", + "weave:service:login:start", + "weave:service:login:finish", + "weave:service:logout:finish", + "weave:service:start-over", + "weave:ui:login:error", + "weave:ui:sync:error", + "weave:ui:sync:finish", + "weave:ui:clear-error"], + + _unloaded: false, + + init: function SUI_init() { + // Update the Tools menu according to whether Sync is set up or not. + let taskPopup = document.getElementById("taskPopup"); + if (taskPopup) + taskPopup.addEventListener("popupshowing", this.updateUI.bind(this)); + + // Proceed to set up the UI if Sync has already started up. + // Otherwise we'll do it when Sync is firing up. + if (Cc["@mozilla.org/weave/service;1"] + .getService().wrappedJSObject.ready) { + this.initUI(); + return; + } + + Services.obs.addObserver(this, "weave:service:ready", true); + + // Remove the observer if the window is closed before the observer + // was triggered. + window.addEventListener("unload", function SUI_unload() { + gSyncUI._unloaded = true; + window.removeEventListener("unload", SUI_unload); + Services.obs.removeObserver(gSyncUI, "weave:service:ready"); + + if (Weave.Status.ready) { + gSyncUI._obs.forEach(function(topic) { + Services.obs.removeObserver(gSyncUI, topic); + }); + } + }); + }, + + initUI: function SUI_initUI() { + this._obs.forEach(function(topic) { + Services.obs.addObserver(this, topic, true); + }, this); + + // Find the alltabs-popup + let popup = document.getElementById("alltabs-popup"); + if (popup) { + popup.addEventListener( + "popupshowing", this.alltabsPopupShowing.bind(this), true); + + if (Weave.Notifications.notifications.length) + this.initNotifications(); + } + this.updateUI(); + }, + + initNotifications: function SUI_initNotifications() { + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let notificationbox = document.createElementNS(XULNS, "notificationbox"); + notificationbox.id = "sync-notifications"; + + let statusbar = document.getElementById("status-bar"); + statusbar.parentNode.insertBefore(notificationbox, statusbar); + + // Force a style flush to ensure that our binding is attached. + notificationbox.clientTop; + + // notificationbox will listen to observers from now on. + Services.obs.removeObserver(this, "weave:notification:added"); + }, + + _wasDelayed: false, + + _needsSetup: function SUI__needsSetup() { + let firstSync = ""; + try { + firstSync = Services.prefs.getCharPref("services.sync.firstSync"); + } catch (e) { } + return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + firstSync == "notReady"; + }, + + updateUI: function SUI_updateUI() { + let needsSetup = this._needsSetup(); + document.getElementById("sync-setup-state").hidden = !needsSetup; + document.getElementById("sync-syncnow-state").hidden = needsSetup; + + let syncButton = document.getElementById("sync-button"); + if (syncButton) { + syncButton.removeAttribute("status"); + this._updateLastSyncTime(); + if (needsSetup) + syncButton.removeAttribute("tooltiptext"); + } + }, + + alltabsPopupShowing: function(event) { + // Should we show the menu item? + //XXXphilikon We should remove the check for isLoggedIn here and have + // about:sync-tabs auto-login (bug 583344) + if (!Weave.Service.isLoggedIn || !Weave.Service.engineManager.get("tabs").enabled) + return; + + let label = this._stringBundle.GetStringFromName("tabs.fromOtherComputers.label"); + + let popup = document.getElementById("alltabs-popup"); + if (!popup) + return; + + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("id", "sync-tabs-menuitem"); + menuitem.setAttribute("label", label); + menuitem.setAttribute("class", "alltabs-item"); + menuitem.setAttribute("oncommand", "BrowserOpenSyncTabs();"); + + let sep = document.createElement("menuseparator"); + sep.setAttribute("id", "sync-tabs-sep"); + + // Fake the tab object on the menu entries, so that we don't have to worry + // about removing them ourselves. They will just get cleaned up by popup + // binding. This also makes sure the statusbar updates with the URL. + menuitem.tab = { "linkedBrowser": { "currentURI": { "spec": label } } }; + sep.tab = { "linkedBrowser": { "currentURI": { "spec": " " } } }; + + popup.insertBefore(sep, popup.firstChild); + popup.insertBefore(menuitem, sep); + }, + + // Functions called by observers + onActivityStart: function SUI_onActivityStart() { + let syncButton = document.getElementById("sync-button"); + if (syncButton) + syncButton.setAttribute("status", "active"); + }, + + onSyncDelay: function SUI_onSyncDelay() { + // basically, we want to just inform users that stuff is going to take a while + let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + let description = this._stringBundle.GetStringFromName("error.sync.no_node_found"); + let buttons = [new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )]; + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_INFO, buttons); + Weave.Notifications.replaceTitle(notification); + this._wasDelayed = true; + }, + + onLoginFinish: function SUI_onLoginFinish() { + // Clear out any login failure notifications + let title = this._stringBundle.GetStringFromName("error.login.title"); + this.clearError(title); + }, + + onLoginError: function SUI_onLoginError() { + // if login fails, any other notifications are essentially moot + Weave.Notifications.removeAll(); + + // if we haven't set up the client, don't show errors + if (this._needsSetup()) { + this.updateUI(); + return; + } + + let title = this._stringBundle.GetStringFromName("error.login.title"); + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let reason = Weave.Utils.getErrorString(Weave.Status.login); + description = + this._stringBundle.formatStringFromName("error.sync.description", [reason], 1); + } + + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.login.prefs.label"), + this._stringBundle.GetStringFromName("error.login.prefs.accesskey"), + function() { gSyncUI.openPrefs(); return true; } + )); + + let notification = new Weave.Notification(title, description, null, + Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + this.updateUI(); + }, + + onLogout: function SUI_onLogout() { + this.updateUI(); + }, + + onStartOver: function SUI_onStartOver() { + this.clearError(); + }, + + onQuotaNotice: function onQuotaNotice(subject, data) { + let title = this._stringBundle.GetStringFromName("warning.sync.quota.label"); + let description = this._stringBundle.GetStringFromName("warning.sync.quota.description"); + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"), + function() { gSyncUI.openQuotaDialog(); return true; } + )); + + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + }, + + openServerStatus: function () { + let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); + openUILinkIn(statusURL, "tab"); + }, + + // Commands + doSync: function SUI_doSync() { + setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0); + }, + + handleToolbarButton: function SUI_handleToolbarButton() { + if (this._needsSetup()) + this.openSetup(); + else + this.doSync(); + }, + + //XXXzpao should be part of syncCommon.js - which we might want to make a module... + // To be fixed in a followup (bug 583366) + openSetup: function SUI_openSetup() { + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) + win.focus(); + else { + window.openDialog("chrome://communicator/content/sync/syncSetup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no"); + } + }, + + openQuotaDialog: function SUI_openQuotaDialog() { + let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); + if (win) + win.focus(); + else + Services.ww.activeWindow.openDialog( + "chrome://communicator/content/sync/syncQuota.xul", "", + "centerscreen,chrome,dialog,modal"); + }, + + openPrefs: function SUI_openPrefs() { + goPreferences("sync_pane"); + }, + + + // Helpers + _updateLastSyncTime: function SUI__updateLastSyncTime() { + let syncButton = document.getElementById("sync-button"); + if (!syncButton) + return; + + let lastSync; + try { + lastSync = Services.prefs.getCharPref("services.sync.lastSync"); + } + catch (e) { }; + if (!lastSync || this._needsSetup()) { + syncButton.removeAttribute("tooltiptext"); + return; + } + + // Show the day-of-week and time (HH:MM) of last sync + let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let lastSyncLabel = + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); + syncButton.setAttribute("tooltiptext", lastSyncLabel); + }, + + clearError: function SUI_clearError(errorString) { + Weave.Notifications.removeAll(errorString); + this.updateUI(); + }, + + onSyncFinish: function SUI_onSyncFinish() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + // Clear out sync failures on a successful sync + this.clearError(title); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + this.clearError(title); + this._wasDelayed = false; + } + }, + + onSyncError: function SUI_onSyncError() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) { + this.onLoginError(); + return; + } + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let error = Weave.Utils.getErrorString(Weave.Status.sync); + description = + this._stringBundle.formatStringFromName("error.sync.description", [error], 1); + } + let priority = Weave.Notifications.PRIORITY_WARNING; + let buttons = []; + + // Check if the client is outdated in some way + let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE; + for (let [engine, reason] of Object.entries(Weave.Status.engines)) + outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE; + + if (outdated) { + description = this._stringBundle.GetStringFromName( + "error.sync.needUpdate.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.needUpdate.label"), + this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"), + function() { window.openUILinkIn("https://services.mozilla.com/update/", "tab"); return true; } + )); + } + else if (Weave.Status.sync == Weave.OVER_QUOTA) { + description = this._stringBundle.GetStringFromName( + "error.sync.quota.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.accesskey"), + function() { gSyncUI.openQuotaDialog(); return true; } ) + ); + } + else if (Weave.Status.enforceBackoff) { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )); + } + else { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"), + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"), + function() { gSyncUI.doSync(); return true; } + )); + } + + let notification = + new Weave.Notification(title, description, null, priority, buttons); + Weave.Notifications.replaceTitle(notification); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + Weave.Notifications.removeAll(title); + this._wasDelayed = false; + } + + this.updateUI(); + }, + + observe: function SUI_observe(subject, topic, data) { + if (this._unloaded) + throw "SyncUI observer called after unload: " + topic; + + switch (topic) { + case "weave:service:sync:start": + this.onActivityStart(); + break; + case "weave:ui:sync:finish": + this.onSyncFinish(); + break; + case "weave:ui:sync:error": + this.onSyncError(); + break; + case "weave:service:sync:delayed": + this.onSyncDelay(); + break; + case "weave:service:quota:remaining": + this.onQuotaNotice(); + break; + case "weave:service:setup-complete": + this.onLoginFinish(); + break; + case "weave:service:login:start": + this.onActivityStart(); + break; + case "weave:service:login:finish": + this.onLoginFinish(); + break; + case "weave:ui:login:error": + this.onLoginError(); + break; + case "weave:service:logout:finish": + this.onLogout(); + break; + case "weave:service:start-over": + this.onStartOver(); + break; + case "weave:service:ready": + this.initUI(); + break; + case "weave:notification:added": + this.initNotifications(); + break; + case "weave:ui:clear-error": + this.clearError(); + break; + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]) +}; + +XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() { + //XXXzpao these strings should probably be moved from /services to /browser... (bug 583381) + // but for now just make it work + return Services.strings.createBundle("chrome://weave/locale/services/sync.properties"); +}); diff --git a/comm/suite/components/sync/content/syncUtils.js b/comm/suite/components/sync/content/syncUtils.js new file mode 100644 index 0000000000..fe63654073 --- /dev/null +++ b/comm/suite/components/sync/content/syncUtils.js @@ -0,0 +1,224 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Weave should always exist before before this file gets included. +var gSyncUtils = { + _openLink: function (url) { + if (document.documentElement.id == "change-dialog") + Services.wm.getMostRecentWindow("navigator:browser") + .openUILinkIn(url, "tab"); + else + openUILinkIn(url, "tab"); + }, + + changeName: function changeName(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 openChange(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://communicator/content/sync/syncGenericChange.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")); + }, + + // xxxmpc - fix domain before 1.3 final (bug 583652) + // xxxInvisibleSmiley - we should really have our own pages + // since these refer to Firefox in the page contents + _baseURL: "http://www.mozilla.com/firefox/sync/", + + openFirstClientFirstrun: function () { + let url = this._baseURL + "firstrun.html"; + this._openLink(url); + }, + + openAddedClientFirstrun: function () { + let url = this._baseURL + "secondrun.html"; + this._openLink(url); + }, + + /** + * 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://communicator/content/sync/syncKey.xhtml"); + iframe.setAttribute("type", "content"); + iframe.collapsed = true; + document.documentElement.appendChild(iframe); + iframe.addEventListener("load", function loadListener() { + iframe.removeEventListener("load", loadListener, true); + + // Remove the license block. + let node = iframe.contentDocument.firstChild; + if (node && node.nodeType == Node.COMMENT_NODE) + node.remove(); + + // 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); + }, true); + }, + + /** + * 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._stringBundle.GetStringFromName("save.recoverykey.title"); + let defaultSaveName = this._stringBundle.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(filepicker.file, -1, parseInt("0600", 8), 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 && val1 == Weave.Service.identity.syncKey) + error = "change.password.pwSameAsRecoveryKey"; + 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]; + } +}; + +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyGetter(gSyncUtils, "_stringBundle", function() { + return Services.strings.createBundle("chrome://communicator/locale/sync/syncSetup.properties"); +}); |