diff options
Diffstat (limited to 'application/basilisk/base/content/sync')
-rw-r--r-- | application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml | 46 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/aboutSyncTabs.css | 11 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/aboutSyncTabs.js | 363 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/aboutSyncTabs.xul | 68 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/addDevice.js | 157 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/addDevice.xul | 129 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/customize.css | 28 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/customize.js | 25 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/customize.xul | 62 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/genericChange.js | 226 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/genericChange.xul | 123 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/key.xhtml | 54 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/setup.js | 1057 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/setup.xul | 490 | ||||
-rw-r--r-- | application/basilisk/base/content/sync/utils.js | 231 |
15 files changed, 3070 insertions, 0 deletions
diff --git a/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml b/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml new file mode 100644 index 000000000..e6108209a --- /dev/null +++ b/application/basilisk/base/content/sync/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 pack="start"> + <xul:image class="tabIcon" + xbl:inherits="src=icon"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:label xbl:inherits="value=title,selected" + crop="end" flex="1" class="title"/> + <xul:label xbl:inherits="value=url,selected" + crop="end" flex="1" class="url"/> + </xul:vbox> + </xul:hbox> + </content> + <handlers> + <handler event="dblclick" button="0"> + <![CDATA[ + RemoteTabViewer.openSelected(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="client-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox pack="start" align="center" onfocus="event.target.blur()" onselect="return false;"> + <xul:image/> + <xul:label xbl:inherits="value=clientName" + class="clientName" + crop="center" flex="1"/> + </xul:hbox> + </content> + </binding> +</bindings> diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.css b/application/basilisk/base/content/sync/aboutSyncTabs.css new file mode 100644 index 000000000..5a353175b --- /dev/null +++ b/application/basilisk/base/content/sync/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://browser/content/sync/aboutSyncTabs-bindings.xml#tab-listing); +} + +richlistitem[type="client"] { + -moz-binding: url(chrome://browser/content/sync/aboutSyncTabs-bindings.xml#client-listing); +} diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.js b/application/basilisk/base/content/sync/aboutSyncTabs.js new file mode 100644 index 000000000..6046e7988 --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs.js @@ -0,0 +1,363 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cu = Components.utils; + +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-sync/main.js"); +Cu.import("resource:///modules/PlacesUIUtils.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +#ifdef MOZ_SERVICES_CLOUDSYNC +XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", + "resource://gre/modules/CloudSync.jsm"); +#endif + +var RemoteTabViewer = { + _tabsList: null, + + init() { + Services.obs.addObserver(this, "weave:service:login:finish", false); + Services.obs.addObserver(this, "weave:engine:sync:finish", false); + + Services.obs.addObserver(this, "cloudsync:tabs:update", false); + + this._tabsList = document.getElementById("tabsList"); + + this.buildList(true); + }, + + uninit() { + Services.obs.removeObserver(this, "weave:service:login:finish"); + Services.obs.removeObserver(this, "weave:engine:sync:finish"); + + Services.obs.removeObserver(this, "cloudsync:tabs:update"); + }, + + createItem(attrs) { + let item = document.createElement("richlistitem"); + + // Copy the attributes from the argument into the item. + for (let attr in attrs) { + item.setAttribute(attr, attrs[attr]); + } + + if (attrs["type"] == "tab") { + item.label = attrs.title != "" ? attrs.title : attrs.url; + } + + return item; + }, + + filterTabs(event) { + let val = event.target.value.toLowerCase(); + let numTabs = this._tabsList.getRowCount(); + let clientTabs = 0; + let currentClient = null; + + for (let i = 0; i < numTabs; i++) { + let item = this._tabsList.getItemAtIndex(i); + let hide = false; + if (item.getAttribute("type") == "tab") { + if (!item.getAttribute("url").toLowerCase().includes(val) && + !item.getAttribute("title").toLowerCase().includes(val)) { + hide = true; + } else { + clientTabs++; + } + } else if (item.getAttribute("type") == "client") { + if (currentClient) { + if (clientTabs == 0) { + currentClient.hidden = true; + } + } + currentClient = item; + clientTabs = 0; + } + item.hidden = hide; + } + if (clientTabs == 0) { + currentClient.hidden = true; + } + }, + + openSelected() { + let items = this._tabsList.selectedItems; + let urls = []; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "tab") { + urls.push(items[i].getAttribute("url")); + let index = this._tabsList.getIndexOfItem(items[i]); + this._tabsList.removeItemAt(index); + } + } + if (urls.length) { + getTopWin().gBrowser.loadTabs(urls); + this._tabsList.clearSelection(); + } + }, + + bookmarkSingleTab() { + let item = this._tabsList.selectedItems[0]; + let uri = Weave.Utils.makeURI(item.getAttribute("url")); + let title = item.getAttribute("title"); + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri + , title + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window.top); + }, + + bookmarkSelectedTabs() { + let items = this._tabsList.selectedItems; + let URIs = []; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "tab") { + let uri = Weave.Utils.makeURI(items[i].getAttribute("url")); + if (!uri) { + continue; + } + + URIs.push(uri); + } + } + if (URIs.length) { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "folder" + , URIList: URIs + , hiddenRows: [ "description" ] + }, window.top); + } + }, + + getIcon(iconUri, defaultIcon) { + try { + let iconURI = Weave.Utils.makeURI(iconUri); + return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec; + } catch (ex) { + // Do nothing. + } + + // Just give the provided default icon or the system's default. + return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec; + }, + + _waitingForBuildList: false, + + _buildListRequested: false, + + buildList(forceSync) { + if (this._waitingForBuildList) { + this._buildListRequested = true; + return; + } + + this._waitingForBuildList = true; + this._buildListRequested = false; + + this._clearTabList(); + + if (Weave.Service.isLoggedIn) { + this._refetchTabs(forceSync); + this._generateWeaveTabList(); + } else { + // XXXzpao We should say something about not being logged in & not having data + // or tell the appropriate condition. (bug 583344) + } + + let complete = () => { + this._waitingForBuildList = false; + if (this._buildListRequested) { + CommonUtils.nextTick(this.buildList, this); + } + } + +#ifdef MOZ_SERVICES_CLOUDSYNC + if (CloudSync && CloudSync.ready && + CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs()) { + this._generateCloudSyncTabList() + .then(complete, complete); + } else { + complete(); + } +#else + complete(); +#endif + }, + + _clearTabList() { + let list = this._tabsList; + + // Clear out existing richlistitems. + let count = list.getRowCount(); + if (count > 0) { + for (let i = count - 1; i >= 0; i--) { + list.removeItemAt(i); + } + } + }, + + _generateWeaveTabList() { + let engine = Weave.Service.engineManager.get("tabs"); + let list = this._tabsList; + + let seenURLs = new Set(); + let localURLs = engine.getOpenURLs(); + + for (let [, client] of Object.entries(engine.getAllClients())) { + // Create the client node, but don't add it in-case we don't show any tabs + let appendClient = true; + + client.tabs.forEach(function({title, urlHistory, icon}) { + let url = urlHistory[0]; + if (!url || localURLs.has(url) || seenURLs.has(url)) { + return; + } + seenURLs.add(url); + + if (appendClient) { + let attrs = { + type: "client", + clientName: client.clientName, + class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop" + }; + let clientEnt = this.createItem(attrs); + list.appendChild(clientEnt); + appendClient = false; + clientEnt.disabled = true; + } + let attrs = { + type: "tab", + title: title || url, + url, + icon: this.getIcon(icon), + } + let tab = this.createItem(attrs); + list.appendChild(tab); + }, this); + } + }, + + _generateCloudSyncTabList() { + let updateTabList = function(remoteTabs) { + let list = this._tabsList; + + for (let client of remoteTabs) { + let clientAttrs = { + type: "client", + clientName: client.name, + }; + + let clientEnt = this.createItem(clientAttrs); + list.appendChild(clientEnt); + + for (let tab of client.tabs) { + let tabAttrs = { + type: "tab", + title: tab.title, + url: tab.url, + icon: this.getIcon(tab.icon), + }; + let tabEnt = this.createItem(tabAttrs); + list.appendChild(tabEnt); + } + } + }.bind(this); + + return CloudSync().tabs.getRemoteTabs() + .then(updateTabList, Promise.reject.bind(Promise)); + }, + + adjustContextMenu(event) { + let mode = "all"; + switch (this._tabsList.selectedItems.length) { + case 0: + break; + case 1: + mode = "single" + break; + default: + mode = "multiple"; + break; + } + + let menu = document.getElementById("tabListContext"); + let el = menu.firstChild; + while (el) { + let showFor = el.getAttribute("showFor"); + if (showFor) { + el.hidden = showFor != mode && showFor != "all"; + } + + el = el.nextSibling; + } + }, + + _refetchTabs(force) { + if (!force) { + // Don't bother refetching tabs if we already did so recently + let lastFetch = 0; + try { + lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch"); + } catch (e) { + /* Just use the default value of 0 */ + } + + let now = Math.floor(Date.now() / 1000); + if (now - lastFetch < 30) { + return false; + } + } + + // Ask Sync to just do the tabs engine if it can. + Weave.Service.sync(["tabs"]); + Services.prefs.setIntPref("services.sync.lastTabFetch", + Math.floor(Date.now() / 1000)); + + return true; + }, + + observe(subject, topic, data) { + switch (topic) { + case "weave:service:login:finish": + // A login has finished, which means that a Sync is about to start and + // we will eventually get to the "tabs" engine - but try and force the + // tab engine to sync first by passing |true| for the forceSync param. + this.buildList(true); + break; + case "weave:engine:sync:finish": + if (data == "tabs") { + // The tabs engine just finished, so re-build the list without + // forcing a new sync of the tabs engine. + this.buildList(false); + } + break; + case "cloudsync:tabs:update": + this.buildList(false); + break; + } + }, + + handleClick(event) { + if (event.target.getAttribute("type") != "tab") { + return; + } + + if (event.button == 1) { + let url = event.target.getAttribute("url"); + openUILink(url, event); + let index = this._tabsList.getIndexOfItem(event.target); + this._tabsList.removeItemAt(index); + } + } +} diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.xul b/application/basilisk/base/content/sync/aboutSyncTabs.xul new file mode 100644 index 000000000..a4aa0032f --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs.xul @@ -0,0 +1,68 @@ +<?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://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/aboutSyncTabs.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/sync/aboutSyncTabs.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://browser/locale/aboutSyncTabs.dtd"> + %aboutSyncTabsDTD; +]> + +<window id="tabs-display" + onload="RemoteTabViewer.init()" + onunload="RemoteTabViewer.uninit()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&tabs.otherDevices.label;"> + <script type="application/javascript;version=1.8" src="chrome://browser/content/sync/aboutSyncTabs.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <html:head> + <html:link rel="icon" href="chrome://browser/skin/sync-16.png"/> + </html:head> + + <popupset id="contextmenus"> + <menupopup id="tabListContext"> + <menuitem label="&tabs.context.openTab.label;" + accesskey="&tabs.context.openTab.accesskey;" + oncommand="RemoteTabViewer.openSelected()" + showFor="single"/> + <menuitem label="&tabs.context.bookmarkSingleTab.label;" + accesskey="&tabs.context.bookmarkSingleTab.accesskey;" + oncommand="RemoteTabViewer.bookmarkSingleTab(event)" + showFor="single"/> + <menuitem label="&tabs.context.openMultipleTabs.label;" + accesskey="&tabs.context.openMultipleTabs.accesskey;" + oncommand="RemoteTabViewer.openSelected()" + showFor="multiple"/> + <menuitem label="&tabs.context.bookmarkMultipleTabs.label;" + accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;" + oncommand="RemoteTabViewer.bookmarkSelectedTabs()" + showFor="multiple"/> + <menuseparator/> + <menuitem label="&tabs.context.refreshList.label;" + accesskey="&tabs.context.refreshList.accesskey;" + oncommand="RemoteTabViewer.buildList()" + showFor="all"/> + </menupopup> + </popupset> + <richlistbox context="tabListContext" id="tabsList" seltype="multiple" + align="center" flex="1" + onclick="RemoteTabViewer.handleClick(event)" + oncontextmenu="RemoteTabViewer.adjustContextMenu(event)"> + <hbox id="headers" align="center"> + <label id="tabsListHeading" + value="&tabs.otherDevices.label;"/> + <spacer flex="1"/> + <textbox type="search" + emptytext="&tabs.searchText.label;" + oncommand="RemoteTabViewer.filterTabs(event)"/> + </hbox> + + </richlistbox> +</window> + diff --git a/application/basilisk/base/content/sync/addDevice.js b/application/basilisk/base/content/sync/addDevice.js new file mode 100644 index 000000000..3710dacd4 --- /dev/null +++ b/application/basilisk/base/content/sync/addDevice.js @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const PIN_PART_LENGTH = 4; + +const ADD_DEVICE_PAGE = 0; +const SYNC_KEY_PAGE = 1; +const DEVICE_CONNECTED_PAGE = 2; + +var gSyncAddDevice = { + + init: function init() { + this.pin1.setAttribute("maxlength", PIN_PART_LENGTH); + this.pin2.setAttribute("maxlength", PIN_PART_LENGTH); + this.pin3.setAttribute("maxlength", PIN_PART_LENGTH); + + this.nextFocusEl = {pin1: this.pin2, + pin2: this.pin3, + pin3: this.wizard.getButton("next")}; + + this.throbber = document.getElementById("pairDeviceThrobber"); + this.errorRow = document.getElementById("errorRow"); + + // Kick off a sync. That way the server will have the most recent data from + // this computer and it will show up immediately on the new device. + Weave.Service.scheduler.scheduleNextSync(0); + }, + + onPageShow: function 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) { + if (textbox && textbox.value.length == PIN_PART_LENGTH) + this.nextFocusEl[textbox.id].focus(); + + this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH + && this.pin2.value.length == PIN_PART_LENGTH + && this.pin3.value.length == PIN_PART_LENGTH); + }, + + goToSyncKeyPage: function 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/application/basilisk/base/content/sync/addDevice.xul b/application/basilisk/base/content/sync/addDevice.xul new file mode 100644 index 000000000..83c3b7b3c --- /dev/null +++ b/application/basilisk/base/content/sync/addDevice.xul @@ -0,0 +1,129 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="wizard" + title="&pairDevice.title.label;" + windowtype="Sync:AddDevice" + persist="screenX screenY" + onwizardnext="return gSyncAddDevice.onWizardAdvance();" + onwizardback="return gSyncAddDevice.onWizardBack();" + onwizardcancel="gSyncAddDevice.onWizardCancel();" + onload="gSyncAddDevice.init();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/addDevice.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="errorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncAddDevice.goToSyncKeyPage();"/> + </wizardpage> + + <!-- Need a non-empty label here, otherwise we get a default label on Mac --> + <wizardpage id="syncKeyPage" + label=" " + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &addDevice.dialog.recoveryKey.label; + </description> + <spacer/> + + <groupbox> + <label value="&recoveryKeyEntry.label;" + accesskey="&recoveryKeyEntry.accesskey;" + control="weavePassphrase"/> + <textbox id="weavePassphrase" + readonly="true"/> + </groupbox> + + <groupbox align="center"> + <description>&recoveryKeyBackup.description;</description> + <hbox> + <button id="printSyncKeyButton" + label="&button.syncKeyBackup.print.label;" + accesskey="&button.syncKeyBackup.print.accesskey;" + oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/> + <button id="saveSyncKeyButton" + label="&button.syncKeyBackup.save.label;" + accesskey="&button.syncKeyBackup.save.accesskey;" + oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/> + </hbox> + </groupbox> + </wizardpage> + + <wizardpage id="deviceConnectedPage" + label="&addDevice.dialog.connected.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <vbox align="center"> + <image id="successPageIcon"/> + </vbox> + <separator/> + <description class="normal"> + &addDevice.dialog.successful.label; + </description> + </wizardpage> + +</wizard> diff --git a/application/basilisk/base/content/sync/customize.css b/application/basilisk/base/content/sync/customize.css new file mode 100644 index 000000000..2bb62595d --- /dev/null +++ b/application/basilisk/base/content/sync/customize.css @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +:root { + font-size: 80%; +} + +#sync-customize-pane { + padding-inline-start: 74px; + background: top left url(chrome://browser/skin/sync-128.png) no-repeat; + background-size: 64px; +} + +#sync-customize-title { + margin-inline-start: 0; + padding-bottom: 0.5em; + font-weight: bold; +} + +#sync-customize-subtitle { + font-size: 90%; +} + +checkbox { + margin: 0; + padding: 0.5em 0 0; +} diff --git a/application/basilisk/base/content/sync/customize.js b/application/basilisk/base/content/sync/customize.js new file mode 100644 index 000000000..40ad616d1 --- /dev/null +++ b/application/basilisk/base/content/sync/customize.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +addEventListener("dialogaccept", function() { + let pane = document.getElementById("sync-customize-pane"); + // First determine what the preference for the "global" sync enabled pref + // should be based on the engines selected. + let prefElts = pane.querySelectorAll("preferences > preference"); + let syncEnabled = false; + for (let elt of prefElts) { + if (elt.name.startsWith("services.sync.") && elt.value) { + syncEnabled = true; + break; + } + } + Services.prefs.setBoolPref("services.sync.enabled", syncEnabled); + // and write the individual prefs. + pane.writePreferences(true); + window.arguments[0].accepted = true; +}); diff --git a/application/basilisk/base/content/sync/customize.xul b/application/basilisk/base/content/sync/customize.xul new file mode 100644 index 000000000..f88fe13f4 --- /dev/null +++ b/application/basilisk/base/content/sync/customize.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://browser/content/sync/customize.css" type="text/css"?> + +<!DOCTYPE dialog [ +<!ENTITY % syncCustomizeDTD SYSTEM "chrome://browser/locale/syncCustomize.dtd"> +%syncCustomizeDTD; +]> +<dialog id="sync-customize" + windowtype="Sync:Customize" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&syncCustomize.dialog.title;" + buttonlabelaccept="&syncCustomize.acceptButton.label;" + buttons="accept"> + + <prefpane id="sync-customize-pane"> + <preferences> + <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/> + <preference id="engine.history" name="services.sync.engine.history" type="bool"/> + <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/> + <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/> + <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/> + <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/> + </preferences> + + <label id="sync-customize-title" value="&syncCustomize.title;"/> + <description id="sync-customize-subtitle" + value="&syncCustomize.description;"/> + + <vbox align="start"> + <checkbox label="&engine.tabs.label2;" + accesskey="&engine.tabs.accesskey;" + preference="engine.tabs"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + preference="engine.bookmarks"/> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + preference="engine.passwords"/> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + preference="engine.history"/> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + preference="engine.prefs"/> + </vbox> + + </prefpane> + + <script type="application/javascript" + src="chrome://browser/content/sync/customize.js" /> + +</dialog> diff --git a/application/basilisk/base/content/sync/genericChange.js b/application/basilisk/base/content/sync/genericChange.js new file mode 100644 index 000000000..2e341eca5 --- /dev/null +++ b/application/basilisk/base/content/sync/genericChange.js @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; + +Components.utils.import("resource://services-sync/main.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var Change = { + _dialog: null, + _dialogType: null, + _status: null, + _statusIcon: null, + _firstBox: null, + _secondBox: null, + + get _passphraseBox() { + delete this._passphraseBox; + return this._passphraseBox = document.getElementById("passphraseBox"); + }, + + get _currentPasswordInvalid() { + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + }, + + get _updatingPassphrase() { + return this._dialogType == "UpdatePassphrase"; + }, + + onLoad: function 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._dialog.getButton("finish").disabled = true; + this._dialog.getButton("back").hidden = true; + + this._stringBundle = + Services.strings.createBundle("chrome://browser/locale/syncGenericChange.properties"); + + switch (this._dialogType) { + case "UpdatePassphrase": + case "ResetPassphrase": + document.getElementById("textBox1Row").hidden = true; + document.getElementById("textBox2Row").hidden = true; + document.getElementById("passphraseLabel").value + = this._str("new.recoverykey.label"); + document.getElementById("passphraseSpacer").hidden = false; + + if (this._updatingPassphrase) { + document.getElementById("passphraseHelpBox").hidden = false; + document.title = this._str("new.recoverykey.title"); + introText.textContent = this._str("new.recoverykey.introText"); + this._dialog.getButton("finish").label + = this._str("new.recoverykey.acceptButton"); + } else { + document.getElementById("generatePassphraseButton").hidden = false; + document.getElementById("passphraseBackupButtons").hidden = false; + this._passphraseBox.setAttribute("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.synckey.introText2"); + warningText.textContent = this._str("change.recoverykey.warningText"); + this._dialog.getButton("finish").label + = this._str("change.recoverykey.acceptButton"); + if (this._duringSetup) { + this._dialog.getButton("finish").disabled = false; + } + } + break; + case "ChangePassword": + document.getElementById("passphraseRow").hidden = true; + let box1label = document.getElementById("textBox1Label"); + let box2label = document.getElementById("textBox2Label"); + box1label.value = this._str("new.password.label"); + + if (this._currentPasswordInvalid) { + document.title = this._str("new.password.title"); + introText.textContent = this._str("new.password.introText"); + this._dialog.getButton("finish").label + = this._str("new.password.acceptButton"); + document.getElementById("textBox2Row").hidden = true; + } else { + document.title = this._str("change.password.title"); + box2label.value = this._str("new.password.confirm"); + introText.textContent = this._str("change.password3.introText"); + warningText.textContent = this._str("change.password.warningText"); + this._dialog.getButton("finish").label + = this._str("change.password.acceptButton"); + } + break; + } + document.getElementById("change-page") + .setAttribute("label", document.title); + }, + + _clearStatus: function _clearStatus() { + this._status.value = ""; + 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.value = string; + this._statusIcon.setAttribute("status", state); + + let error = state == "error"; + this._dialog.getButton("cancel").disabled = !error; + this._dialog.getButton("finish").disabled = !error; + document.getElementById("printSyncKeyButton").disabled = !error; + document.getElementById("saveSyncKeyButton").disabled = !error; + + if (state == "success") + window.setTimeout(window.close, 1500); + }, + + onDialogAccept() { + switch (this._dialogType) { + case "UpdatePassphrase": + case "ResetPassphrase": + return this.doChangePassphrase(); + case "ChangePassword": + return this.doChangePassword(); + } + return undefined; + }, + + doGeneratePassphrase() { + let passphrase = Weave.Utils.generatePassphrase(); + this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase); + 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(); + Weave.Service.scheduler.delayedAutoConnect(0); + } else { + this._updateStatus("new.passphrase.status.incorrect", "error"); + } + } else { + this._updateStatus("change.recoverykey.label", "active"); + + if (Weave.Service.changePassphrase(pp)) + this._updateStatus("change.recoverykey.success", "success"); + else + this._updateStatus("change.recoverykey.error", "error"); + } + + return false; + }, + + doChangePassword: function 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(event) { + let valid = false; + let errorString = ""; + + if (this._dialogType == "ChangePassword") { + if (this._currentPasswordInvalid) + [valid, errorString] = gSyncUtils.validatePassword(this._firstBox); + else + [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox); + } else { + 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) { + return this._stringBundle.GetStringFromName(str); + } +}; diff --git a/application/basilisk/base/content/sync/genericChange.xul b/application/basilisk/base/content/sync/genericChange.xul new file mode 100644 index 000000000..db74a1b31 --- /dev/null +++ b/application/basilisk/base/content/sync/genericChange.xul @@ -0,0 +1,123 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="change-dialog" + windowtype="Weave:ChangeSomething" + persist="screenX screenY" + onwizardnext="Change.onLoad()" + onwizardfinish="return Change.onDialogAccept();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/genericChange.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="change-page" + label=""> + + <description id="introText"> + </description> + + <separator class="thin"/> + + <groupbox> + <grid> + <columns> + <column align="right"/> + <column flex="3"/> + <column flex="1"/> + </columns> + <rows> + <row id="textBox1Row" align="center"> + <label id="textBox1Label" control="textBox1"/> + <textbox id="textBox1" type="password" oninput="Change.validate()"/> + <spacer/> + </row> + <row id="textBox2Row" align="center"> + <label id="textBox2Label" control="textBox2"/> + <textbox id="textBox2" type="password" oninput="Change.validate()"/> + <spacer/> + </row> + </rows> + </grid> + + <vbox id="passphraseRow"> + <hbox flex="1"> + <label id="passphraseLabel" control="passphraseBox"/> + <spacer flex="1"/> + <label id="generatePassphraseButton" + hidden="true" + value="&syncGenerateNewKey.label;" + class="text-link" + 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"> + </description> + + <spacer flex="1"/> + </wizardpage> +</wizard> diff --git a/application/basilisk/base/content/sync/key.xhtml b/application/basilisk/base/content/sync/key.xhtml new file mode 100644 index 000000000..1363132e7 --- /dev/null +++ b/application/basilisk/base/content/sync/key.xhtml @@ -0,0 +1,54 @@ +<?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://browser/locale/syncBrand.dtd"> + %syncBrandDTD; + <!ENTITY % syncKeyDTD SYSTEM "chrome://browser/locale/syncKey.dtd"> + %syncKeyDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > + %globalDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&syncKey.page.title;</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="robots" content="noindex"/> + <style type="text/css"> + #synckey { font-size: 150% } + footer { font-size: 70% } + /* Bug 575675: Need to have an a:visited rule in a chrome document. */ + a:visited { color: purple; } + </style> +</head> + +<body dir="&locale.dir;"> +<h1>&syncKey.page.title;</h1> + +<p id="synckey" dir="ltr">SYNCKEY</p> + +<p>&syncKey.page.description2;</p> + +<div id="column1"> + <h2>&syncKey.keepItSecret.heading;</h2> + <p>&syncKey.keepItSecret.description;</p> +</div> + +<div id="column2"> + <h2>&syncKey.keepItSafe.heading;</h2> + <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p> +</div> + +<p>&syncKey.findOutMore1.label;<a href="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/application/basilisk/base/content/sync/setup.js b/application/basilisk/base/content/sync/setup.js new file mode 100644 index 000000000..6215e3a7b --- /dev/null +++ b/application/basilisk/base/content/sync/setup.js @@ -0,0 +1,1057 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +// page consts + +const PAIR_PAGE = 0; +const INTRO_PAGE = 1; +const NEW_ACCOUNT_START_PAGE = 2; +const EXISTING_ACCOUNT_CONNECT_PAGE = 3; +const EXISTING_ACCOUNT_LOGIN_PAGE = 4; +const OPTIONS_PAGE = 5; +const OPTIONS_CONFIRM_PAGE = 6; + +// Broader than we'd like, but after this changed from api-secure.recaptcha.net +// we had no choice. At least we only do this for the duration of setup. +// See discussion in Bugs 508112 and 653307. +const RECAPTCHA_DOMAIN = "https://www.google.com"; + +const PIN_PART_LENGTH = 4; + +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); +Cu.import("resource://gre/modules/PluralForm.jsm"); + + +function setVisibility(element, visible) { + element.style.visibility = visible ? "visible" : "hidden"; +} + +var gSyncSetup = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + captchaBrowser: null, + wizard: null, + _disabledSites: [], + + status: { + password: false, + email: false, + server: false + }, + + get _remoteSites() { + return [Weave.Service.serverURL, RECAPTCHA_DOMAIN]; + }, + + get _usingMainServers() { + if (this._settingUpNew) + return document.getElementById("server").selectedIndex == 0; + return document.getElementById("existingServer").selectedIndex == 0; + }, + + init() { + let obs = [ + ["weave:service:change-passphrase", "onResetPassphrase"], + ["weave:service:login:start", "onLoginStart"], + ["weave:service:login:error", "onLoginEnd"], + ["weave:service:login:finish", "onLoginEnd"]]; + + // Add the observers now and remove them on unload + let self = this; + let addRem = function(add) { + obs.forEach(function([topic, func]) { + // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + // of `this`. Fix in a followup. (bug 583347) + if (add) + Weave.Svc.Obs.add(topic, self[func], self); + else + Weave.Svc.Obs.remove(topic, self[func], self); + }); + }; + addRem(true); + window.addEventListener("unload", () => addRem(false)); + + window.setTimeout(function() { + // Force Service to be loaded so that engines are registered. + // See Bug 670082. + Weave.Service; + }, 0); + + this.captchaBrowser = document.getElementById("captcha"); + + this.wizardType = null; + if (window.arguments && window.arguments[0]) { + this.wizardType = window.arguments[0]; + } + switch (this.wizardType) { + case null: + this.wizard.pageIndex = INTRO_PAGE; + // Fall through! + case "pair": + this.captchaBrowser.addProgressListener(this); + Weave.Svc.Prefs.set("firstSync", "notReady"); + break; + case "reset": + this._resettingSync = true; + this.wizard.pageIndex = OPTIONS_PAGE; + break; + } + + this.wizard.getButton("extra1").label = + this._stringBundle.GetStringFromName("button.syncOptions.label"); + + // Remember these values because the options pages change them temporarily. + this._nextButtonLabel = this.wizard.getButton("next").label; + this._nextButtonAccesskey = this.wizard.getButton("next") + .getAttribute("accesskey"); + this._backButtonLabel = this.wizard.getButton("back").label; + this._backButtonAccesskey = this.wizard.getButton("back") + .getAttribute("accesskey"); + }, + + startNewAccountSetup() { + if (!Weave.Utils.ensureMPUnlocked()) + return; + this._settingUpNew = true; + this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE; + }, + + useExistingAccount() { + if (!Weave.Utils.ensureMPUnlocked()) + return; + this._settingUpNew = false; + if (this.wizardType == "pair") { + // We're already pairing, so there's no point in pairing again. + // Go straight to the manual login page. + this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; + } else { + this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE; + } + }, + + resetPassphrase: function 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].indexOf(Weave.Status.login) == -1) { + return; + } + + // Hide any errors about the passphrase, we know it's not right. + let feedback = document.getElementById("existingPassphraseFeedbackRow"); + feedback.hidden = true; + let el = document.getElementById("existingPassphrase"); + el.value = Weave.Utils.hyphenatePassphrase(passphrase); + + // changePassphrase() will sync, make sure we set the "firstSync" pref + // according to the user's pref. + Weave.Svc.Prefs.reset("firstSync"); + this.setupInitialSync(); + gSyncUtils.resetPassphrase(true); + }, + + onResetPassphrase() { + document.getElementById("existingPassphrase").value = + Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey); + this.checkFields(); + this.wizard.advance(); + }, + + onLoginStart() { + this.toggleLoginFeedback(false); + }, + + onLoginEnd() { + this.toggleLoginFeedback(true); + }, + + sendCredentialsAfterSync() { + let send = function() { + Services.obs.removeObserver("weave:service:sync:finish", send); + Services.obs.removeObserver("weave:service:sync:error", send); + let credentials = {account: Weave.Service.identity.account, + password: Weave.Service.identity.basicPassword, + synckey: Weave.Service.identity.syncKey, + serverURL: Weave.Service.serverURL}; + this._jpakeclient.sendAndComplete(credentials); + }.bind(this); + Services.obs.addObserver("weave:service:sync:finish", send, false); + Services.obs.addObserver("weave:service:sync:error", send, false); + }, + + toggleLoginFeedback(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() { + let action = document.getElementById("mergeChoiceRadio").selectedItem.id; + switch (action) { + case "resetClient": + // if we're not resetting sync, we don't need to explicitly + // call resetClient + if (!this._resettingSync) + return; + // otherwise, fall through + case "wipeClient": + case "wipeRemote": + Weave.Svc.Prefs.set("firstSync", action); + break; + } + }, + + // fun with validation! + checkFields() { + this.wizard.canAdvance = this.readyToAdvance(); + }, + + readyToAdvance() { + switch (this.wizard.pageIndex) { + case INTRO_PAGE: + return false; + case NEW_ACCOUNT_START_PAGE: + for (let i in this.status) { + if (!this.status[i]) + return false; + } + if (this._usingMainServers) + return document.getElementById("tos").checked; + + return true; + case EXISTING_ACCOUNT_LOGIN_PAGE: + let hasUser = document.getElementById("existingAccountName").value != ""; + let hasPass = document.getElementById("existingPassword").value != ""; + let hasKey = document.getElementById("existingPassphrase").value != ""; + + if (hasUser && hasPass && hasKey) { + if (this._usingMainServers) + return true; + + if (this._validateServer(document.getElementById("existingServer"))) { + return true; + } + } + return false; + } + // Default, e.g. wizard's special page -1 etc. + return true; + }, + + onPINInput: function onPINInput(textbox) { + if (textbox && textbox.value.length == PIN_PART_LENGTH) { + this.nextFocusEl[textbox.id].focus(); + } + this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH && + this.pin2.value.length == PIN_PART_LENGTH && + this.pin3.value.length == PIN_PART_LENGTH); + }, + + onEmailInput() { + // 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() { + 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() { + let password = document.getElementById("weavePassword"); + let pwconfirm = document.getElementById("weavePasswordConfirm"); + let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm); + + let feedback = document.getElementById("passwordFeedbackRow"); + this._setFeedback(feedback, valid, errorString); + + this.status.password = valid; + this.checkFields(); + }, + + onPageShow() { + switch (this.wizard.pageIndex) { + case PAIR_PAGE: + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("extra1").hidden = true; + this.onPINInput(); + this.pin1.focus(); + break; + case INTRO_PAGE: + // We may not need the captcha in the Existing Account branch of the + // wizard. However, we want to preload it to avoid any flickering while + // the Create Account page is shown. + this.loadCaptcha(); + this.wizard.getButton("next").hidden = true; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("extra1").hidden = true; + this.checkFields(); + break; + case NEW_ACCOUNT_START_PAGE: + this.wizard.getButton("extra1").hidden = false; + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.onServerCommand(); + this.wizard.canRewind = true; + this.checkFields(); + break; + case EXISTING_ACCOUNT_CONNECT_PAGE: + Weave.Svc.Prefs.set("firstSync", "existingAccount"); + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.canAdvance = false; + this.wizard.canRewind = true; + this.startEasySetup(); + break; + case EXISTING_ACCOUNT_LOGIN_PAGE: + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.canRewind = true; + this.checkFields(); + break; + case OPTIONS_PAGE: + this.wizard.canRewind = false; + this.wizard.canAdvance = true; + if (!this._resettingSync) { + this.wizard.getButton("next").label = + this._stringBundle.GetStringFromName("button.syncOptionsDone.label"); + this.wizard.getButton("next").removeAttribute("accesskey"); + } + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("cancel").hidden = !this._resettingSync; + this.wizard.getButton("extra1").hidden = true; + document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; + document.getElementById("syncOptions").collapsed = this._resettingSync; + document.getElementById("mergeOptions").collapsed = this._settingUpNew; + break; + case OPTIONS_CONFIRM_PAGE: + this.wizard.canRewind = true; + this.wizard.canAdvance = true; + this.wizard.getButton("back").label = + this._stringBundle.GetStringFromName("button.syncOptionsCancel.label"); + this.wizard.getButton("back").removeAttribute("accesskey"); + this.wizard.getButton("back").hidden = this._resettingSync; + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("finish").hidden = true; + break; + } + }, + + onWizardAdvance() { + // Check pageIndex so we don't prompt before the Sync setup wizard appears. + // This is a fallback in case the Master Password gets locked mid-wizard. + if ((this.wizard.pageIndex >= 0) && + !Weave.Utils.ensureMPUnlocked()) { + return false; + } + + switch (this.wizard.pageIndex) { + case PAIR_PAGE: + this.startPairing(); + return false; + case NEW_ACCOUNT_START_PAGE: + // If the user selects Next (e.g. by hitting enter) when we haven't + // executed the delayed checks yet, execute them immediately. + if (this._checkAccountTimer) { + this.checkAccount(); + } + if (this._checkServerTimer) { + this.checkServer(); + } + if (!this.wizard.canAdvance) { + return false; + } + + let doc = this.captchaBrowser.contentDocument; + let getField = function getField(field) { + let node = doc.getElementById("recaptcha_" + field + "_field"); + return node && node.value; + }; + + // Display throbber + let feedback = document.getElementById("captchaFeedback"); + let image = feedback.firstChild; + let label = image.nextSibling; + image.setAttribute("status", "active"); + label.value = this._stringBundle.GetStringFromName("verifying.label"); + setVisibility(feedback, true); + + let password = document.getElementById("weavePassword").value; + let email = Weave.Utils.normalizeAccount( + document.getElementById("weaveEmail").value); + let challenge = getField("challenge"); + let response = getField("response"); + + let error = Weave.Service.createAccount(email, password, + challenge, response); + + if (error == null) { + Weave.Service.identity.account = email; + Weave.Service.identity.basicPassword = password; + Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase(); + this._handleNoScript(false); + Weave.Svc.Prefs.set("firstSync", "newAccount"); + this.wizardFinish(); + return false; + } + + image.setAttribute("status", "error"); + label.value = Weave.Utils.getErrorString(error); + return false; + case EXISTING_ACCOUNT_LOGIN_PAGE: + Weave.Service.identity.account = Weave.Utils.normalizeAccount( + document.getElementById("existingAccountName").value); + Weave.Service.identity.basicPassword = + document.getElementById("existingPassword").value; + let pp = document.getElementById("existingPassphrase").value; + Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp); + if (Weave.Service.login()) { + this.wizardFinish(); + } + return false; + case OPTIONS_PAGE: + let desc = document.getElementById("mergeChoiceRadio").selectedIndex; + // No confirmation needed on new account setup or merge option + // with existing account. + if (this._settingUpNew || (!this._resettingSync && desc == 0)) + return this.returnFromOptions(); + return this._handleChoice(); + case OPTIONS_CONFIRM_PAGE: + if (this._resettingSync) { + this.wizardFinish(); + return false; + } + return this.returnFromOptions(); + } + return true; + }, + + onWizardBack() { + switch (this.wizard.pageIndex) { + case NEW_ACCOUNT_START_PAGE: + this.wizard.pageIndex = INTRO_PAGE; + return false; + case EXISTING_ACCOUNT_CONNECT_PAGE: + this.abortEasySetup(); + this.wizard.pageIndex = INTRO_PAGE; + return false; + case EXISTING_ACCOUNT_LOGIN_PAGE: + // If we were already pairing on entry, we went straight to the manual + // login page. If subsequently we go back, return to the page that lets + // us choose whether we already have an account. + if (this.wizardType == "pair") { + this.wizard.pageIndex = INTRO_PAGE; + return false; + } + return true; + case OPTIONS_CONFIRM_PAGE: + // Backing up from the confirmation page = resetting first sync to merge. + document.getElementById("mergeChoiceRadio").selectedIndex = 0; + return this.returnFromOptions(); + } + return true; + }, + + wizardFinish() { + this.setupInitialSync(); + + if (this.wizardType == "pair") { + this.completePairing(); + } + + if (!this._resettingSync) { + function isChecked(element) { + return document.getElementById(element).hasAttribute("checked"); + } + + let prefs = ["engine.bookmarks", "engine.passwords", "engine.history", + "engine.tabs", "engine.prefs", "engine.addons"]; + for (let i = 0;i < prefs.length;i++) { + Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i])); + } + 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"); + } + Weave.Utils.nextTick(Weave.Service.sync, Weave.Service); + window.close(); + }, + + onWizardCancel() { + if (this._resettingSync) + return; + + this.abortEasySetup(); + this._handleNoScript(false); + Weave.Service.startOver(); + }, + + onSyncOptions() { + this._beforeOptionsPage = this.wizard.pageIndex; + this.wizard.pageIndex = OPTIONS_PAGE; + }, + + returnFromOptions() { + this.wizard.getButton("next").label = this._nextButtonLabel; + this.wizard.getButton("next").setAttribute("accesskey", + this._nextButtonAccesskey); + this.wizard.getButton("back").label = this._backButtonLabel; + this.wizard.getButton("back").setAttribute("accesskey", + this._backButtonAccesskey); + this.wizard.getButton("cancel").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.pageIndex = this._beforeOptionsPage; + return false; + }, + + startPairing: function startPairing() { + this.pairDeviceErrorRow.hidden = true; + // When onAbort is called, Weave may already be gone. + const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; + + let self = this; + let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({ + onPaired: function onPaired() { + self.wizard.pageIndex = INTRO_PAGE; + }, + onComplete: function onComplete() { + // This method will never be called since SendCredentialsController + // will take over after the wizard completes. + }, + onAbort: function onAbort(error) { + delete self._jpakeclient; + + // Aborted by user, ignore. The window is almost certainly going to close + // or is already closed. + if (error == JPAKE_ERROR_USERABORT) { + return; + } + + self.pairDeviceErrorRow.hidden = false; + self.pairDeviceThrobber.hidden = true; + self.pin1.value = self.pin2.value = self.pin3.value = ""; + self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false; + if (self.wizard.pageIndex == PAIR_PAGE) { + self.pin1.focus(); + } + } + }); + this.pairDeviceThrobber.hidden = false; + this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true; + this.wizard.canAdvance = false; + + let pin = this.pin1.value + this.pin2.value + this.pin3.value; + let expectDelay = true; + jpakeclient.pairWithPIN(pin, expectDelay); + }, + + completePairing: function completePairing() { + if (!this._jpakeclient) { + // The channel was aborted while we were setting up the account + // locally. XXX TODO should we do anything here, e.g. tell + // the user on the last wizard page that it's ok, they just + // have to pair again? + return; + } + let controller = new Weave.SendCredentialsController(this._jpakeclient, + Weave.Service); + this._jpakeclient.controller = controller; + }, + + startEasySetup() { + // 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() {}, + + 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; + gSyncSetup.wizardFinish(); + }, + + 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() { + document.getElementById("easySetupPIN1").value = ""; + document.getElementById("easySetupPIN2").value = ""; + document.getElementById("easySetupPIN3").value = ""; + if (!this._jpakeclient) + return; + + this._jpakeclient.abort(); + delete this._jpakeclient; + }, + + manualSetup() { + 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(addExceptions) { + // if NoScript isn't installed, or is disabled, bail out. + let ns = Cc["@maone.net/noscript-service;1"]; + if (ns == null) + return; + + ns = ns.getService().wrappedJSObject; + if (addExceptions) { + this._remoteSites.forEach(function(site) { + site = ns.getSite(site); + if (!ns.isJSEnabled(site)) { + this._disabledSites.push(site); // save status + ns.setJSEnabled(site, true); // allow site + } + }, this); + } else { + this._disabledSites.forEach(function(site) { + ns.setJSEnabled(site, false); + }); + this._disabledSites = []; + } + }, + + onExistingServerCommand() { + let control = document.getElementById("existingServer"); + if (control.selectedIndex == 0) { + control.removeAttribute("editable"); + Weave.Svc.Prefs.reset("serverURL"); + } else { + control.setAttribute("editable", "true"); + // Force a style flush to ensure that the binding is attached. + control.clientTop; + control.value = ""; + control.inputField.focus(); + } + document.getElementById("existingServerFeedbackRow").hidden = true; + this.checkFields(); + }, + + onExistingServerInput() { + // 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() { + setVisibility(document.getElementById("TOSRow"), this._usingMainServers); + let control = document.getElementById("server"); + if (!this._usingMainServers) { + control.setAttribute("editable", "true"); + // Force a style flush to ensure that the binding is attached. + control.clientTop; + control.value = ""; + control.inputField.focus(); + // checkServer() will call checkAccount() and checkFields(). + this.checkServer(); + return; + } + control.removeAttribute("editable"); + Weave.Svc.Prefs.reset("serverURL"); + if (this._settingUpNew) { + this.loadCaptcha(); + } + this.checkAccount(); + this.status.server = true; + document.getElementById("serverFeedbackRow").hidden = true; + this.checkFields(); + }, + + onServerInput() { + // 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() { + delete this._checkServerTimer; + let el = document.getElementById("server"); + let valid = false; + let feedback = document.getElementById("serverFeedbackRow"); + if (el.value) { + valid = this._validateServer(el); + let str = valid ? "" : "serverInvalid.label"; + this._setFeedbackMessage(feedback, valid, str); + } else + this._setFeedbackMessage(feedback, true); + + // Recheck account against the new server. + if (valid) + this.checkAccount(); + + this.status.server = valid; + this.checkFields(); + }, + + _validateServer(element) { + let valid = false; + let val = element.value; + if (!val) + return false; + + let uri = Weave.Utils.makeURI(val); + + if (!uri) + uri = Weave.Utils.makeURI("https://" + val); + + if (uri && this._settingUpNew) { + function isValid(validUri) { + Weave.Service.serverURL = validUri.spec; + let check = Weave.Service.checkAccount("a"); + return (check == "available" || check == "notAvailable"); + } + + if (uri.schemeIs("http")) { + uri.scheme = "https"; + if (isValid(uri)) + valid = true; + else + // setting the scheme back to http + uri.scheme = "http"; + } + if (!valid) + valid = isValid(uri); + + if (valid) { + this.loadCaptcha(); + } + } else if (uri) { + valid = true; + Weave.Service.serverURL = uri.spec; + } + + if (valid) + element.value = Weave.Service.serverURL; + else + Weave.Svc.Prefs.reset("serverURL"); + + return valid; + }, + + _handleChoice() { + let desc = document.getElementById("mergeChoiceRadio").selectedIndex; + document.getElementById("chosenActionDeck").selectedIndex = desc; + switch (desc) { + case 1: + if (this._case1Setup) + break; + + let places_db = PlacesUtils.history + .QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + if (Weave.Service.engineManager.get("history").enabled) { + let daysOfHistory = 0; + let stm = places_db.createStatement( + "SELECT ROUND(( " + + "strftime('%s','now','localtime','utc') - " + + "( " + + "SELECT visit_date FROM moz_historyvisits " + + "ORDER BY visit_date ASC LIMIT 1 " + + ")/1000000 " + + ")/86400) AS daysOfHistory "); + + if (stm.step()) + daysOfHistory = stm.getInt32(0); + // Support %S for historical reasons (see bug 600141) + document.getElementById("historyCount").value = + PluralForm.get(daysOfHistory, + this._stringBundle.GetStringFromName("historyDaysCount.label")) + .replace("%S", daysOfHistory) + .replace("#1", daysOfHistory); + } else { + document.getElementById("historyCount").hidden = true; + } + + if (Weave.Service.engineManager.get("bookmarks").enabled) { + let bookmarks = 0; + let stm = places_db.createStatement( + "SELECT count(*) AS bookmarks " + + "FROM moz_bookmarks b " + + "LEFT JOIN moz_bookmarks t ON " + + "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag"); + stm.params.tag = PlacesUtils.tagsFolderId; + if (stm.executeStep()) + bookmarks = stm.row.bookmarks; + // Support %S for historical reasons (see bug 600141) + document.getElementById("bookmarkCount").value = + PluralForm.get(bookmarks, + this._stringBundle.GetStringFromName("bookmarksCount.label")) + .replace("%S", bookmarks) + .replace("#1", bookmarks); + } else { + document.getElementById("bookmarkCount").hidden = true; + } + + if (Weave.Service.engineManager.get("passwords").enabled) { + let logins = Services.logins.getAllLogins({}); + // Support %S for historical reasons (see bug 600141) + document.getElementById("passwordCount").value = + PluralForm.get(logins.length, + this._stringBundle.GetStringFromName("passwordsCount.label")) + .replace("%S", logins.length) + .replace("#1", logins.length); + } else { + document.getElementById("passwordCount").hidden = true; + } + + if (!Weave.Service.engineManager.get("prefs").enabled) { + document.getElementById("prefsWipe").hidden = true; + } + + let addonsEngine = Weave.Service.engineManager.get("addons"); + if (addonsEngine.enabled) { + let ids = addonsEngine._store.getAllIDs(); + let blessedcount = 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; + } + + 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(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(element, success, string) { + let str = ""; + if (string) { + try { + str = this._stringBundle.GetStringFromName(string); + } catch (e) {} + + if (!str) + str = Weave.Utils.getErrorString(string); + } + this._setFeedback(element, success, str); + }, + + loadCaptcha: function loadCaptcha() { + let captchaURI = Weave.Service.miscAPI + "captcha_html"; + // First check for NoScript and whitelist the right sites. + this._handleNoScript(true); + if (this.captchaBrowser.currentURI.spec != captchaURI) { + this.captchaBrowser.loadURI(captchaURI); + } + }, + + onStateChange(webProgress, request, stateFlags, status) { + // We're only looking for the end of the frame load + if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0) + return; + if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0) + return; + if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0) + return; + + // If we didn't find a captcha, assume it's not needed and don't show it. + let responseStatus = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; + setVisibility(this.captchaBrowser, responseStatus != 404); + // XXX TODO we should really log any responseStatus other than 200 + }, + onProgressChange() {}, + onStatusChange() {}, + onSecurityChange() {}, + onLocationChange() {} +}; + +// Define lazy getters for various XUL elements. +// +// onWizardAdvance() and onPageShow() are run before init(), so we'll even +// define things that will almost certainly be used (like 'wizard') as a lazy +// getter here. +["wizard", + "pin1", + "pin2", + "pin3", + "pairDeviceErrorRow", + "pairDeviceThrobber"].forEach(function(id) { + XPCOMUtils.defineLazyGetter(gSyncSetup, id, function() { + return document.getElementById(id); + }); +}); +XPCOMUtils.defineLazyGetter(gSyncSetup, "nextFocusEl", function() { + return {pin1: this.pin2, + pin2: this.pin3, + pin3: this.wizard.getButton("next")}; +}); +XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() { + return Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); +}); diff --git a/application/basilisk/base/content/sync/setup.xul b/application/basilisk/base/content/sync/setup.xul new file mode 100644 index 000000000..e4f53f6c9 --- /dev/null +++ b/application/basilisk/base/content/sync/setup.xul @@ -0,0 +1,490 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard id="wizard" + title="&accountSetupTitle.label;" + windowtype="Weave:AccountSetup" + persist="screenX screenY" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onwizardnext="return gSyncSetup.onWizardAdvance()" + onwizardback="return gSyncSetup.onWizardBack()" + onwizardcancel="gSyncSetup.onWizardCancel()" + onload="gSyncSetup.init()"> + + <script type="application/javascript" + src="chrome://browser/content/sync/setup.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncSetup.onPageShow()"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="pairDeviceErrorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + </wizardpage> + + <wizardpage id="pickSetupType" + label="&syncBrand.fullName.label;" + onpageshow="gSyncSetup.onPageShow()"> + <vbox align="center" flex="1"> + <description style="padding: 0 7em;"> + &setup.pickSetupType.description2; + </description> + <spacer flex="3"/> + <button id="newAccount" + class="accountChoiceButton" + label="&button.createNewAccount.label;" + oncommand="gSyncSetup.startNewAccountSetup()" + align="center"/> + <spacer flex="1"/> + </vbox> + <separator class="groove"/> + <vbox align="center" flex="1"> + <spacer flex="1"/> + <button id="existingAccount" + class="accountChoiceButton" + label="&button.haveAccount.label;" + oncommand="gSyncSetup.useExistingAccount()"/> + <spacer flex="3"/> + </vbox> + </wizardpage> + + <wizardpage label="&setup.newAccountDetailsPage.title.label;" + id="newAccountStart" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow();"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="emailRow" align="center"> + <label value="&setup.emailAddress.label;" + accesskey="&setup.emailAddress.accesskey;" + control="weaveEmail"/> + <textbox id="weaveEmail" + oninput="gSyncSetup.onEmailInput()"/> + </row> + <row id="emailFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row id="passwordRow" align="center"> + <label value="&setup.choosePassword.label;" + accesskey="&setup.choosePassword.accesskey;" + control="weavePassword"/> + <textbox id="weavePassword" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="confirmRow" align="center"> + <label value="&setup.confirmPassword.label;" + accesskey="&setup.confirmPassword.accesskey;" + control="weavePasswordConfirm"/> + <textbox id="weavePasswordConfirm" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="passwordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <label control="server" + value="&server.label;"/> + <menulist id="server" + oncommand="gSyncSetup.onServerCommand()" + oninput="gSyncSetup.onServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="serverFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row id="TOSRow" align="center"> + <spacer/> + <hbox align="center"> + <checkbox id="tos" + accesskey="&setup.tosAgree1.accesskey;" + oncommand="this.focus(); gSyncSetup.checkFields();"/> + <description id="tosDesc" + flex="1" + onclick="document.getElementById('tos').focus(); + document.getElementById('tos').click()"> + &setup.tosAgree1.label; + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openToS();"> + &setup.tosLink.label; + </label> + &setup.tosAgree2.label; + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"> + &setup.ppLink.label; + </label> + &setup.tosAgree3.label; + </description> + </hbox> + </row> + </rows> + </grid> + <spacer flex="1"/> + <vbox flex="1" align="center"> + <browser height="150" + width="500" + id="captcha" + type="content" + disablehistory="true"/> + <spacer flex="1"/> + <hbox id="captchaFeedback"> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="addDevice" + label="&pairDevice.title.label;" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow()"> + <description> + &pairDevice.setup.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/easy-setup"/> + </description> + <label value="&addDevice.setup.enterCode.label;" + control="easySetupPIN1"/> + <spacer flex="1"/> + <vbox align="center" flex="1"> + <textbox id="easySetupPIN1" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN2" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN3" + class="pin" + value="" + readonly="true" + /> + </vbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncSetup.manualSetup();"/> + </wizardpage> + + <wizardpage id="existingAccount" + label="&setup.signInPage.title.label;" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow()"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="existingAccountRow" align="center"> + <label id="existingAccountLabel" + value="&signIn.account2.label;" + accesskey="&signIn.account2.accesskey;" + control="existingAccount"/> + <textbox id="existingAccountName" + oninput="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordRow" align="center"> + <label id="existingPasswordLabel" + value="&signIn.password.label;" + accesskey="&signIn.password.accesskey;" + control="existingPassword"/> + <textbox id="existingPassword" + type="password" + onkeyup="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <spacer/> + <label class="text-link" + value="&resetPassword.label;" + onclick="gSyncUtils.resetPassword(); return false;"/> + </row> + <row align="center"> + <label control="existingServer" + value="&server.label;"/> + <menulist id="existingServer" + oncommand="gSyncSetup.onExistingServerCommand()" + oninput="gSyncSetup.onExistingServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="existingServerFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <vbox> + <label class="status" value=" "/> + </vbox> + </hbox> + </row> + </rows> + </grid> + + <groupbox> + <label id="existingPassphraseLabel" + value="&signIn.recoveryKey.label;" + accesskey="&signIn.recoveryKey.accesskey;" + control="existingPassphrase"/> + <textbox id="existingPassphrase" + oninput="gSyncSetup.checkFields()"/> + <hbox id="login-throbber" hidden="true"> + <image/> + <label value="&verifying.label;"/> + </hbox> + <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true"> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </vbox> + </groupbox> + + <vbox id="passphraseHelpBox"> + <description> + &existingRecoveryKey.description; + <label class="text-link" + href="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 flex="1" style="margin-inline-end: 2px"/> + </columns> + <rows> + <row align="center"> + <label value="&syncDeviceName.label;" + accesskey="&syncDeviceName.accesskey;" + control="syncComputerName"/> + <textbox id="syncComputerName" flex="1" + onchange="gSyncUtils.changeName(this)"/> + </row> + <row> + <label value="&syncMy.label;" /> + <vbox> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + id="engine.addons" + checked="true"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + id="engine.bookmarks" + checked="true"/> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + id="engine.passwords" + checked="true"/> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + id="engine.prefs" + checked="true"/> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + id="engine.history" + checked="true"/> + <checkbox label="&engine.tabs.label2;" + accesskey="&engine.tabs.accesskey;" + id="engine.tabs" + checked="true"/> + </vbox> + </row> + </rows> + </grid> + </groupbox> + + <groupbox id="mergeOptions"> + <radiogroup id="mergeChoiceRadio" pack="start"> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows flex="1"> + <row align="center"> + <radio id="resetClient" + class="mergeChoiceButton" + aria-labelledby="resetClientLabel"/> + <label id="resetClientLabel" control="resetClient"> + <html:strong>&choice2.merge.recommended.label;</html:strong> + &choice2a.merge.main.label; + </label> + </row> + <row align="center"> + <radio id="wipeClient" + class="mergeChoiceButton" + aria-labelledby="wipeClientLabel"/> + <label id="wipeClientLabel" + control="wipeClient"> + &choice2a.client.main.label; + </label> + </row> + <row align="center"> + <radio id="wipeRemote" + class="mergeChoiceButton" + aria-labelledby="wipeRemoteLabel"/> + <label id="wipeRemoteLabel" + control="wipeRemote"> + &choice2a.server.main.label; + </label> + </row> + </rows> + </grid> + </radiogroup> + </groupbox> + </wizardpage> + + <wizardpage id="syncOptionsConfirm" + label="&setup.optionsConfirmPage.title;" + onpageshow="gSyncSetup.onPageShow()"> + <deck id="chosenActionDeck"> + <vbox id="chosenActionMerge" class="confirm"> + <description class="normal"> + &confirm.merge2.label; + </description> + </vbox> + <vbox id="chosenActionWipeClient" class="confirm"> + <description class="normal"> + &confirm.client3.label; + </description> + <separator class="thin"/> + <vbox id="dataList"> + <label class="data indent" id="bookmarkCount"/> + <label class="data indent" id="historyCount"/> + <label class="data indent" id="passwordCount"/> + <label class="data indent" id="addonCount"/> + <label class="data indent" id="prefsWipe" + value="&engine.prefs.label;"/> + </vbox> + <separator class="thin"/> + <description class="normal"> + &confirm.client2.moreinfo.label; + </description> + </vbox> + <vbox id="chosenActionWipeServer" class="confirm"> + <description class="normal"> + &confirm.server2.label; + </description> + <separator class="thin"/> + <vbox id="clientList"> + </vbox> + </vbox> + </deck> + </wizardpage> + <!-- In terms of the wizard flow shown to the user, the 'syncOptionsConfirm' + page above is not the last wizard page. To prevent the wizard binding from + assuming that it is, we're inserting this dummy page here. This also means + that the wizard needs to always be closed manually via wizardFinish(). --> + <wizardpage> + </wizardpage> +</wizard> + diff --git a/application/basilisk/base/content/sync/utils.js b/application/basilisk/base/content/sync/utils.js new file mode 100644 index 000000000..0d608dffc --- /dev/null +++ b/application/basilisk/base/content/sync/utils.js @@ -0,0 +1,231 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Equivalent to 0o600 permissions; used for saved Sync Recovery Key. +// This constant can be replaced when the equivalent values are available to +// chrome JS; see Bug 433295 and Bug 757351. +const PERMISSIONS_RWUSR = 0x180; + +// Weave should always exist before before this file gets included. +var gSyncUtils = { + get bundle() { + delete this.bundle; + return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); + }, + + get fxAccountsEnabled() { + let service = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + return service.fxAccountsEnabled; + }, + + // opens in a new window if we're in a modal prefwindow world, in a new tab otherwise + _openLink(url) { + let thisDocEl = document.documentElement, + openerDocEl = window.opener && window.opener.document.documentElement; + if (thisDocEl.id == "accountSetup" && window.opener && + openerDocEl.id == "BrowserPreferences" && !openerDocEl.instantApply) + openUILinkIn(url, "window"); + else if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply) + openUILinkIn(url, "window"); + else if (document.documentElement.id == "change-dialog") + Services.wm.getMostRecentWindow("navigator:browser") + .openUILinkIn(url, "tab"); + else + openUILinkIn(url, "tab"); + }, + + changeName: function 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://browser/content/sync/genericChange.xul"; + let changeOpt = "centerscreen,chrome,resizable=no"; + Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt, + type, duringSetup); + }, + + changePassword() { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("ChangePassword"); + }, + + resetPassphrase(duringSetup) { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("ResetPassphrase", duringSetup); + }, + + updatePassphrase() { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("UpdatePassphrase"); + }, + + resetPassword() { + this._openLink(Weave.Service.pwResetURL); + }, + + get tosURL() { + let root = this.fxAccountsEnabled ? "fxa." : ""; + return Weave.Svc.Prefs.get(root + "termsURL"); + }, + + openToS() { + this._openLink(this.tosURL); + }, + + get privacyPolicyURL() { + let root = this.fxAccountsEnabled ? "fxa." : ""; + return Weave.Svc.Prefs.get(root + "privacyURL"); + }, + + openPrivacyPolicy() { + this._openLink(this.privacyPolicyURL); + }, + + /** + * 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(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://browser/content/sync/key.xhtml"); + iframe.collapsed = true; + document.documentElement.appendChild(iframe); + iframe.contentWindow.addEventListener("load", function() { + iframe.contentWindow.removeEventListener("load", arguments.callee); + + // 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); + }); + }, + + /** + * Print passphrase backup document. + * + * @param elid : ID of the form element containing the passphrase. + */ + passphrasePrint(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(elid) { + let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title"); + let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename"); + this._preparePPiframe(elid, function(iframe) { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK || + aResult == Ci.nsIFilePicker.returnReplace) { + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(fp.file, -1, PERMISSIONS_RWUSR, 0); + + let serializer = new XMLSerializer(); + let output = serializer.serializeToString(iframe.contentDocument); + output = output.replace(/<!DOCTYPE (.|\n)*?]>/, + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' + + '"DTD/xhtml1-strict.dtd">'); + output = Weave.Utils.encodeUTF8(output); + stream.write(output, output.length); + } + }; + + fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave); + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.defaultString = defaultSaveName; + fp.open(fpCallback); + return false; + }); + }, + + /** + * validatePassword + * + * @param el1 : the first textbox element in the form + * @param el2 : the second textbox element, if omitted it's an update form + * + * returns [valid, errorString] + */ + validatePassword(el1, el2) { + let valid = false; + let val1 = el1.value; + let val2 = el2 ? el2.value : ""; + let error = ""; + + if (!el2) + valid = val1.length >= Weave.MIN_PASS_LENGTH; + else if (val1 && val1 == Weave.Service.identity.username) + error = "change.password.pwSameAsUsername"; + else if (val1 && val1 == Weave.Service.identity.account) + error = "change.password.pwSameAsEmail"; + else if (val1 && val1 == Weave.Service.identity.basicPassword) + error = "change.password.pwSameAsPassword"; + else if (val1 && val2) { + if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH) + valid = true; + else if (val1.length < Weave.MIN_PASS_LENGTH) + error = "change.password.tooShort"; + else if (val1 != val2) + error = "change.password.mismatch"; + } + let errorString = error ? Weave.Utils.getErrorString(error) : ""; + return [valid, errorString]; + } +}; |