summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 07:58:57 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 07:58:57 +0000
commit592d2180f5d3984853bf55f91be87ac5d1dc0c1a (patch)
tree74ebbedcb80b9eca5e9773fb1b558e75f69cad8d /content
parentInitial commit. (diff)
downloaddav4tbsync-592d2180f5d3984853bf55f91be87ac5d1dc0c1a.tar.xz
dav4tbsync-592d2180f5d3984853bf55f91be87ac5d1dc0c1a.zip
Adding upstream version 4.7.upstream/4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'content')
-rw-r--r--content/api/BootstrapLoader/CHANGELOG.md75
-rw-r--r--content/api/BootstrapLoader/README.md1
-rw-r--r--content/api/BootstrapLoader/implementation.js917
-rw-r--r--content/api/BootstrapLoader/schema.json61
-rw-r--r--content/bootstrap.js62
-rw-r--r--content/includes/network.js431
-rw-r--r--content/includes/sync.js462
-rw-r--r--content/includes/tools.js198
-rw-r--r--content/locales.js3
-rw-r--r--content/manager/createAccount.js524
-rw-r--r--content/manager/createAccount.xhtml158
-rw-r--r--content/manager/editAccountOverlay.js46
-rw-r--r--content/manager/editAccountOverlay.xhtml93
-rw-r--r--content/provider.js691
-rw-r--r--content/skin/ab.css82
-rw-r--r--content/skin/arrow.down10.pngbin0 -> 17764 bytes
-rw-r--r--content/skin/arrow.up10.pngbin0 -> 17762 bytes
-rw-r--r--content/skin/dragdrop.pngbin0 -> 18111 bytes
-rw-r--r--content/skin/fruux16.pngbin0 -> 788 bytes
-rw-r--r--content/skin/fruux32.pngbin0 -> 22319 bytes
-rw-r--r--content/skin/fruux48.pngbin0 -> 27455 bytes
-rw-r--r--content/skin/gmx16.pngbin0 -> 416 bytes
-rw-r--r--content/skin/gmx32.pngbin0 -> 896 bytes
-rw-r--r--content/skin/gmx48.pngbin0 -> 1386 bytes
-rw-r--r--content/skin/icloud16.pngbin0 -> 427 bytes
-rw-r--r--content/skin/icloud32.pngbin0 -> 769 bytes
-rw-r--r--content/skin/icloud48.pngbin0 -> 1196 bytes
-rw-r--r--content/skin/ics16.pngbin0 -> 789 bytes
-rw-r--r--content/skin/mbo16.pngbin0 -> 535 bytes
-rw-r--r--content/skin/mbo32.pngbin0 -> 14636 bytes
-rw-r--r--content/skin/mbo48.pngbin0 -> 1172 bytes
-rw-r--r--content/skin/posteo16.pngbin0 -> 461 bytes
-rw-r--r--content/skin/posteo32.pngbin0 -> 742 bytes
-rw-r--r--content/skin/posteo48.pngbin0 -> 922 bytes
-rw-r--r--content/skin/sabredav16.pngbin0 -> 18198 bytes
-rw-r--r--content/skin/sabredav32.pngbin0 -> 19228 bytes
-rw-r--r--content/skin/sabredav48.pngbin0 -> 2825 bytes
-rw-r--r--content/skin/type.car10.pngbin0 -> 18319 bytes
-rw-r--r--content/skin/type.car16.pngbin0 -> 17917 bytes
-rw-r--r--content/skin/type.cell10.pngbin0 -> 18495 bytes
-rw-r--r--content/skin/type.cell16.pngbin0 -> 18517 bytes
-rw-r--r--content/skin/type.fax10.pngbin0 -> 18312 bytes
-rw-r--r--content/skin/type.fax16.pngbin0 -> 17919 bytes
-rw-r--r--content/skin/type.home10.pngbin0 -> 18247 bytes
-rw-r--r--content/skin/type.home16.pngbin0 -> 18283 bytes
-rw-r--r--content/skin/type.nopref.pngbin0 -> 15977 bytes
-rw-r--r--content/skin/type.other10.pngbin0 -> 18325 bytes
-rw-r--r--content/skin/type.other16.pngbin0 -> 18368 bytes
-rw-r--r--content/skin/type.pager10.pngbin0 -> 18450 bytes
-rw-r--r--content/skin/type.pager16.pngbin0 -> 18471 bytes
-rw-r--r--content/skin/type.pref.pngbin0 -> 18261 bytes
-rw-r--r--content/skin/type.video10.pngbin0 -> 18298 bytes
-rw-r--r--content/skin/type.video16.pngbin0 -> 18339 bytes
-rw-r--r--content/skin/type.voice10.pngbin0 -> 18270 bytes
-rw-r--r--content/skin/type.voice16.pngbin0 -> 17864 bytes
-rw-r--r--content/skin/type.work10.pngbin0 -> 18257 bytes
-rw-r--r--content/skin/type.work16.pngbin0 -> 18287 bytes
-rw-r--r--content/skin/web16.pngbin0 -> 564 bytes
-rw-r--r--content/skin/web32.pngbin0 -> 1419 bytes
-rw-r--r--content/skin/web48.pngbin0 -> 2301 bytes
-rw-r--r--content/skin/yahoo16.pngbin0 -> 561 bytes
-rw-r--r--content/skin/yahoo32.pngbin0 -> 1103 bytes
-rw-r--r--content/skin/yahoo48.pngbin0 -> 1640 bytes
63 files changed, 3804 insertions, 0 deletions
diff --git a/content/api/BootstrapLoader/CHANGELOG.md b/content/api/BootstrapLoader/CHANGELOG.md
new file mode 100644
index 0000000..5006ecf
--- /dev/null
+++ b/content/api/BootstrapLoader/CHANGELOG.md
@@ -0,0 +1,75 @@
+Version: 1.21
+-------------
+- Explicitly set hasAddonManagerEventListeners flag to false on uninstall
+
+Version: 1.20
+-------------
+- hard fork BootstrapLoader v1.19 implementation and continue to serve it for
+ Thunderbird 111 and older
+- BootstrapLoader v1.20 has removed a lot of unnecessary code used for backward
+ compatibility
+
+Version: 1.19
+-------------
+- fix race condition which could prevent the AOM tab to be monkey patched correctly
+
+Version: 1.18
+-------------
+- be precise on which revision the wrench symbol should be displayed, instead of
+ the options button
+
+Version: 1.17
+-------------
+- fix "ownerDoc.getElementById() is undefined" bug
+
+Version: 1.16
+-------------
+- fix "tab.browser is undefined" bug
+
+Version 1.15
+------------
+- clear cache only if add-on is uninstalled/updated, not on app shutdown
+
+Version 1.14
+------------
+- fix for TB90 ("view-loaded" event) and TB78.10 (wrench icon for options)
+
+Version 1.13
+------------
+- removed notifyTools and move it into its own NotifyTools API
+
+Version 1.12
+------------
+- add support for notifyExperiment and onNotifyBackground
+
+Version 1.11
+------------
+- add openOptionsDialog()
+
+Version 1.10
+------------
+- fix for 68
+
+Version 1.7
+-----------
+- fix for beta 87
+
+Version 1.6
+-----------
+- add support for options button/menu in add-on manager and fix 68 double menu entry
+
+Version 1.5
+-----------
+- fix for e10s
+
+Version 1.4
+-----------
+- add registerOptionsPage
+
+Version 1.3
+-----------
+- flush cache
+
+Version 1.2
+-----------
+- add support for resource urls
diff --git a/content/api/BootstrapLoader/README.md b/content/api/BootstrapLoader/README.md
new file mode 100644
index 0000000..7e8fe2a
--- /dev/null
+++ b/content/api/BootstrapLoader/README.md
@@ -0,0 +1 @@
+Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-BootstrapLoader-API-to-convert-a-Legacy-Bootstrap-WebExtension-into-a-MailExtension-for-Thunderbird-78).
diff --git a/content/api/BootstrapLoader/implementation.js b/content/api/BootstrapLoader/implementation.js
new file mode 100644
index 0000000..03e6b76
--- /dev/null
+++ b/content/api/BootstrapLoader/implementation.js
@@ -0,0 +1,917 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.21
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+// Get various parts of the WebExtension framework that we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function getThunderbirdVersion() {
+ let parts = Services.appinfo.version.split(".");
+ return {
+ major: parseInt(parts[0]),
+ minor: parseInt(parts[1]),
+ revision: parts.length > 2 ? parseInt(parts[2]) : 0,
+ }
+}
+
+function getMessenger(context) {
+ let apis = ["storage", "runtime", "extension", "i18n"];
+
+ function getStorage() {
+ let localstorage = null;
+ try {
+ localstorage = context.apiCan.findAPIPath("storage");
+ localstorage.local.get = (...args) =>
+ localstorage.local.callMethodInParentProcess("get", args);
+ localstorage.local.set = (...args) =>
+ localstorage.local.callMethodInParentProcess("set", args);
+ localstorage.local.remove = (...args) =>
+ localstorage.local.callMethodInParentProcess("remove", args);
+ localstorage.local.clear = (...args) =>
+ localstorage.local.callMethodInParentProcess("clear", args);
+ } catch (e) {
+ console.info("Storage permission is missing");
+ }
+ return localstorage;
+ }
+
+ let messenger = {};
+ for (let api of apis) {
+ switch (api) {
+ case "storage":
+ XPCOMUtils.defineLazyGetter(messenger, "storage", () =>
+ getStorage()
+ );
+ break;
+
+ default:
+ XPCOMUtils.defineLazyGetter(messenger, api, () =>
+ context.apiCan.findAPIPath(api)
+ );
+ }
+ }
+ return messenger;
+}
+
+var BootstrapLoader_102 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc;
+
+ // 78,86, and 87+ need special handholding. *Yeah*.
+ if (getThunderbirdVersion().major < 86) {
+ let ownerDoc = e.document || e.target.ownerDocument;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else if (getThunderbirdVersion().major < 87) {
+ let ownerDoc = e.document || e.target;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else {
+ doc = e.document || e.target;
+ }
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Add pref entry to 68
+ add68PrefsEntry(event) {
+ let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
+
+ // Get the best size of the icon (16px or bigger)
+ let iconSizes = this.extension.manifest.icons
+ ? Object.keys(this.extension.manifest.icons)
+ : [];
+ iconSizes.sort((a, b) => a - b);
+ let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
+ let icon = bestSize ? this.extension.manifest.icons[bestSize] : "";
+
+ let name = this.extension.manifest.name;
+ let entry = icon
+ ? event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem class="menuitem-iconic" id="${id}" image="${icon}" label="${name}" />`)
+ : event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem id="${id}" label="${name}" />`);
+
+ event.target.appendChild(entry);
+ let noPrefsElem = event.target.querySelector('[disabled="true"]');
+ // using collapse could be undone by core, so we use display none
+ // noPrefsElem.setAttribute("collapsed", "true");
+ noPrefsElem.style.display = "none";
+ event.target.ownerGlobal.document.getElementById(id).addEventListener("command", this);
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ // 68 add-on options menu showing
+ case "popupshowing": {
+ this.add68PrefsEntry(e);
+ }
+ break;
+
+ // 78/88 add-on options menu/button click
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // 68 add-on options menu command
+ case "command": {
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ e.target.ownerGlobal.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ let optionsMenu =
+ (getThunderbirdVersion().major > 78 && getThunderbirdVersion().major < 88) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor < 10) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor == 10 && getThunderbirdVersion().revision < 2);
+ if (optionsMenu) {
+ // Options menu in 78.0-78.10 and 79-87
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (card.addon.isActive && !addonOptionsLegacyEntry) {
+ let addonOptionsEntry = card.querySelector("addon-options panel-list panel-item[action='preferences']");
+ addonOptionsLegacyEntry = card.ownerDocument.createElement("panel-item");
+ addonOptionsLegacyEntry.setAttribute("data-l10n-id", "preferences-addon-button");
+ addonOptionsLegacyEntry.classList.add("extension-options-legacy");
+ addonOptionsEntry.parentNode.insertBefore(
+ addonOptionsLegacyEntry,
+ addonOptionsEntry
+ );
+ card.querySelector(".extension-options-legacy").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsLegacyEntry) {
+ addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Add-on button in 88
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (card.addon.isActive && !addonOptionsButton) {
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("extension-options-button2");
+ addonOptionsButton.style["min-width"] = "auto";
+ addonOptionsButton.style["min-height"] = "auto";
+ addonOptionsButton.style["width"] = "24px";
+ addonOptionsButton.style["height"] = "24px";
+ addonOptionsButton.style["margin"] = "0";
+ addonOptionsButton.style["margin-inline-start"] = "8px";
+ addonOptionsButton.style["-moz-context-properties"] = "fill";
+ addonOptionsButton.style["fill"] = "currentColor";
+ addonOptionsButton.style["background-image"] = "url('chrome://messenger/skin/icons/developer.svg')";
+ addonOptionsButton.style["background-repeat"] = "no-repeat";
+ addonOptionsButton.style["background-position"] = "center center";
+ addonOptionsButton.style["padding"] = "1px";
+ addonOptionsButton.style["display"] = "flex";
+ addonOptionsButton.style["justify-content"] = "flex-end";
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card.querySelector(".extension-options-button2").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+// Removed all extra code for backward compatibility for better maintainability.
+var BootstrapLoader_115 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc = e.document || e.target;
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(
+ this.pathToOptionsPage,
+ "AddonOptions",
+ "chrome,resizable,centerscreen",
+ BL
+ );
+ }
+ break;
+
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ // Add-on button
+ let addonOptionsButton = card.querySelector(
+ ".windowlistener-options-button"
+ );
+ if (card.addon.isActive && !addonOptionsButton) {
+ let origAddonOptionsButton = card.querySelector(".extension-options-button")
+ origAddonOptionsButton.setAttribute("hidden", "true");
+
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("windowlistener-options-button");
+ addonOptionsButton.classList.add("extension-options-button");
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card
+ .querySelector(".windowlistener-options-button")
+ .addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (!this.pathToOptionsPage) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+var BootstrapLoader = getThunderbirdVersion().major < 111
+ ? BootstrapLoader_102
+ : BootstrapLoader_115;
diff --git a/content/api/BootstrapLoader/schema.json b/content/api/BootstrapLoader/schema.json
new file mode 100644
index 0000000..fe48fb6
--- /dev/null
+++ b/content/api/BootstrapLoader/schema.json
@@ -0,0 +1,61 @@
+[
+ {
+ "namespace": "BootstrapLoader",
+ "functions": [
+ {
+ "name": "registerOptionsPage",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
+ }
+ ]
+ },
+ {
+ "name": "openOptionsDialog",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowId",
+ "type": "integer",
+ "description": "Id of the window the dialog should be opened from."
+ }
+ ]
+ },
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "async": true,
+ "parameters": [
+ {
+ "name": "chromeData",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "description": "Array of ChromeData Arrays."
+ }
+ ]
+ },
+ {
+ "name": "registerBootstrapScript",
+ "type": "function",
+ "description": "Register a bootstrap.js style script",
+ "async": true,
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Either the chrome:// path to the script or its relative location from the root of the extension,"
+ }
+ ]
+ }
+ ]
+ }
+] \ No newline at end of file
diff --git a/content/bootstrap.js b/content/bootstrap.js
new file mode 100644
index 0000000..d4e4d7c
--- /dev/null
+++ b/content/bootstrap.js
@@ -0,0 +1,62 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+// no need to create namespace, we are in a sandbox
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+let component = {};
+
+let onInitDoneObserver = {
+ observe: async function (aSubject, aTopic, aData) {
+ let valid = false;
+ try {
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ valid = TbSync.enabled;
+ } catch (e) {
+ // If this fails, TbSync is not loaded yet and we will get the notification later again.
+ }
+
+ //load this provider add-on into TbSync
+ if (valid) {
+ await TbSync.providers.loadProvider(extension, "dav", "chrome://dav4tbsync/content/provider.js");
+ }
+ }
+}
+
+
+function startup(data, reason) {
+ // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ Services.obs.addObserver(onInitDoneObserver, "tbsync.observer.initialized", false);
+
+ // Did we miss the observer?
+ onInitDoneObserver.observe();
+}
+
+function shutdown(data, reason) {
+ // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ // When the application is shutting down we normally don't have to clean up.
+ if (reason == APP_SHUTDOWN) {
+ return;
+ }
+
+
+ Services.obs.removeObserver(onInitDoneObserver, "tbsync.observer.initialized");
+ //unload this provider add-on from TbSync
+ try {
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ TbSync.providers.unloadProvider("dav");
+ } catch (e) {
+ //if this fails, TbSync has been unloaded already and has unloaded this addon as well
+ }
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+}
diff --git a/content/includes/network.js b/content/includes/network.js
new file mode 100644
index 0000000..3f94c30
--- /dev/null
+++ b/content/includes/network.js
@@ -0,0 +1,431 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var { HttpRequest } = ChromeUtils.import("chrome://tbsync/content/HttpRequest.jsm");
+var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm");
+const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm");
+
+var network = {
+
+ getAuthData: function(accountData) {
+ let connection = {
+ get host() {
+ return "TbSync#" + accountData.accountID;
+ },
+
+ get username() {
+ return accountData.getAccountProperty("user");
+ },
+
+ get password() {
+ // try new host first
+ let pw = TbSync.passwordManager.getLoginInfo(this.host, "TbSync/DAV", this.username);
+ if (pw) {
+ return pw;
+ }
+
+ // try old host as fallback
+ let oldHost = accountData.getAccountProperty("calDavHost") ? accountData.getAccountProperty("calDavHost") : accountData.getAccountProperty("cardDavHost");
+ if (oldHost.startsWith("http://")) oldHost = oldHost.substr(7);
+ if (oldHost.startsWith("https://")) oldHost = oldHost.substr(8);
+ pw = TbSync.passwordManager.getLoginInfo(oldHost, "TbSync/DAV", this.username);
+ if (pw) {
+ //migrate
+ this.updateLoginData(this.username, pw);
+ }
+ return pw;
+ },
+
+ updateLoginData: function(newUsername, newPassword) {
+ let oldUsername = this.username;
+ TbSync.passwordManager.updateLoginInfo(this.host, "TbSync/DAV", oldUsername, newUsername, newPassword);
+ // Also update the username of this account.
+ accountData.setAccountProperty("user", newUsername);
+ },
+
+ removeLoginData: function() {
+ TbSync.passwordManager.removeLoginInfos(this.host, "TbSync/DAV");
+ }
+ };
+ return connection;
+ },
+
+ ConnectionData: class {
+ constructor(data) {
+ this._password = "";
+ this._username = "";
+ this._https = "";
+ this._type = "";
+ this._fqdn = "";
+ this._timeout = dav.Base.getConnectionTimeout();
+
+ //for error logging
+ this._eventLogInfo = null;
+
+ //typof syncdata?
+ let folderData = null;
+ let accountData = null;
+
+ if (data instanceof TbSync.SyncData) {
+ folderData = data.currentFolderData;
+ accountData = data.accountData;
+ this._eventLogInfo = data.eventLogInfo;
+ } else if (data instanceof TbSync.FolderData) {
+ folderData = data;
+ accountData = data.accountData;
+ this._eventLogInfo = new TbSync.EventLogInfo(
+ accountData.getAccountProperty("provider"),
+ accountData.getAccountProperty("accountname"),
+ accountData.accountID,
+ folderData.getFolderProperty("foldername"));
+ } else if (data instanceof TbSync.AccountData) {
+ accountData = data;
+ this._eventLogInfo = new TbSync.EventLogInfo(
+ accountData.getAccountProperty("provider"),
+ accountData.getAccountProperty("accountname"),
+ accountData.accountID,
+ "");
+ }
+
+ if (accountData) {
+ let authData = dav.network.getAuthData(accountData);
+ this._password = authData.password;
+ this._username = authData.username;
+
+ this._accountname = accountData.getAccountProperty("accountname");
+ if (folderData) {
+ this._fqdn = folderData.getFolderProperty("fqdn");
+ this._https = folderData.getFolderProperty("https");
+ }
+ this.accountData = accountData;
+ }
+ }
+
+
+ set password(v) {this._password = v;}
+ set username(v) {this._username = v;}
+ set timeout(v) {this._timeout = v;}
+ set https(v) {this._https = v;}
+ set fqdn(v) {this._fqdn = v;}
+ set eventLogInfo(v) {this._eventLogInfo = v;}
+
+ get password() {return this._password;}
+ get username() {return this._username;}
+ get timeout() {return this._timeout;}
+ get https() {return this._https;}
+ get fqdn() {return this._fqdn;}
+ get eventLogInfo() {return this._eventLogInfo;}
+ },
+
+
+ checkForRFC6764Request: async function (path, eventLogInfo) {
+ function checkDefaultSecPort (sec) {
+ return sec ? "443" : "80";
+ }
+
+ if (!this.isRFC6764Request(path)) {
+ return path;
+ }
+
+ let parts = path.toLowerCase().split("6764://");
+ let type = parts[0].endsWith("caldav") ? "caldav" : "carddav";
+
+ // obey preselected security level for DNS lookup
+ // and only use insecure option if specified
+ let scheme = parts[0].startsWith("httpca") ? "http" : "https"; //httpcaldav or httpcarddav = httpca = http
+ let sec = (scheme == "https");
+
+ let hostPath = parts[1];
+ while (hostPath.endsWith("/")) { hostPath = hostPath.slice(0,-1); }
+ let host = hostPath.split("/")[0];
+
+ let result = {};
+
+ //only perform dns lookup, if the provided path does not contain any path information
+ if (host == hostPath) {
+ let request = "_" + type + (sec ? "s" : "") + "._tcp." + host;
+
+ // get host from SRV record
+ let rv = await DNS.srv(request);
+ if (rv && Array.isArray(rv) && rv.length>0 && rv[0].host) {
+ result.secure = sec;
+ result.host = rv[0].host + ((checkDefaultSecPort(sec) == rv[0].port) ? "" : ":" + rv[0].port);
+ TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "SRV record @ " + request + "\n" + JSON.stringify(rv[0]));
+
+ // Now try to get path from TXT
+ rv = await DNS.txt(request);
+ if (rv && Array.isArray(rv) && rv.length>0 && rv[0].data && rv[0].data.toLowerCase().startsWith("path=")) {
+ result.path = rv[0].data.substring(5);
+ TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "TXT record @ " + request + "\n" + JSON.stringify(rv[0]));
+ } else {
+ result.path = "/.well-known/" + type;
+ }
+
+ result.url = "http" + (result.secure ? "s" : "") + "://" + result.host + result.path;
+ return result.url;
+ } else {
+ TbSync.eventlog.add("warning", eventLogInfo, "RFC6764 DNS request failed", "SRV record @ " + request);
+ }
+ }
+
+ // use the provided hostPath and build standard well-known url
+ return scheme + "://" + hostPath + "/.well-known/" + type;
+ },
+
+ startsWithScheme: function (url) {
+ return (url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://") || this.isRFC6764Request(url));
+ },
+
+ isRFC6764Request: function (url) {
+ let parts = url.split("6764://");
+ return (parts.length == 2 && parts[0].endsWith("dav"));
+ },
+
+ sendRequest: async function (requestData, path, method, connectionData, headers = {}, options = {}) {
+ let url = await this.checkForRFC6764Request(path, connectionData.eventLogInfo);
+ let enforcedPermanentlyRedirectedUrl = (url != path) ? url : null;
+
+ // path could be absolute or relative, we may need to rebuild the full url.
+ if (url.startsWith("http://") || url.startsWith("https://")) {
+ // extract segments from url
+ let uri = Services.io.newURI(url);
+ connectionData.https = (uri.scheme == "https");
+ connectionData.fqdn = uri.hostPort;
+ } else {
+ url = "http" + (connectionData.https ? "s" : "") + "://" + connectionData.fqdn + url;
+ }
+
+ let currentSyncState = connectionData.accountData ? connectionData.accountData.syncData.getSyncState().state : "";
+ let accountID = connectionData.accountData ? connectionData.accountData.accountID : "";
+
+ // Loop: Prompt user for password and retry
+ const MAX_RETRIES = options.hasOwnProperty("passwordRetries") ? options.passwordRetries+1 : 5;
+ for (let i=1; i <= MAX_RETRIES; i++) {
+ TbSync.dump("URL Request #" + i, url);
+
+ connectionData.url = url;
+
+ // Restore original syncstate before open the connection
+ if (connectionData.accountData && currentSyncState != connectionData.accountData.syncData.getSyncState().state) {
+ connectionData.accountData.syncData.setSyncState(currentSyncState);
+ }
+
+ let r = await dav.network.promisifiedHttpRequest(requestData, method, connectionData, headers, options);
+ if (r && enforcedPermanentlyRedirectedUrl && !r.permanentlyRedirectedUrl) {
+ r.permanentlyRedirectedUrl = enforcedPermanentlyRedirectedUrl;
+ }
+
+ if (r && r.passwordPrompt && r.passwordPrompt === true) {
+ if (i == MAX_RETRIES) {
+ // If this is the final retry, abort with error.
+ throw r.passwordError;
+ } else {
+ let credentials = null;
+ let retry = false;
+
+ // Prompt, if connection belongs to an account (and not from the create wizard)
+ if (connectionData.accountData) {
+ let promptData = {
+ windowID: "auth:" + connectionData.accountData.accountID,
+ accountname: connectionData.accountData.getAccountProperty("accountname"),
+ usernameLocked: connectionData.accountData.isConnected(),
+ username: connectionData.username,
+ }
+ connectionData.accountData.syncData.setSyncState("passwordprompt");
+
+ credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, dav.openWindows);
+ if (credentials) {
+ // update login data
+ dav.network.getAuthData(connectionData.accountData).updateLoginData(credentials.username, credentials.password);
+ // update connection data
+ connectionData.username = credentials.username;
+ connectionData.password = credentials.password;
+ retry = true;
+ }
+ }
+
+ if (!retry) {
+ throw r.passwordError;
+ }
+
+ }
+ } else {
+ return r;
+ }
+ }
+ },
+
+ // Promisified implementation of TbSync's HttpRequest (with XHR interface)
+ promisifiedHttpRequest: function (requestData, method, connectionData, headers, options) {
+ let responseData = "";
+
+ //do not log HEADERS, as it could contain an Authorization header
+ //TbSync.dump("HEADERS", JSON.stringify(headers));
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("REQUEST", method + " : " + requestData);
+
+ if (!options.hasOwnProperty("softfail")) {
+ options.softfail = [];
+ }
+
+ if (!options.hasOwnProperty("responseType")) {
+ options.responseType = "xml";
+ }
+
+ return new Promise(function(resolve, reject) {
+ let req = new HttpRequest();
+
+ req.timeout = connectionData.timeout;
+ req.mozBackgroundRequest = true;
+
+ req.open(method, connectionData.url, true, connectionData.username, connectionData.password);
+
+ if (options.hasOwnProperty("containerRealm")) req.setContainerRealm(options.containerRealm);
+ if (options.hasOwnProperty("containerReset") && options.containerReset == true) req.clearContainerCache();
+
+ if (headers) {
+ for (let header in headers) {
+ req.setRequestHeader(header, headers[header]);
+ }
+ }
+
+ if (options.responseType == "base64") {
+ req.responseAsBase64 = true;
+ }
+
+ req.setRequestHeader("User-Agent", dav.sync.prefSettings.getCharPref("clientID.useragent"));
+
+ req.realmCallback = function(username, realm, host) {
+ // Store realm, needed later to setup lightning passwords.
+ TbSync.dump("Found CalDAV authRealm for <"+host+">", realm);
+ connectionData.realm = realm;
+ };
+
+ req.onerror = function () {
+ let error = TbSync.network.createTCPErrorFromFailedXHR(req);
+ if (!error) {
+ return reject(dav.sync.finish("error", "networkerror", "URL:\n" + connectionData.url + " ("+method+")")); //reject/resolve do not terminate control flow
+ } else {
+ return reject(dav.sync.finish("error", error, "URL:\n" + connectionData.url + " ("+method+")"));
+ }
+ };
+
+ req.ontimeout = req.onerror;
+
+ req.onredirect = function(flags, uri) {
+ console.log("Redirect ("+ flags.toString(2) +"): " + uri.spec);
+ // Update connection settings from current URL
+ let newHttps = (uri.scheme == "https");
+ if (connectionData.https != newHttps) {
+ TbSync.dump("Updating HTTPS", connectionData.https + " -> " + newHttps);
+ connectionData.https = newHttps;
+ }
+ if (connectionData.fqdn !=uri.hostPort) {
+ TbSync.dump("Updating FQDN", connectionData.fqdn + " -> " + uri.hostPort);
+ connectionData.fqdn = uri.hostPort;
+ }
+ };
+
+ req.onload = function() {
+ if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("RESPONSE", req.status + " ("+req.statusText+")" + " : " + req.responseText);
+ responseData = req.responseText.split("><").join(">\n<");
+
+ let commLog = "URL:\n" + connectionData.url + " ("+method+")" + "\n\nRequest:\n" + requestData + "\n\nResponse:\n" + responseData;
+ let aResult = req.responseText;
+ let responseStatus = req.status;
+
+ switch(responseStatus) {
+ case 401: //AuthError
+ {
+ let response = {};
+ response.passwordPrompt = true;
+ response.passwordError = dav.sync.finish("error", responseStatus, commLog);
+ return resolve(response);
+ }
+ break;
+
+ case 207: //preprocess multiresponse
+ {
+ let xml = dav.tools.convertToXML(aResult);
+ if (xml === null) return reject(dav.sync.finish("warning", "malformed-xml", commLog));
+
+ let response = {};
+ response.davOptions = req.getResponseHeader("dav");
+ response.responseURL = req.responseURL;
+ response.permanentlyRedirectedUrl = req.permanentlyRedirectedUrl;
+ response.commLog = commLog;
+ response.node = xml.documentElement;
+
+ let multi = xml.documentElement.getElementsByTagNameNS(dav.sync.ns.d, "response");
+ response.multi = [];
+ for (let i=0; i < multi.length; i++) {
+ let hrefNode = dav.tools.evaluateNode(multi[i], [["d","href"]]);
+ let responseStatusNode = dav.tools.evaluateNode(multi[i], [["d", "status"]]);
+ let propstats = multi[i].getElementsByTagNameNS(dav.sync.ns.d, "propstat");
+ if (propstats.length > 0) {
+ //response contains propstats, push each as single entry
+ for (let p=0; p < propstats.length; p++) {
+ let statusNode = dav.tools.evaluateNode(propstats[p], [["d", "status"]]);
+
+ let resp = {};
+ resp.node = propstats[p];
+ resp.status = statusNode === null ? null : statusNode.textContent.split(" ")[1];
+ resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1];
+ resp.href = hrefNode === null ? null : hrefNode.textContent;
+ response.multi.push(resp);
+ }
+ } else {
+ //response does not contain any propstats, push raw response
+ let resp = {};
+ resp.node = multi[i];
+ resp.status = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1];
+ resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1];
+ resp.href = hrefNode === null ? null : hrefNode.textContent;
+ response.multi.push(resp);
+ }
+ }
+
+ return resolve(response);
+ }
+
+
+ case 200: //returned by DELETE by radicale - watch this !!!
+ return resolve(aResult);
+
+ case 204: //is returned by DELETE - no data
+ case 201: //is returned by CREATE - no data
+ return resolve(null);
+ break;
+
+ default:
+ if (options.softfail.includes(responseStatus)) {
+ let noresponse = {};
+ noresponse.softerror = responseStatus;
+ let xml = dav.tools.convertToXML(aResult);
+ if (xml !== null) {
+ let exceptionNode = dav.tools.evaluateNode(xml.documentElement, [["s","exception"]]);
+ if (exceptionNode !== null) {
+ noresponse.exception = exceptionNode.textContent;
+ }
+ }
+ //manually log this non-fatal error
+ TbSync.eventlog.add("info", connectionData.eventLogInfo, "softerror::"+responseStatus, commLog);
+ return resolve(noresponse);
+ } else {
+ return reject(dav.sync.finish("warning", responseStatus, commLog));
+ }
+ break;
+
+ }
+ };
+
+ req.send(requestData);
+ });
+ }
+}
diff --git a/content/includes/sync.js b/content/includes/sync.js
new file mode 100644
index 0000000..5a73bb3
--- /dev/null
+++ b/content/includes/sync.js
@@ -0,0 +1,462 @@
+/*
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+
+var sync = {
+
+ finish: function (aStatus = "", msg = "", details = "") {
+ let status = TbSync.StatusData.SUCCESS
+ switch (aStatus) {
+
+ case "":
+ case "ok":
+ status = TbSync.StatusData.SUCCESS;
+ break;
+
+ case "info":
+ status = TbSync.StatusData.INFO;
+ break;
+
+ case "resyncAccount":
+ status = TbSync.StatusData.ACCOUNT_RERUN;
+ break;
+
+ case "resyncFolder":
+ status = TbSync.StatusData.FOLDER_RERUN;
+ break;
+
+ case "warning":
+ status = TbSync.StatusData.WARNING;
+ break;
+
+ case "error":
+ status = TbSync.StatusData.ERROR;
+ break;
+
+ default:
+ console.log("TbSync/DAV: Unknown status <"+aStatus+">");
+ status = TbSync.StatusData.ERROR;
+ break;
+ }
+
+ let e = new Error();
+ e.name = "dav4tbsync";
+ e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")";
+ e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString());
+ return e;
+ },
+
+ prefSettings: Services.prefs.getBranch("extensions.dav4tbsync."),
+
+ ns: {
+ d: "DAV:",
+ cal: "urn:ietf:params:xml:ns:caldav" ,
+ card: "urn:ietf:params:xml:ns:carddav" ,
+ cs: "http://calendarserver.org/ns/",
+ s: "http://sabredav.org/ns",
+ apple: "http://apple.com/ns/ical/"
+ },
+
+ serviceproviders: {
+ "fruux" : {revision: 1, icon: "fruux", caldav: "https://dav.fruux.com", carddav: "https://dav.fruux.com"},
+ "mbo" : {revision: 1, icon: "mbo", caldav: "caldav6764://mailbox.org", carddav: "carddav6764://mailbox.org"},
+ "icloud" : {revision: 1, icon: "icloud", caldav: "https://caldav.icloud.com", carddav: "https://contacts.icloud.com"},
+ "gmx.net" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.net", carddav: "carddav6764://gmx.net"},
+ "gmx.com" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.com", carddav: "carddav6764://gmx.com"},
+ "posteo" : {revision: 1, icon: "posteo", caldav: "https://posteo.de:8443", carddav: "posteo.de:8843"},
+ "web.de" : {revision: 1, icon: "web", caldav: "caldav6764://web.de", carddav: "carddav6764://web.de"},
+ "yahoo" : {revision: 1, icon: "yahoo", caldav: "caldav6764://yahoo.com", carddav: "carddav6764://yahoo.com"},
+ },
+
+ resetFolderSyncInfo : function (folderData) {
+ folderData.resetFolderProperty("ctag");
+ folderData.resetFolderProperty("token");
+ folderData.setFolderProperty("createdWithProviderVersion", folderData.accountData.providerData.getVersion());
+ },
+
+ folderList: async function (syncData) {
+ //Method description: http://sabre.io/dav/building-a-caldav-client/
+ //get all folders currently known
+ let folderTypes = ["caldav", "carddav", "ics"];
+ let unhandledFolders = {};
+ for (let type of folderTypes) {
+ unhandledFolders[type] = [];
+ }
+
+
+ let folders = syncData.accountData.getAllFolders();
+ for (let folder of folders) {
+ //just in case
+ if (!unhandledFolders.hasOwnProperty(folder.getFolderProperty("type"))) {
+ unhandledFolders[folder.getFolderProperty("type")] = [];
+ }
+ unhandledFolders[folder.getFolderProperty("type")].push(folder);
+ }
+
+ // refresh urls of service provider, if they have been updated
+ let serviceprovider = syncData.accountData.getAccountProperty("serviceprovider");
+ let serviceproviderRevision = syncData.accountData.getAccountProperty("serviceproviderRevision");
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider) && serviceproviderRevision != dav.sync.serviceproviders[serviceprovider].revision) {
+ TbSync.eventlog.add("info", syncData.eventLogInfo, "updatingServiceProvider", serviceprovider);
+ syncData.accountData.setAccountProperty("serviceproviderRevision", dav.sync.serviceproviders[serviceprovider].revision);
+ syncData.accountData.resetAccountProperty("calDavPrincipal");
+ syncData.accountData.resetAccountProperty("cardDavPrincipal");
+ syncData.accountData.setAccountProperty("calDavHost", dav.sync.serviceproviders[serviceprovider].caldav);
+ syncData.accountData.setAccountProperty("cardDavHost", dav.sync.serviceproviders[serviceprovider].carddav);
+ }
+
+ let davjobs = {
+ cal : {server: syncData.accountData.getAccountProperty("calDavHost")},
+ card : {server: syncData.accountData.getAccountProperty("cardDavHost")},
+ };
+
+ for (let job in davjobs) {
+ if (!davjobs[job].server) continue;
+
+ // SOGo needs some special handling for shared addressbooks. We detect
+ // it by having SOGo/dav in the url.
+ let isSogo = davjobs[job].server.includes("/SOGo/dav");
+
+ // sync states are only printed while the account state is "syncing"
+ // to inform user about sync process (it is not stored in DB, just in
+ // syncData)
+ // example state "getfolders" to get folder information from server
+ // if you send a request to a server and thus have to wait for answer,
+ // use a "send." syncstate, which will give visual feedback to the user,
+ // that we are waiting for an answer with timeout countdown
+
+ let home = [];
+ let own = [];
+
+ // migration code for http setting, we might keep it as a fallback, if user removed the http:// scheme from the url in the settings
+ if (!dav.network.startsWithScheme(davjobs[job].server)) {
+ davjobs[job].server = "http" + (syncData.accountData.getAccountProperty("https") ? "s" : "") + "://" + davjobs[job].server;
+ syncData.accountData.setAccountProperty(job + "DavHost", davjobs[job].server);
+ }
+
+ //add connection to syncData
+ syncData.connectionData = new dav.network.ConnectionData(syncData);
+
+ //only do that, if a new calendar has been enabled
+ TbSync.network.resetContainerForUser(syncData.connectionData.username);
+
+ syncData.setSyncState("send.getfolders");
+ let principal = syncData.accountData.getAccountProperty(job + "DavPrincipal"); // defaults to null
+ if (principal === null) {
+
+ let response = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-principal /></d:prop></d:propfind>", davjobs[job].server , "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"});
+ syncData.setSyncState("eval.folders");
+
+ // keep track of permanent redirects for the server URL
+ if (response && response.permanentlyRedirectedUrl) {
+ syncData.accountData.setAccountProperty(job + "DavHost", response.permanentlyRedirectedUrl)
+ }
+
+ // store dav options send by server
+ if (response && response.davOptions) {
+ syncData.accountData.setAccountProperty(job + "DavOptions", response.davOptions.split(",").map(e => e.trim()));
+ }
+
+ // allow 404 because iCloud sends it on valid answer (yeah!)
+ if (response && response.multi) {
+ principal = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d","current-user-principal"], ["d","href"]], null, ["200","404"]);
+ }
+ }
+
+ //principal now contains something like "/remote.php/carddav/principals/john.bieling/"
+ //principal can also be an absolute url
+ // -> get home/root of storage
+ if (principal !== null) {
+ syncData.setSyncState("send.getfolders");
+
+ let options = syncData.accountData.getAccountProperty(job + "DavOptions");
+
+ let homeset = (job == "cal")
+ ? "calendar-home-set"
+ : "addressbook-home-set";
+
+ let request = "<d:propfind "+dav.tools.xmlns(["d", job, "cs"])+"><d:prop><"+job+":" + homeset + " />"
+ + (job == "cal" && options.includes("calendar-proxy") ? "<cs:calendar-proxy-write-for /><cs:calendar-proxy-read-for />" : "")
+ + "<d:group-membership />"
+ + "</d:prop></d:propfind>";
+
+ let response = await dav.network.sendRequest(request, principal, "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"});
+ syncData.setSyncState("eval.folders");
+
+ // keep track of permanent redirects for the principal URL
+ if (response && response.permanentlyRedirectedUrl) {
+ principal = response.permanentlyRedirectedUrl;
+ }
+
+ own = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], principal);
+ home = own.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-read-for" ], ["d","href"]], principal));
+ home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-write-for" ], ["d","href"]], principal));
+
+ //Any groups we need to find? Only diving one level at the moment,
+ let g = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["d", "group-membership" ], ["d","href"]], principal);
+ for (let gc=0; gc < g.length; gc++) {
+ //SOGo reports a 403 if I request the provided resource, also since we do not dive, remove the request for group-membership
+ response = await dav.network.sendRequest(request.replace("<d:group-membership />",""), g[gc], "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {softfail: [403, 404]});
+ if (response && response.softerror) {
+ continue;
+ }
+ home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], g[gc]));
+ }
+
+ //calendar-proxy and group-membership could have returned the same values, make the homeset unique
+ home = home.filter((v,i,a) => a.indexOf(v) == i);
+ } else {
+ // do not throw here, but log the error and skip this server
+ TbSync.eventlog.add("error", syncData.eventLogInfo, job+"davservernotfound", davjobs[job].server);
+ }
+
+ //home now contains something like /remote.php/caldav/calendars/john.bieling/
+ // -> get all resources
+ if (home.length > 0) {
+ // the used principal returned valid resources, store/update it
+ // as the principal is being used as a starting point, it must be stored as absolute url
+ syncData.accountData.setAccountProperty(job + "DavPrincipal", dav.network.startsWithScheme(principal)
+ ? principal
+ : "http" + (syncData.connectionData.https ? "s" : "") + "://" + syncData.connectionData.fqdn + principal);
+
+ for (let h=0; h < home.length; h++) {
+ syncData.setSyncState("send.getfolders");
+ let request = (job == "cal")
+ ? "<d:propfind "+dav.tools.xmlns(["d","apple","cs","cal"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /><apple:calendar-color/><cs:source/><cal:supported-calendar-component-set/></d:prop></d:propfind>"
+ : "<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /></d:prop></d:propfind>";
+
+ //some servers report to have calendar-proxy-read but return a 404 when that gets actually queried
+ let response = await dav.network.sendRequest(request, home[h], "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: [403, 404]});
+ if (response && response.softerror) {
+ continue;
+ }
+
+ for (let r=0; r < response.multi.length; r++) {
+ if (response.multi[r].status != "200") continue;
+
+ let resourcetype = null;
+ //is this a result with a valid recourcetype? (the node must be present)
+ switch (job) {
+ case "card":
+ if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["card", "addressbook"]]) !== null) resourcetype = "carddav";
+ break;
+
+ case "cal":
+ if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cal", "calendar"]]) !== null) resourcetype = "caldav";
+ else if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cs", "subscribed"]]) !== null) resourcetype = "ics";
+ break;
+ }
+ if (resourcetype === null) continue;
+
+ //get ACL (grant read rights per default, if it is SOGo, as they do not send that permission)
+ let acl = isSogo ? 0x1 : 0;
+
+ let privilegNode = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","current-user-privilege-set"]]);
+ if (privilegNode) {
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "all").length > 0) {
+ acl = 0xF; //read=1, mod=2, create=4, delete=8
+ } else {
+ // check for individual write permissions
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write").length > 0) {
+ acl = 0xF;
+ } else {
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write-content").length > 0) acl |= 0x2;
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "bind").length > 0) acl |= 0x4;
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "unbind").length > 0) acl |= 0x8;
+ }
+
+ // check for read permission (implying read if any write is given)
+ if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "read").length > 0 || acl != 0) acl |= 0x1;
+ }
+ }
+
+ //ignore this resource, if no read access
+ if ((acl & 0x1) == 0) continue;
+
+ let href = response.multi[r].href;
+ if (resourcetype == "ics") href = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["cs","source"], ["d","href"]]).textContent;
+
+ let name_node = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","displayname"]]);
+ let name = TbSync.getString("defaultname." + ((job == "cal") ? "calendar" : "contacts") , "dav");
+ if (name_node != null) {
+ name = name_node.textContent;
+ }
+ let color = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["apple","calendar-color"]]);
+ let supportedCalComponent = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["cal","supported-calendar-component-set"]]);
+ if (supportedCalComponent) {
+ supportedCalComponent = Array.from(supportedCalComponent.children, e => e.getAttribute("name"));
+ } else {
+ supportedCalComponent = [];
+ }
+ if (job == "cal" && supportedCalComponent.length > 0 && !supportedCalComponent.includes("VTODO") && !supportedCalComponent.includes("VEVENT")) {
+ // This does not seem to be a valid resource.
+ continue;
+ }
+
+ //remove found folder from list of unhandled folders
+ unhandledFolders[resourcetype] = unhandledFolders[resourcetype].filter(item => item.getFolderProperty("href") !== href);
+
+
+ // interaction with TbSync
+ // do we have a folder for that href?
+ let folderData = syncData.accountData.getFolder("href", href);
+ if (!folderData) {
+ // create a new folder entry
+ folderData = syncData.accountData.createNewFolder();
+ // this MUST be set to either "addressbook" or "calendar" to use the standard target support, or any other value, which
+ // requires a corresponding targets implementation by this provider
+ folderData.setFolderProperty("targetType", (job == "card") ? "addressbook" : "calendar");
+
+ folderData.setFolderProperty("href", href);
+ folderData.setFolderProperty("foldername", name);
+ folderData.setFolderProperty("type", resourcetype);
+ folderData.setFolderProperty("supportedCalComponent", supportedCalComponent);
+ folderData.setFolderProperty("shared", !own.includes(home[h]));
+ folderData.setFolderProperty("acl", acl.toString());
+ folderData.setFolderProperty("downloadonly", (acl == 0x1)); //if any write access is granted, setup as writeable
+
+ //we assume the folder has the same fqdn as the homeset, otherwise href must contain the full URL and the fqdn is ignored
+ folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn);
+ folderData.setFolderProperty("https", syncData.connectionData.https);
+
+ //do we have a cached folder?
+ let cachedFolderData = syncData.accountData.getFolderFromCache("href", href);
+ if (cachedFolderData) {
+ // copy fields from cache which we want to re-use
+ folderData.setFolderProperty("targetColor", cachedFolderData.getFolderProperty("targetColor"));
+ folderData.setFolderProperty("targetName", cachedFolderData.getFolderProperty("targetName"));
+ //if we have only READ access, do not restore cached value for downloadonly
+ if (acl > 0x1) folderData.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly"));
+ }
+ } else {
+ //Update name & color
+ folderData.setFolderProperty("foldername", name);
+ folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn);
+ folderData.setFolderProperty("https", syncData.connectionData.https);
+ folderData.setFolderProperty("acl", acl);
+ //if the acl changed from RW to RO we need to update the downloadonly setting
+ if (acl == 0x1) {
+ folderData.setFolderProperty("downloadonly", true);
+ }
+ }
+
+ // Update color from server.
+ if (color && job == "cal") {
+ color = color.textContent.substring(0,7);
+ folderData.setFolderProperty("targetColor", color);
+
+ // Do we have to update the calendar?
+ if (folderData.targetData && folderData.targetData.hasTarget()) {
+ try {
+ let targetCal = await folderData.targetData.getTarget();
+ targetCal.calendar.setProperty("color", color);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+ }
+ }
+ } else {
+ //home was not found - connection error? - do not delete unhandled folders
+ switch (job) {
+ case "card":
+ unhandledFolders.carddav = [];
+ break;
+
+ case "cal":
+ unhandledFolders.caldav = [];
+ unhandledFolders.ics = [];
+ break;
+ }
+ //reset stored principal
+ syncData.accountData.resetAccountProperty(job + "DavPrincipal");
+ }
+ }
+
+ // Remove unhandled old folders, (because they no longer exist on the server).
+ // Do not delete the targets, but keep them as stale/unconnected elements.
+ for (let type of folderTypes) {
+ for (let folder of unhandledFolders[type]) {
+ folder.remove("[deleted on server]");
+ }
+ }
+ },
+
+
+
+
+
+
+ folder: async function (syncData) {
+ // add connection data to syncData
+ syncData.connectionData = new dav.network.ConnectionData(syncData);
+
+ // add target to syncData
+ let hadTarget;
+ try {
+ // accessing the target for the first time will check if it is avail and if not will create it (if possible)
+ hadTarget = syncData.currentFolderData.targetData.hasTarget();
+ syncData.target = await syncData.currentFolderData.targetData.getTarget();
+ } catch (e) {
+ Components.utils.reportError(e);
+ throw dav.sync.finish("warning", e.message);
+ }
+
+ switch (syncData.currentFolderData.getFolderProperty("type")) {
+ case "carddav":
+ {
+ // update downloadonly - we do not use AbDirectory (syncData.target) but the underlying thunderbird addressbook obj
+ if (syncData.currentFolderData.getFolderProperty("downloadonly")) syncData.target.directory.setBoolValue("readOnly", true);
+
+ try {
+ let davDirectory = CardDAVDirectory.forFile(syncData.target.directory.fileName);
+ if (!hadTarget) {
+ davDirectory.fetchAllFromServer();
+ } else {
+ davDirectory.syncWithServer();
+ }
+ } catch (ex) {
+ throw dav.sync.finish("error", "non-carddav-addrbook");
+ }
+
+ throw dav.sync.finish("ok", "managed-by-thunderbird");
+ }
+ break;
+
+ case "caldav":
+ case "ics":
+ {
+ // update downloadonly - we do not use TbCalendar (syncData.target) but the underlying lightning calendar obj
+ if (syncData.currentFolderData.getFolderProperty("downloadonly")) syncData.target.calendar.setProperty("readOnly", true);
+
+ // update username of calendar
+ syncData.target.calendar.setProperty("username", syncData.connectionData.username);
+
+ //init sync via lightning
+ if (hadTarget) syncData.target.calendar.refresh();
+
+ throw dav.sync.finish("ok", "managed-by-thunderbird");
+ }
+ break;
+
+ default:
+ {
+ throw dav.sync.finish("warning", "notsupported");
+ }
+ break;
+ }
+ },
+
+}
diff --git a/content/includes/tools.js b/content/includes/tools.js
new file mode 100644
index 0000000..df51d79
--- /dev/null
+++ b/content/includes/tools.js
@@ -0,0 +1,198 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var tools = {
+
+ //* * * * * * * * * * * * *
+ //* UTILS
+ //* * * * * * * * * * * * *
+
+ /**
+ * Removes XML-invalid characters from a string.
+ * @param {string} string - a string potentially containing XML-invalid characters, such as non-UTF8 characters, STX, EOX and so on.
+ * @param {boolean} removeDiscouragedChars - a string potentially containing XML-invalid characters, such as non-UTF8 characters, STX, EOX and so on.
+ * @returns : a sanitized string without all the XML-invalid characters.
+ *
+ * Source: https://www.ryadel.com/en/javascript-remove-xml-invalid-chars-characters-string-utf8-unicode-regex/
+ */
+ removeXMLInvalidChars: function (string, removeDiscouragedChars = true)
+ {
+ // remove everything forbidden by XML 1.0 specifications, plus the unicode replacement character U+FFFD
+ var regex = /((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g;
+ string = string.replace(regex, "");
+
+ if (removeDiscouragedChars) {
+ // remove everything not suggested by XML 1.0 specifications
+ regex = new RegExp(
+ "([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF"+
+ "FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD"+
+ "FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])"+
+ "|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\"+
+ "uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF"+
+ "[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\"+
+ "uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|"+
+ "(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))", "g");
+ string = string.replace(regex, "");
+ }
+
+ return string;
+ },
+
+ xmlns: function (ns) {
+ let _xmlns = [];
+ for (let i=0; i < ns.length; i++) {
+ _xmlns.push('xmlns:'+ns[i]+'="'+dav.sync.ns[ns[i]]+'"');
+ }
+ return _xmlns.join(" ");
+ },
+
+ parseUri: function (aUri) {
+ let uri;
+ try {
+ // Test if the entered uri can be parsed.
+ uri = Services.io.newURI(aUri, null, null);
+ } catch (ex) {
+ throw new Error("invalid-calendar-url");
+ }
+ return uri;
+ },
+
+ parseVcardDateTime: function ( newServerValue, metadata ) {
+ if (!newServerValue) {
+ return false;
+ }
+
+ /*
+ ** This accepts RFC2426 BDAY values (with/without hyphens),
+ ** though TB doesn't handle the time part of date-times, so we discard it.
+ */
+ let bday = newServerValue.match( /^(\d{4})-?(\d{2})-?(\d{2})/ );
+ if (!bday) {
+ return false;
+ }
+
+ /*
+ ** Apple Contacts shoehorns date with missing year into vcard3 thus: BDAY;X-APPLE-OMIT-YEAR=1604:1604-03-15
+ ** Later in vcard4, it will be represented as BDAY:--0315
+ */
+ if (metadata
+ && metadata['x-apple-omit-year']
+ && metadata['x-apple-omit-year'] == bday[1]) {
+ bday[1] = '';
+ }
+ return bday;
+ },
+
+ //* * * * * * * * * * * * * *
+ //* EVALUATE XML RESPONSES *
+ //* * * * * * * * * * * * * *
+
+ convertToXML: function(text) {
+ //try to convert response body to xml
+ let xml = null;
+ let oParser = new DOMParser();
+ try {
+ xml = oParser.parseFromString(dav.tools.removeXMLInvalidChars(text), "application/xml");
+ } catch (e) {
+ //however, domparser does not throw an error, it returns an error document
+ //https://developer.mozilla.org/de/docs/Web/API/DOMParser
+ xml = null;
+ }
+ //check if xml is error document
+ if (xml && xml.documentElement.nodeName == "parsererror") {
+ xml = null;
+ }
+
+ return xml;
+ },
+
+ evaluateNode: function (_node, path) {
+ let node = _node;
+ let valid = false;
+
+ for (let i=0; i < path.length; i++) {
+
+ let children = node.children;
+ valid = false;
+
+ for (let c=0; c < children.length; c++) {
+ if (children[c].localName == path[i][1] && children[c].namespaceURI == dav.sync.ns[path[i][0]]) {
+ node = children[c];
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid) {
+ //none of the children matched the path abort
+ return null;
+ }
+ }
+
+ if (valid) return node;
+ return null;
+ },
+
+ hrefMatch:function (_requestHref, _responseHref) {
+ if (_requestHref === null)
+ return true;
+
+ let requestHref = _requestHref;
+ let responseHref = _responseHref;
+ while (requestHref.endsWith("/")) { requestHref = requestHref.slice(0,-1); }
+ while (responseHref.endsWith("/")) { responseHref = responseHref.slice(0,-1); }
+ if (requestHref.endsWith(responseHref) || decodeURIComponent(requestHref).endsWith(responseHref) || requestHref.endsWith(decodeURIComponent(responseHref)))
+ return true;
+
+ return false;
+ },
+
+ getNodeTextContentFromMultiResponse: function (response, path, href = null, status = ["200"]) {
+ for (let i=0; i < response.multi.length; i++) {
+ let node = dav.tools.evaluateNode(response.multi[i].node, path);
+ if (node !== null && dav.tools.hrefMatch(href, response.multi[i].href) && status.includes(response.multi[i].status)) {
+ return node.textContent;
+ }
+ }
+ return null;
+ },
+
+ getNodesTextContentFromMultiResponse: function (response, path, href = null, status = "200") {
+ //remove last element from path
+ let lastPathElement = path.pop();
+ let rv = [];
+
+ for (let i=0; i < response.multi.length; i++) {
+ let node = dav.tools.evaluateNode(response.multi[i].node, path);
+ if (node !== null && dav.tools.hrefMatch(href, response.multi[i].href) && response.multi[i].status == status) {
+
+ //get all children
+ let children = node.getElementsByTagNameNS(dav.sync.ns[lastPathElement[0]], lastPathElement[1]);
+ for (let c=0; c < children.length; c++) {
+ if (children[c].textContent) rv.push(children[c].textContent);
+ }
+ }
+ }
+ return rv;
+ },
+
+ getMultiGetRequest: function(hrefs) {
+ let request = "<card:addressbook-multiget "+dav.tools.xmlns(["d", "card"])+"><d:prop><d:getetag /><card:address-data /></d:prop>";
+ let counts = 0;
+ for (let i=0; i < hrefs.length; i++) {
+ request += "<d:href>"+hrefs[i]+"</d:href>";
+ counts++;
+ }
+ request += "</card:addressbook-multiget>";
+
+ if (counts > 0) return request;
+ else return null;
+ },
+}
diff --git a/content/locales.js b/content/locales.js
new file mode 100644
index 0000000..a8beca0
--- /dev/null
+++ b/content/locales.js
@@ -0,0 +1,3 @@
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+TbSync.localizeOnLoad(window, "dav");
diff --git a/content/manager/createAccount.js b/content/manager/createAccount.js
new file mode 100644
index 0000000..3e331fe
--- /dev/null
+++ b/content/manager/createAccount.js
@@ -0,0 +1,524 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+const dav = TbSync.providers.dav;
+
+var tbSyncDavNewAccount = {
+
+ // standard data fields
+ get elementName() { return document.getElementById('tbsync.newaccount.name'); },
+ get elementUser() { return document.getElementById('tbsync.newaccount.user'); },
+ get elementPass() { return document.getElementById('tbsync.newaccount.password'); },
+ get elementServer() { return document.getElementById('tbsync.newaccount.server'); },
+ get elementCalDavServer() { return document.getElementById('tbsync.newaccount.caldavserver'); },
+ get elementCardDavServer() { return document.getElementById('tbsync.newaccount.carddavserver'); },
+ get serviceproviderlist() { return document.getElementById('tbsync.newaccount.serviceproviderlist'); },
+
+ get accountname() { return this.elementName.value.trim(); },
+ get username() { return this.elementUser.value.trim(); },
+ get password() { return this.elementPass.value.trim(); },
+ get server() { return this.elementServer.value.trim(); },
+ get calDavServer() { return this.elementCalDavServer.value.trim(); },
+ get cardDavServer() { return this.elementCardDavServer.value.trim(); },
+ get serviceprovider() { return this.serviceproviderlist.value; },
+ get userdomain() {
+ let parts = this.username.split("@");
+ if (parts.length == 2) {
+ let subparts = parts[1].split(".");
+ if (subparts.length > 1 && subparts[subparts.length-1].length > 1) return parts[1];
+ }
+ return null;
+ },
+
+ set accountname(v) { this.elementName.value = v; },
+ set username(v) { this.elementUser.value = v; },
+ set password(v) { this.elementPass.value = v; },
+ set server(v) { this.elementServer.value = v; },
+ set calDavServer(v) { this.elementCalDavServer.value = v; },
+ set cardDavServer(v) { this.elementCardDavServer.value = v; },
+
+
+
+ // final data fields on final page
+ get elementFinalName() { return document.getElementById('tbsync.finalaccount.name'); },
+ get elementFinalUser() { return document.getElementById('tbsync.finalaccount.user'); },
+ get elementFinalCalDavServer() { return document.getElementById('tbsync.finalaccount.caldavserver'); },
+ get elementFinalCardDavServer() { return document.getElementById('tbsync.finalaccount.carddavserver'); },
+
+ get finalAccountname() { return this.elementFinalName.value.trim(); },
+ get finalUsername() { return this.elementFinalUser.value.trim(); },
+ get finalCalDavServer() { return this.elementFinalCalDavServer.value.trim(); },
+ get finalCardDavServer() { return this.elementFinalCardDavServer.value.trim(); },
+
+ set finalAccountname(v) { this.elementFinalName.value = v;},
+ set finalUsername(v) {
+ this.elementFinalUser.value = v;
+ this.elementFinalUser.setAttribute("tooltiptext", v);
+ },
+ set finalCalDavServer(v) {
+ this.elementFinalCalDavServer.value = v;
+ this.elementFinalCalDavServer.setAttribute("tooltiptext", v);
+ document.getElementById("tbsyncfinalaccount.caldavserver.row").hidden = (v.trim() == "");
+ },
+ set finalCardDavServer(v) {
+ this.elementFinalCardDavServer.value = v;
+ this.elementFinalCardDavServer.setAttribute("tooltiptext", v);
+ document.getElementById("tbsyncfinalaccount.carddavserver.row").hidden = (v.trim() == "");
+ },
+
+ get validated() { return this._validated || false; },
+ set validated(v) {
+ this._validated = v;
+ if (v) {
+ this.finalAccountname = this.accountname;
+ } else {
+ this.finalAccountname = "";
+ this.finalUsername = "";
+ this.finalCalDavServer = "";
+ this.finalCardDavServer = "";
+ }
+ },
+
+
+ showSpinner: function(spinnerText) {
+ document.getElementById("tbsync.spinner").hidden = false;
+ document.getElementById("tbsync.spinner.label").value = TbSync.getString("add.spinner." + spinnerText, "dav");
+ },
+
+ hideSpinner: function() {
+ document.getElementById("tbsync.spinner").hidden = true;
+ },
+
+ onLoad: function () {
+ this.providerData = new TbSync.ProviderData("dav");
+
+ //init list
+ this.serviceproviderlist.appendChild(this.addProviderEntry("sabredav32.png", "discovery"));
+ this.serviceproviderlist.appendChild(this.addProviderEntry("sabredav32.png", "custom"));
+ for (let p in dav.sync.serviceproviders) {
+ this.serviceproviderlist.appendChild(this.addProviderEntry(dav.sync.serviceproviders[p].icon +"32.png", p));
+ }
+
+ document.addEventListener("wizardfinish", tbSyncDavNewAccount.onFinish.bind(this));
+ document.addEventListener("wizardnext", tbSyncDavNewAccount.onAdvance.bind(this));
+ document.addEventListener("wizardcancel", tbSyncDavNewAccount.onCancel.bind(this));
+ document.getElementById("firstPage").addEventListener("pageshow", tbSyncDavNewAccount.resetFirstPage.bind(this));
+ document.getElementById("secondPage").addEventListener("pageshow", tbSyncDavNewAccount.resetSecondPage.bind(this));
+ document.getElementById("thirdPage").addEventListener("pageshow", tbSyncDavNewAccount.resetThirdPage.bind(this));
+
+ this.serviceproviderlist.selectedIndex = 0;
+ tbSyncDavNewAccount.resetFirstPage();
+ },
+
+ onUnload: function () {
+ },
+
+ onClose: function () {
+ //disallow closing of wizard while isLocked
+ return !this.isLocked;
+ },
+
+ onCancel: function (event) {
+ //disallow closing of wizard while isLocked
+ if (this.isLocked) {
+ event.preventDefault();
+ }
+ },
+
+ onFinish () {
+ let newAccountEntry = this.providerData.getDefaultAccountEntries();
+ newAccountEntry.createdWithProviderVersion = this.providerData.getVersion();
+ newAccountEntry.serviceprovider = this.serviceprovider == "discovery" ? "custom" : this.serviceprovider;
+ newAccountEntry.serviceproviderRevision = dav.sync.serviceproviders.hasOwnProperty(this.serviceprovider) ? dav.sync.serviceproviders[this.serviceprovider].revision : 0
+ newAccountEntry.calDavHost = this.finalCalDavServer;
+ newAccountEntry.cardDavHost = this.finalCardDavServer;
+
+ // Add the new account.
+ let newAccountData = this.providerData.addAccount(this.finalAccountname, newAccountEntry);
+ dav.network.getAuthData(newAccountData).updateLoginData(this.finalUsername, this.password);
+ },
+
+
+
+
+
+ // HELPER FUNCTIONS
+ addProviderEntry: function (icon, serviceprovider) {
+ let name = TbSync.getString("add.serverprofile."+serviceprovider, "dav");
+ let description = TbSync.getString("add.serverprofile."+serviceprovider+".description", "dav");
+
+ //left column
+ let image = document.createXULElement("image");
+ image.setAttribute("src", "chrome://dav4tbsync/content/skin/" + icon);
+ image.setAttribute("style", "margin:1ex;");
+
+ let leftColumn = document.createXULElement("vbox");
+ leftColumn.appendChild(image);
+
+ //right column
+ let label = document.createXULElement("label");
+ label.setAttribute("class", "header");
+ label.setAttribute("value", name);
+
+ let desc = document.createXULElement("description");
+ desc.setAttribute("style", "width: 300px");
+ desc.textContent = description;
+
+ let rightColumn = document.createXULElement("vbox");
+ rightColumn.appendChild(label);
+ rightColumn.appendChild(desc);
+
+ //columns
+ let columns = document.createXULElement("hbox");
+ columns.appendChild(leftColumn);
+ columns.appendChild(rightColumn);
+
+ //richlistitem
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("style", "padding:4px");
+ richlistitem.setAttribute("value", serviceprovider);
+ richlistitem.appendChild(columns);
+
+ return richlistitem;
+ },
+
+ checkUrlForPrincipal: async function (job) {
+ // according to RFC6764, we must also try the username with cut-off domain part
+ // Note: This is never called for OAUTH serves (see onAdvance)
+ let users = [];
+ users.push(this.username);
+ if (this.userdomain) users.push(this.username.split("@")[0]);
+
+ for (let user of users) {
+ let connectionData = new dav.network.ConnectionData();
+ connectionData.password = this.password;
+ connectionData.username = user;
+ connectionData.timeout = 5000;
+
+ //only needed for proper error reporting - that dav needs this is beyond API - connectionData is not used by TbSync
+ //connectionData is a structure which contains all the information needed to establish and evaluate a network connection
+ connectionData.eventLogInfo = new TbSync.EventLogInfo("dav", this.accountname);
+
+ job.valid = false;
+ job.error = "";
+
+ try {
+ let response = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-principal /></d:prop></d:propfind>", job.server , "PROPFIND", connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {containerRealm: "setup", containerReset: true, passwordRetries: 0});
+ // allow 404 because iCloud sends it on valid answer (yeah!)
+ let principal = (response && response.multi) ? dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d","current-user-principal"], ["d","href"]], null, ["200","404"]) : null;
+ job.valid = (principal !== null);
+ if (!job.valid) {
+ job.error = job.type + "servernotfound";
+ TbSync.eventlog.add("warning", connectionData.eventLogInfo, job.error, response ? response.commLog : "");
+ } else {
+ job.validUser = user;
+ job.validUrl = (response ? response.permanentlyRedirectedUrl : null) || job.server;
+ return;
+ }
+ } catch (e) {
+ job.valid = false;
+ job.error = (e.statusData ? e.statusData.message : e.message);
+
+ if (e.name == "dav4tbsync") {
+ TbSync.eventlog.add("warning", connectionData.eventLogInfo, e.statusData.message, e.statusData.details);
+ } else {
+ Components.utils.reportError(e);
+ }
+ }
+
+ // only retry with other user, if 401
+ if (!job.error.startsWith("401")) {
+ break;
+ }
+ }
+
+ return;
+ },
+
+ advance: function () {
+ document.getElementById("tbsync.newaccount.wizard").advance(null);
+ },
+
+
+
+
+
+ // RESET AND INIT FUNCTIONS
+ clearValues: function () {
+ //clear fields
+ this.username = "";
+ this.password = "";
+ this.server = "";
+ this.calDavServer = "";
+ this.cardDavServer = "";
+
+ if (this.serviceprovider == "discovery" || this.serviceprovider == "custom") {
+ this.accountname = "";
+ } else {
+ this.accountname = TbSync.getString("add.serverprofile." + this.serviceprovider, "dav");
+ }
+ },
+
+ resetFirstPage: function () {
+ // RESET / INIT first page
+ document.getElementById("tbsync.newaccount.wizard").canRewind = false;
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = true;
+ // bug https://bugzilla.mozilla.org/show_bug.cgi?id=1618252
+ document.getElementById('tbsync.newaccount.wizard')._adjustWizardHeader();
+ this.isLocked = false;
+ this.validated = false;
+ },
+
+ resetSecondPage: function () {
+ // RESET / INIT second page
+ this.isLocked = false;
+ this.validated = false;
+
+ document.getElementById("tbsync.newaccount.wizard").canRewind = true;
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = false;
+ this.hideSpinner();
+ document.getElementById("tbsync.error").hidden = true;
+
+ this.checkUI();
+ },
+
+ resetThirdPage: function () {
+ // RESET / INIT third page
+ document.getElementById("tbsync.newaccount.wizard").canRewind = true;
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = true;
+ this.isLocked = false;
+ },
+
+
+
+
+
+ // UI FUNCTIONS
+ lockUI: function(spinnerText) {
+ this.showSpinner(spinnerText);
+ document.getElementById("tbsync.error").hidden = true;
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = false;
+ document.getElementById("tbsync.newaccount.wizard").canRewind = false;
+ this.isLocked = true;
+ },
+
+ unlockUI: function() {
+ this.hideSpinner();
+ document.getElementById("tbsync.newaccount.wizard").canRewind = true;
+ this.isLocked = false;
+ this.checkUI();
+ },
+
+ checkUI: function (hideError) {
+ if (hideError) {
+ document.getElementById("tbsync.error").hidden = true;
+ }
+
+ // determine, if we can advance or not
+ if (this.serviceprovider == "discovery") {
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = !(
+ (this.accountname == "") ||
+ (this.server == "" && !this.userdomain) ||
+ (this.server == "" && this.username == ""));
+ } else if (this.serviceprovider == "custom") {
+ // custom does not need username or password (allow annonymous access)
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = !(
+ (this.accountname == "") ||
+ (this.calDavServer + this.cardDavServer == ""));
+ } else {
+ // build in service providers do need a username and password
+ document.getElementById("tbsync.newaccount.wizard").canAdvance = !(
+ (this.accountname == "") ||
+ (this.password == "") ||
+ (this.username == ""));
+ }
+
+ // update placeholder attribute of server
+ this.elementServer.setAttribute("placeholder", this.userdomain ? TbSync.getString("add.serverprofile.discovery.server-optional", "dav") : "");
+
+
+ //show/hide additional descriptions (if avail)
+ let dFound = 0;
+ for (let i=1; i < 4; i++) {
+ let dElement = document.getElementById("tbsync.newaccount.details" + i);
+ let dLocaleString = "add.serverprofile." + this.serviceprovider + ".details" + i;
+ let dLocaleValue = TbSync.getString(dLocaleString, "dav");
+
+ let hide = (dLocaleValue == dLocaleString);
+ if (this.serviceprovider == "discovery") {
+ // show them according to UI state
+ switch (i) {
+ case 1:
+ hide = false;
+ break;
+ case 2:
+ hide = !this.userdomain;
+ break;
+ }
+ }
+
+ if (hide) {
+ dElement.textContent = "";
+ dElement.hidden = true;
+ } else {
+ dFound++;
+ dElement.textContent = dLocaleValue
+ dElement.hidden =false;
+ }
+ }
+
+ //hide Notes header, if no descriptions avail
+ let dLabel = document.getElementById("tbsync.newaccount.details.header");
+ dLabel.hidden = (dFound == 0);
+
+
+ //which server fields to show?
+ document.getElementById("tbsync.newaccount.finaluser.row").hidden = false;
+ document.getElementById("tbsync.newaccount.user.row").hidden = false;
+ document.getElementById("tbsync.newaccount.password.row").hidden = false;
+
+ if (this.serviceprovider == "discovery") {
+ document.getElementById("tbsync.newaccount.caldavserver.row").hidden = true;
+ document.getElementById("tbsync.newaccount.carddavserver.row").hidden = true;
+ document.getElementById("tbsync.newaccount.server.row").hidden = false;
+ //this.elementCalDavServer.disabled = false;
+ //this.elementCardDavServer.disabled = false;
+ } else if (this.serviceprovider == "custom") {
+ // custom
+ document.getElementById("tbsync.newaccount.caldavserver.row").hidden = false;
+ document.getElementById("tbsync.newaccount.carddavserver.row").hidden = false;
+ document.getElementById("tbsync.newaccount.server.row").hidden = true;
+ //this.elementCalDavServer.disabled = false;
+ //this.elementCardDavServer.disabled = false;
+ } else {
+ // build in service provider
+ document.getElementById("tbsync.newaccount.caldavserver.row").hidden = true;
+ document.getElementById("tbsync.newaccount.carddavserver.row").hidden = true;
+ document.getElementById("tbsync.newaccount.server.row").hidden = true;
+ //this.elementCalDavServer.disabled = true;
+ //this.elementCardDavServer.disabled = true;
+ this.calDavServer = dav.sync.serviceproviders[this.serviceprovider].caldav;
+ this.cardDavServer = dav.sync.serviceproviders[this.serviceprovider].carddav;
+ }
+ },
+
+
+
+
+
+ // SETUP LOGIC FUNCTION
+ onAdvance: function (event) {
+ // do not prevent advancing if we go from page 1 to page 2, or if validation succeeded
+ if (document.getElementById("tbsync.newaccount.wizard").currentPage.id == "firstPage" || this.validated) {
+ return;
+ }
+
+ // if we reach this, we are on page 2 but may not advance but
+ // go through the setup steps
+
+ if (this.serviceprovider == "discovery") {
+ while (this.server.endsWith("/")) { this.server = this.server.slice(0,-1); }
+ // the user may either specify a server or he could have entered an email with domain
+ let parts = (this.server || this.userdomain).split("://");
+ let scheme = (parts.length > 1) ? parts[0].toLowerCase() : "";
+ let host = parts[parts.length-1];
+
+ this.calDavServer = scheme + "caldav6764://" + host;
+ this.cardDavServer = scheme + "carddav6764://" + host;
+ this.validateDavServers();
+ } else {
+ // custom or service provider
+ this.validateDavServers();
+ }
+
+ event.preventDefault();
+ },
+
+ validateDavServers: async function() {
+ this.lockUI("validating");
+
+ // Do not manipulate input here.
+ //while (this.calDavServer.endsWith("/")) { this.calDavServer = this.calDavServer.slice(0,-1); }
+ //while (this.cardDavServer.endsWith("/")) { this.cardDavServer = this.cardDavServer.slice(0,-1); }
+
+ // Default to https, if http is not explicitly specified
+ if (this.calDavServer && !dav.network.startsWithScheme(this.calDavServer)) {
+ this.calDavServer = "https://" + this.calDavServer;
+ }
+ if (this.cardDavServer && !dav.network.startsWithScheme(this.cardDavServer)) {
+ this.cardDavServer = "https://" + this.cardDavServer;
+ }
+
+ let davJobs = [
+ {type: "caldav", server: this.calDavServer},
+ {type: "carddav", server: this.cardDavServer},
+ ];
+
+ let failedDavJobs = [];
+ let validUserFound = "";
+
+ for (let job = 0; job < davJobs.length; job++) {
+ if (!davJobs[job].server) {
+ continue;
+ }
+ await this.checkUrlForPrincipal(davJobs[job]);
+ if (!davJobs[job].valid) {
+ failedDavJobs.push(job);
+ } else if (!validUserFound) {
+ // set the found user
+ validUserFound = davJobs[job].validUser;
+ } else if (validUserFound != davJobs[job].validUser) {
+ // users do not match
+ failedDavJobs.push(job);
+ }
+ }
+
+ if (failedDavJobs.length == 0) {
+ // boom, setup completed
+ this.finalCalDavServer = davJobs[0].validUrl || "";
+ this.finalCardDavServer = davJobs[1].validUrl || "";
+ this.finalUsername = validUserFound;
+ this.validated = true;
+ this.unlockUI();
+ this.advance();
+ return;
+ } else {
+ //only display one error
+ let failedJob = failedDavJobs[0];
+ console.log("ERROR ("+davJobs[failedJob].type+"): " + davJobs[failedJob].error.toString());
+ switch (davJobs[failedJob].error.toString().split("::")[0]) {
+ case "401":
+ case "403":
+ case "503":
+ case "security":
+ document.getElementById("tbsync.error.message").textContent = TbSync.getString("status."+davJobs[failedJob].error, "dav");
+ break;
+ default:
+ if (this.serviceprovider == "discovery" && this.userdomain && !this.server) {
+ // the discovery mode has a special error msg, in case a userdomain was used as server, but failed and we need the user to provide the server
+ document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.rfc6764-lookup-failed::" +this.userdomain, "dav");
+ } else if (this.serviceprovider != "discovery" && this.serviceprovider != "custom") {
+ // error msg, that the serviceprovider setup seems wrong
+ document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.service-provider-setup-failed", "dav");
+ } else if (dav.network.isRFC6764Request(davJobs[failedJob].server)) {
+ // error msg, that discovery mode failed
+ document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.service-discovery-failed::" +davJobs[failedJob].server.split("://")[1], "dav");
+ } else {
+ document.getElementById("tbsync.error.message").textContent = TbSync.getString("status." + davJobs[failedJob].type + "servernotfound", "dav");
+ }
+ }
+ document.getElementById("tbsync.error").hidden = false;
+ this.unlockUI();
+ }
+ },
+};
diff --git a/content/manager/createAccount.xhtml b/content/manager/createAccount.xhtml
new file mode 100644
index 0000000..7bdc84a
--- /dev/null
+++ b/content/manager/createAccount.xhtml
@@ -0,0 +1,158 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window onload="tbSyncDavNewAccount.onLoad();" onunload="tbSyncDavNewAccount.onUnload();"
+ onclose="return tbSyncDavNewAccount.onClose()" xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl" />
+ </linkset>
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/manager/createAccount.js" />
+ <script type="application/javascript" src="chrome://dav4tbsync/content/locales.js" />
+
+ <wizard title="__DAV4TBSYNCMSG_add.title__" id="tbsync.newaccount.wizard" >
+
+ <wizardpage id="firstPage" onFirstPage="true" label="__DAV4TBSYNCMSG_add.serverprofile.title__">
+ <vbox flex="1">
+ <description style="width: auto; margin-top:1em;">__DAV4TBSYNCMSG_add.serverprofile.description__
+ </description>
+ <richlistbox id="tbsync.newaccount.serviceproviderlist" seltype="single"
+ style="width: auto; height: 400px; margin-top:1ex" onselect="tbSyncDavNewAccount.clearValues();"
+ ondblclick="tbSyncDavNewAccount.advance()" />
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="secondPage" label="__DAV4TBSYNCMSG_add.data.title__">
+ <vbox flex="1">
+ <description style="width: auto; margin-top:1em;">__DAV4TBSYNCMSG_add.data.description__</description>
+ <html:table style="margin-top:1ex">
+ <html:tr id="tbsync.newaccount.name.row" width="100%">
+ <html:td width="33%">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.name__" /></vbox>
+ </html:td>
+ <html:td width="67%">
+ <html:input style="width: 90%" id="tbsync.newaccount.name"
+ oninput="tbSyncDavNewAccount.checkUI();" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.user.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.user__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%" id="tbsync.newaccount.user"
+ oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.password.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.password__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%" id="tbsync.newaccount.password" type="password"
+ oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.server.row" style="margin-top:2em;">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.server__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%" id="tbsync.newaccount.server"
+ oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.caldavserver.row" style="margin-top:2em;">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.caldavserver__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%" id="tbsync.newaccount.caldavserver"
+ oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.carddavserver.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.carddavserver__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%" id="tbsync.newaccount.carddavserver"
+ oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </html:td>
+ </html:tr>
+ </html:table>
+ <label class="header" id="tbsync.newaccount.details.header" value="__DAV4TBSYNCMSG_add.data.notes__"
+ style="margin-top:2em" />
+ <description id="tbsync.newaccount.details1" style="width: auto; margin-top:1ex;"></description>
+ <description id="tbsync.newaccount.details2" style="width: auto; margin-top:1ex;"></description>
+ <description id="tbsync.newaccount.details3" style="width: auto; margin-top:1ex;"></description>
+ <vbox flex="1">
+ </vbox>
+ <hbox id="tbsync.spinner">
+ <label id="tbsync.spinner.label" value="" />
+ <image src="chrome://tbsync/content/skin/spinner.gif" style="margin-left:1em" width="16"
+ height="16" />
+ </hbox>
+ <vbox id="tbsync.error" style="width: auto;">
+ <description id="tbsync.error.message" flex="1" style="font-weight: bold;"></description>
+ <vbox>
+ <button id="tbsync.error.link" label="__DAV4TBSYNCMSG_manager.ShowEventLog__"
+ oncommand="TbSync.eventlog.open();" />
+ </vbox>
+ </vbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="thirdPage" label="__DAV4TBSYNCMSG_add.finish.title__">
+ <vbox flex="1">
+ <description style="width: auto; margin-top:1em;">__DAV4TBSYNCMSG_add.finish.description__
+ </description>
+ <html:table style="margin-top:1ex">
+ <html:tr flex="1">
+ <html:td width="33%">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.name__" /></vbox>
+ </html:td>
+ <html:td width="67%">
+ <html:input style="width: 90%; margin:2px" id="tbsync.finalaccount.name" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsync.newaccount.finaluser.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.user__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%; margin:2px; background-color:silver;" type="text"
+ id="tbsync.finalaccount.user" readonly="true" />
+ </html:td>
+ </html:tr>
+ <html:tr style="margin-bottom:2em;">
+ </html:tr>
+ <html:tr id="tbsyncfinalaccount.caldavserver.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.caldavserver__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%; margin:2px; background-color:silver;"
+ id="tbsync.finalaccount.caldavserver" readonly="true" />
+ </html:td>
+ </html:tr>
+ <html:tr id="tbsyncfinalaccount.carddavserver.row">
+ <html:td>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.carddavserver__" /></vbox>
+ </html:td>
+ <html:td>
+ <html:input style="width: 90%; margin:2px; background-color:silver;"
+ id="tbsync.finalaccount.carddavserver" readonly="true" />
+ </html:td>
+ </html:tr>
+ </html:table>
+ <description id="tbsync.finalaccount.details1" style="width: auto; margin-top:2em;">
+ __DAV4TBSYNCMSG_add.finish.details__</description>
+ </vbox>
+ </wizardpage>
+
+ </wizard>
+
+</window>
diff --git a/content/manager/editAccountOverlay.js b/content/manager/editAccountOverlay.js
new file mode 100644
index 0000000..8f32af8
--- /dev/null
+++ b/content/manager/editAccountOverlay.js
@@ -0,0 +1,46 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const dav = TbSync.providers.dav;
+
+var tbSyncEditAccountOverlay = {
+
+ onload: function (window, accountData) {
+ this.accountData = accountData;
+
+ let serviceprovider = this.accountData.getAccountProperty("serviceprovider");
+ let isServiceProvider = dav.sync.serviceproviders.hasOwnProperty(serviceprovider);
+
+ // special treatment for configuration label, which is a permanent setting and will not change by switching modes
+ let configlabel = window.document.getElementById("tbsync.accountsettings.label.config");
+ if (configlabel) {
+ let extra = "";
+ if (isServiceProvider) {
+ extra = " [" + TbSync.getString("add.serverprofile." + serviceprovider, "dav") + "]";
+ }
+ configlabel.setAttribute("value", TbSync.getString("config.custom", "dav") + extra);
+ }
+
+ //set certain elements as "alwaysDisable", if locked by service provider
+ if (isServiceProvider) {
+ let items = window.document.getElementsByClassName("lockIfServiceProvider");
+ for (let i=0; i < items.length; i++) {
+ items[i].setAttribute("alwaysDisabled", "true");
+ }
+ }
+ },
+
+ stripHost: function (document, field) {
+ let host = document.getElementById('tbsync.accountsettings.pref.' + field).value;
+ while (host.endsWith("/")) { host = host.slice(0,-1); }
+ document.getElementById('tbsync.accountsettings.pref.' + field).value = host
+ this.accountData.setAccountProperty(field, host);
+ }
+};
diff --git a/content/manager/editAccountOverlay.xhtml b/content/manager/editAccountOverlay.xhtml
new file mode 100644
index 0000000..bee8d72
--- /dev/null
+++ b/content/manager/editAccountOverlay.xhtml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://tbsync/content/skin/fix_dropdown_1534697.css" type="text/css"?>
+<overlay
+ id="tbSyncAccountConfig"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/manager/editAccountOverlay.js"/>
+ <script type="application/javascript" src="chrome://dav4tbsync/content/locales.js"/>
+
+ <tab id="manager.tabs.accountsettings" label="__DAV4TBSYNCMSG_manager.tabs.accountsettings__" appendto="manager.tabs" />
+ <tab id="manager.tabs.syncsettings" label="__DAV4TBSYNCMSG_manager.tabs.syncsettings__" appendto="manager.tabs" />
+
+ <tabpanel id="manager.tabpanels.accountsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- ACCOUNT SETTINGS -->
+ <vbox flex="1">
+ <label class="header lockIfConnected" style="margin-left:0; margin-bottom:1ex;" value="" id="tbsync.accountsettings.label.config" />
+ <html:table>
+
+ <html:tr>
+ <html:td>
+ <vbox pack="center">
+ <label style="text-align:left" control="tbsync.accountsettings.pref.accountname" value="__DAV4TBSYNCMSG_pref.AccountName__" />
+ </vbox>
+ </html:td>
+ <html:td width="100%">
+ <html:input id="tbsync.accountsettings.pref.accountname" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td>
+ <vbox pack="center">
+ <label class="lockIfConnected" style="text-align:left" control="tbsync.accountsettings.pref.user" value="__DAV4TBSYNCMSG_pref.UserName__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <html:input class="lockIfConnected" id="tbsync.accountsettings.pref.user" />
+ </html:td>
+ </html:tr>
+
+ <html:tr>
+ <html:td>
+ <vbox pack="center">
+ <label class="lockIfConnected lockIfServiceProvider" style="text-align:left" control="tbsync.accountsettings.pref.calDavHost" value="__DAV4TBSYNCMSG_pref.CalDavServer__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <html:input class="lockIfConnected lockIfServiceProvider" id="tbsync.accountsettings.pref.calDavHost" onblur="tbSyncEditAccountOverlay.stripHost(document, 'calDavHost');"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:td>
+ <vbox pack="center">
+ <label class="lockIfConnected lockIfServiceProvider" style="text-align:left" control="tbsync.accountsettings.pref.cardDavHost" value="__DAV4TBSYNCMSG_pref.CardDavServer__" />
+ </vbox>
+ </html:td>
+ <html:td>
+ <html:input class="lockIfConnected lockIfServiceProvider" id="tbsync.accountsettings.pref.cardDavHost" onblur="tbSyncEditAccountOverlay.stripHost(document, 'cardDavHost');" />
+ </html:td>
+ </html:tr>
+
+ </html:table>
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+
+ </vbox>
+ </tabpanel>
+
+ <tabpanel id="manager.tabpanels.syncsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- SYNC SETTINGS -->
+ <vbox flex="1">
+ <!--label style="margin-left:0; margin-bottom: 1ex;" class="header lockIfConnected" value="__DAV4TBSYNCMSG_pref.generaloptions__"/-->
+
+ <label style="margin-left:0; margin-bottom: 1ex; margin-top: 3ex" class="header lockIfConnected" value="__DAV4TBSYNCMSG_pref.calendaroptions__"/>
+ <vbox>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.useCalendarCache" label="__DAV4TBSYNCMSG_pref.useCalendarCache__" />
+ </vbox>
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+
+</overlay>
diff --git a/content/provider.js b/content/provider.js
new file mode 100644
index 0000000..dfd8906
--- /dev/null
+++ b/content/provider.js
@@ -0,0 +1,691 @@
+/*
+ * This file is part of DAV-4-TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+// check if getItem returns an array because of recursions!
+
+// Every object in here will be loaded into TbSync.providers.<providername>.
+const dav = TbSync.providers.dav;
+
+/**
+ * Implementing the TbSync interface for external provider extensions.
+ */
+
+var Base = class {
+ /**
+ * Called during load of external provider extension to init provider.
+ */
+ static async load() {
+ // Set default prefs
+ let branch = Services.prefs.getDefaultBranch("extensions.dav4tbsync.");
+ branch.setIntPref("maxitems", 50);
+ branch.setIntPref("timeout", 90000);
+ branch.setCharPref("clientID.type", "TbSync");
+ branch.setCharPref("clientID.useragent", "Thunderbird CalDAV/CardDAV");
+ branch.setBoolPref("enforceUniqueCalendarUrls", false);
+
+ dav.openWindows = {};
+ }
+
+
+ /**
+ * Called during unload of external provider extension to unload provider.
+ */
+ static async unload() {
+ // Close all open windows of this provider.
+ for (let id in dav.openWindows) {
+ if (dav.openWindows.hasOwnProperty(id)) {
+ try {
+ dav.openWindows[id].close();
+ } catch (e) {
+ //NOOP
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns string for the name of provider for the add account menu.
+ */
+ static getProviderName() {
+ return TbSync.getString("menu.name", "dav");
+ }
+
+
+ /**
+ * Returns version of the TbSync API this provider is using
+ */
+ static getApiVersion() { return "2.5"; }
+
+
+
+ /**
+ * Returns location of a provider icon.
+ */
+ static getProviderIcon(size, accountData = null) {
+ let root = "sabredav";
+ if (accountData) {
+ let serviceprovider = accountData.getAccountProperty("serviceprovider");
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider)) {
+ root = dav.sync.serviceproviders[serviceprovider].icon;
+ }
+ }
+
+ switch (size) {
+ case 16:
+ return "chrome://dav4tbsync/content/skin/"+root+"16.png";
+ case 32:
+ return "chrome://dav4tbsync/content/skin/"+root+"32.png";
+ default :
+ return "chrome://dav4tbsync/content/skin/"+root+"48.png";
+ }
+ }
+
+
+ /**
+ * Returns a list of sponsors, they will be sorted by the index
+ */
+ static getSponsors() {
+ return {
+ "Thoben, Marc" : {name: "Marc Thoben", description: "Zimbra", icon: "", link: "" },
+ "Biebl, Michael" : {name: "Michael Biebl", description: "Nextcloud", icon: "", link: "" },
+ "László, Kovács" : {name: "Kovács László", description : "Radicale", icon: "", link: "" },
+ "Lütticke, David" : {name: "David Lütticke", description : "", icon: "", link: "" },
+ };
+ }
+
+
+ /**
+ * Returns the url of a page with details about contributors (used in the manager UI)
+ */
+ static getContributorsUrl() {
+ return "https://github.com/jobisoft/DAV-4-TbSync/blob/master/CONTRIBUTORS.md";
+ }
+
+
+ /**
+ * Returns the email address of the maintainer (used for bug reports).
+ */
+ static getMaintainerEmail() {
+ return "john.bieling@gmx.de";
+ }
+
+
+ /**
+ * Returns URL of the new account window.
+ *
+ * The URL will be opened via openDialog(), when the user wants to create a
+ * new account of this provider.
+ */
+ static getCreateAccountWindowUrl() {
+ return "chrome://dav4tbsync/content/manager/createAccount.xhtml";
+ }
+
+
+ /**
+ * Returns overlay XUL URL of the edit account dialog
+ * (chrome://tbsync/content/manager/editAccount.xhtml)
+ */
+ static getEditAccountOverlayUrl() {
+ return "chrome://dav4tbsync/content/manager/editAccountOverlay.xhtml";
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the
+ * accounts database with the default value if not yet stored in the
+ * database.
+ */
+ static getDefaultAccountEntries() {
+ let row = {
+ "useCalendarCache" : true,
+ "calDavHost" : "",
+ "cardDavHost" : "",
+ // these must return null if not defined
+ "calDavPrincipal" : null,
+ "cardDavPrincipal" : null,
+
+ "calDavOptions" : [],
+ "cardDavOptions" : [],
+
+ "serviceprovider" : "",
+ "serviceproviderRevision" : 0,
+
+ "user" : "",
+ "https" : true, //deprecated, because this is part of the URL now
+ "createdWithProviderVersion" : "0",
+ };
+ return row;
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the folder
+ * database with the default value if not yet stored in the database.
+ */
+ static getDefaultFolderEntries() {
+ let folder = {
+ // different folders (caldav/carddav) can be stored on different
+ // servers (as with yahoo, icloud, gmx, ...), so we need to store
+ // the fqdn information per folders
+ "href" : "",
+ "https" : true,
+ "fqdn" : "",
+
+ "url" : "", // used by calendar to store the full url of this cal
+
+ "type" : "", //caldav, carddav or ics
+ "shared": false, //identify shared resources
+ "acl": "", //acl send from server
+ "target" : "",
+ "targetColor" : "",
+ "targetName" : "",
+ "ctag" : "",
+ "token" : "",
+ "createdWithProviderVersion" : "0",
+ "supportedCalComponent" : []
+ };
+ return folder;
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is enabled in the
+ * manager UI.
+ */
+ static onEnableAccount(accountData) {
+ accountData.resetAccountProperty("calDavPrincipal");
+ accountData.resetAccountProperty("cardDavPrincipal");
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is disabled in the
+ * manager UI.
+ */
+ static onDisableAccount(accountData) {
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is deleted in the
+ * manager UI.
+ */
+ static onDeleteAccount(accountData) {
+ dav.network.getAuthData(accountData).removeLoginData();
+ }
+
+
+ /**
+ * Returns all folders of the account, sorted in the desired order.
+ * The most simple implementation is to return accountData.getAllFolders();
+ */
+ static getSortedFolders(accountData) {
+ let folders = accountData.getAllFolders();
+
+ // we can only sort arrays, so we create an array of objects which must
+ // contain the sort key and the associated folder
+ let toBeSorted = [];
+ for (let folder of folders) {
+ let t = 100;
+ let comp = folder.getFolderProperty("supportedCalComponent");
+ switch (folder.getFolderProperty("type")) {
+ case "carddav":
+ t+=0;
+ break;
+ case "caldav":
+ t+=10;
+ if (comp.length > 0 && !comp.includes("VEVENT") && comp.includes("VTODO")) t+=5;
+ break;
+ case "ics":
+ t+=20;
+ break;
+ default:
+ t+=90;
+ break;
+ }
+
+ if (folder.getFolderProperty("shared")) {
+ t+=100;
+ }
+
+ toBeSorted.push({"key": t.toString() + folder.getFolderProperty("foldername"), "folder": folder});
+ }
+
+ //sort
+ toBeSorted.sort(function(a,b) {
+ return a.key > b.key;
+ });
+
+ let sortedFolders = [];
+ for (let sortObj of toBeSorted) {
+ sortedFolders.push(sortObj.folder);
+ }
+ return sortedFolders;
+ }
+
+
+ /**
+ * Return the connection timeout for an active sync, so TbSync can append
+ * a countdown to the connection timeout, while waiting for an answer from
+ * the server. Only syncstates which start with "send." will trigger this.
+ */
+ static getConnectionTimeout(accountData) {
+ return dav.sync.prefSettings.getIntPref("timeout");
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize the folder list.
+ */
+ static async syncFolderList(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ try {
+ await dav.sync.folderList(syncData);
+ } catch (e) {
+ if (e.name == "dav4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // we fall through, if there was no error
+ return new TbSync.StatusData();
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize a folder.
+ */
+ static async syncFolder(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ // Process a single folder.
+ try {
+ await dav.sync.folder(syncData);
+ } catch (e) {
+ if (e.name == "dav4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // we fall through, if there was no error
+ return new TbSync.StatusData();
+ }
+}
+
+
+
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced address book TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_addressbook = class extends TbSync.addressbook.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+
+ // enable or disable changelog
+ get logUserChanges() {
+ return false;
+ }
+
+ directoryObserver(aTopic) {
+ switch (aTopic) {
+ case "addrbook-directory-deleted":
+ case "addrbook-directory-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] " + this.folderData.getFolderProperty("foldername"));
+ break;
+ }
+ }
+
+ cardObserver(aTopic, abCardItem) {
+ switch (aTopic) {
+ case "addrbook-contact-updated":
+ case "addrbook-contact-deleted":
+ case "addrbook-contact-created":
+ //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName"));
+ break;
+ }
+ }
+
+ listObserver(aTopic, abListItem, abListMember) {
+ switch (aTopic) {
+ case "addrbook-list-member-added":
+ case "addrbook-list-member-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-list-deleted":
+ case "addrbook-list-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName"));
+ break;
+
+ case "addrbook-list-created":
+ //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for List <"+abListItem.getProperty("ListName")+">");
+ break;
+ }
+ }
+
+ async createAddressbook(newname) {
+ let authData = dav.network.getAuthData(this.folderData.accountData);
+
+ let baseUrl = "http" + (this.folderData.getFolderProperty("https") ? "s" : "") + "://" + this.folderData.getFolderProperty("fqdn");
+ let url = dav.tools.parseUri(baseUrl + this.folderData.getFolderProperty("href") + (dav.sync.prefSettings.getBoolPref("enforceUniqueCalendarUrls") ? "?" + this.folderData.accountID : ""));
+ this.folderData.setFolderProperty("url", url.spec);
+
+ const getDirectory = (url) => {
+ // Check if that directory exists already.
+ for (let ab of MailServices.ab.directories) {
+ if (ab.dirType == Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE && ab.getStringValue("carddav.url","") == url.spec) {
+ return ab;
+ }
+ }
+ let dirPrefId = MailServices.ab.newAddressBook(
+ newname,
+ null,
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE,
+ null
+ );
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ directory.setStringValue("carddav.url", url.spec);
+ return directory;
+ }
+
+ let directory = getDirectory(url);
+ if (!directory || !(directory instanceof Components.interfaces.nsIAbDirectory)) {
+ return null;
+ }
+
+ // Setup password for CardDAV address book, so users do not get prompted.
+ directory.setStringValue("carddav.username", authData.username);
+ if (this.folderData.getFolderProperty("downloadonly")) {
+ directory.setBoolValue("readOnly", true);
+ }
+ TbSync.dump("Searching CardDAV authRealm for", url.host);
+ let connectionData = new dav.network.ConnectionData(this.folderData);
+ await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:resourcetype /><d:displayname /></d:prop></d:propfind>", url.spec , "PROPFIND", connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {containerRealm: "setup", containerReset: true, passwordRetries: 0});
+
+ let realm = connectionData.realm || "";
+ if (realm !== "") {
+ TbSync.dump("Adding CardDAV password", "User <"+authData.username+">, Realm <"+realm+">");
+ // Manually create a CardDAV style entry in the password manager.
+ TbSync.passwordManager.updateLoginInfo(
+ url.prePath, realm,
+ /* old */ authData.username,
+ /* new */ authData.username,
+ authData.password
+ );
+ }
+
+ dav.sync.resetFolderSyncInfo(this.folderData);
+
+ /*
+ // Since icons are no longer supported, lets disable this for 102.
+ let serviceprovider = this.folderData.accountData.getAccountProperty("serviceprovider");
+ let icon = "custom";
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider)) {
+ icon = dav.sync.serviceproviders[serviceprovider].icon;
+ }
+ directory.setStringValue("tbSyncIcon", "dav" + icon);
+ */
+
+ return directory;
+ }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced calendar TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_calendar = class extends TbSync.lightning.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+ // The calendar target does not support a custom primaryKeyField, because
+ // the lightning implementation only allows to search for items via UID.
+ // Like the addressbook target, the calendar target item element has a
+ // primaryKey getter/setter which - however - only works on the UID.
+
+ // enable or disable changelog
+ get logUserChanges(){
+ return false;
+ }
+
+ calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) {
+ switch (aTopic) {
+ case "onCalendarPropertyChanged":
+ {
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName);
+ switch (aPropertyName) {
+ case "color":
+ if (aOldPropertyValue.toString().toUpperCase() != aPropertyValue.toString().toUpperCase()) {
+ //prepare connection data
+ let connection = new dav.network.ConnectionData(this.folderData);
+ //update color on server
+ dav.network.sendRequest("<d:propertyupdate "+dav.tools.xmlns(["d","apple"])+"><d:set><d:prop><apple:calendar-color>"+(aPropertyValue + "FFFFFFFF").slice(0,9)+"</apple:calendar-color></d:prop></d:set></d:propertyupdate>", this.folderData.getFolderProperty("href"), "PROPPATCH", connection);
+ }
+ break;
+ }
+ }
+ break;
+
+ case "onCalendarDeleted":
+ case "onCalendarPropertyDeleted":
+ //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name);
+ break;
+ }
+ }
+
+ itemObserver(aTopic, tbItem, tbOldItem) {
+ switch (aTopic) {
+ case "onAddItem":
+ case "onModifyItem":
+ case "onDeleteItem":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title);
+ break;
+ }
+ }
+
+ async createCalendar(newname) {
+ let calManager = TbSync.lightning.cal.manager;
+ let authData = dav.network.getAuthData(this.folderData.accountData);
+
+ let caltype = this.folderData.getFolderProperty("type");
+
+ let baseUrl = "";
+ if (caltype == "caldav") {
+ baseUrl = "http" + (this.folderData.getFolderProperty("https") ? "s" : "") + "://" + this.folderData.getFolderProperty("fqdn");
+ }
+
+ let url = dav.tools.parseUri(baseUrl + this.folderData.getFolderProperty("href") + (dav.sync.prefSettings.getBoolPref("enforceUniqueCalendarUrls") ? "?" + this.folderData.accountID : ""));
+ this.folderData.setFolderProperty("url", url.spec);
+
+ // Check if that calendar already exists.
+ let cals = calManager.getCalendars({});
+ let newCalendar = null;
+ let found = false;
+ for (let calendar of calManager.getCalendars({})) {
+ if (calendar.uri.spec == url.spec) {
+ newCalendar = calendar;
+ found = true;
+ break;
+ }
+ }
+
+
+ if (found) {
+ newCalendar.setProperty("username", authData.username);
+ newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor"));
+ newCalendar.name = newname;
+ } else {
+ newCalendar = calManager.createCalendar(caltype, url); // caldav or ics
+ newCalendar.id = TbSync.lightning.cal.getUUID();
+ newCalendar.name = newname;
+
+ newCalendar.setProperty("username", authData.username);
+ newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor"));
+ // removed in TB78, as it seems to not fully enable the calendar, if present before registering
+ // https://searchfox.org/comm-central/source/calendar/base/content/calendar-management.js#385
+ //newCalendar.setProperty("calendar-main-in-composite",true);
+ newCalendar.setProperty("cache.enabled", this.folderData.accountData.getAccountProperty("useCalendarCache"));
+ }
+
+ if (this.folderData.getFolderProperty("downloadonly")) newCalendar.setProperty("readOnly", true);
+
+ let comp = this.folderData.getFolderProperty("supportedCalComponent");
+ if (comp.length > 0 && !comp.includes("VTODO")) {
+ newCalendar.setProperty("capabilities.tasks.supported", false);
+ }
+ if (comp.length > 0 && !comp.includes("VEVENT")) {
+ newCalendar.setProperty("capabilities.events.supported", false);
+ }
+
+ // Setup password for CalDAV calendar, so users do not get prompted (ICS urls do not need a password).
+ if (caltype == "caldav") {
+ TbSync.dump("Searching CalDAV authRealm for", url.host);
+ let connectionData = new dav.network.ConnectionData(this.folderData);
+ await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:resourcetype /><d:displayname /></d:prop></d:propfind>", url.spec , "PROPFIND", connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {containerRealm: "setup", containerReset: true, passwordRetries: 0});
+
+ let realm = connectionData.realm || "";
+ if (realm !== "") {
+ TbSync.dump("Adding CalDAV password", "User <"+authData.username+">, Realm <"+realm+">");
+ // Manually create a CalDAV style entry in the password manager.
+ TbSync.passwordManager.updateLoginInfo(
+ url.prePath, realm,
+ /* old */ authData.username,
+ /* new */ authData.username,
+ authData.password
+ );
+ }
+ }
+
+ if (!found) {
+ calManager.registerCalendar(newCalendar);
+ }
+ return newCalendar;
+ }
+}
+
+
+
+
+
+/**
+ * This provider is implementing the StandardFolderList class instead of
+ * the FolderList class.
+ */
+var StandardFolderList = class {
+ /**
+ * Is called before the context menu of the folderlist is shown, allows to
+ * show/hide custom menu options based on selected folder. During an active
+ * sync, folderData will be null.
+ */
+ static onContextMenuShowing(window, folderData) {
+ }
+
+
+ /**
+ * Return the icon used in the folderlist to represent the different folder
+ * types.
+ */
+ static getTypeImage(folderData) {
+ let src = "";
+ switch (folderData.getFolderProperty("type")) {
+ case "carddav":
+ if (folderData.getFolderProperty("shared")) {
+ return "chrome://tbsync/content/skin/contacts16_shared.png";
+ } else {
+ return "chrome://tbsync/content/skin/contacts16.png";
+ }
+ case "caldav":
+ let comp = folderData.getFolderProperty("supportedCalComponent");
+ if (folderData.getFolderProperty("shared")) {
+ return (comp.length > 0 && comp.includes("VTODO") && !comp.includes("VEVENT"))
+ ? "chrome://tbsync/content/skin/todo16_shared.png"
+ : "chrome://tbsync/content/skin/calendar16_shared.png"
+ } else {
+ return (comp.length > 0 && comp.includes("VTODO") && !comp.includes("VEVENT"))
+ ? "chrome://tbsync/content/skin/todo16.png"
+ : "chrome://tbsync/content/skin/calendar16.png"
+ }
+ case "ics":
+ return "chrome://dav4tbsync/content/skin/ics16.png";
+ }
+ }
+
+
+ /**
+ * Return the name of the folder shown in the folderlist.
+ */
+ static getFolderDisplayName(folderData) {
+ return folderData.getFolderProperty("foldername");
+ }
+
+
+ /**
+ * Return the attributes for the ACL RO (readonly) menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values. If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRoAcl(folderData) {
+ return {
+ label: TbSync.getString("acl.readonly", "dav"),
+ };
+ }
+
+
+ /**
+ * Return the attributes for the ACL RW (readwrite) menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values. If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRwAcl(folderData) {
+ let acl = parseInt(folderData.getFolderProperty("acl"));
+ let acls = [];
+ if (acl & 0x2) acls.push(TbSync.getString("acl.modify", "dav"));
+ if (acl & 0x4) acls.push(TbSync.getString("acl.add", "dav"));
+ if (acl & 0x8) acls.push(TbSync.getString("acl.delete", "dav"));
+ if (acls.length == 0) acls.push(TbSync.getString("acl.none", "dav"));
+
+ return {
+ label: TbSync.getString("acl.readwrite::"+acls.join(", "), "dav"),
+ disabled: (acl & 0x7) != 0x7,
+ }
+ }
+}
+
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/sync.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/tools.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/network.js", this, "UTF-8");
diff --git a/content/skin/ab.css b/content/skin/ab.css
new file mode 100644
index 0000000..a1398da
--- /dev/null
+++ b/content/skin/ab.css
@@ -0,0 +1,82 @@
+treechildren::-moz-tree-image(DirCol, davgoogle) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/google16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davweb) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/web16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davfruux) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/fruux16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davposteo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/posteo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davmbo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/mbo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davicloud) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/icloud16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davyahoo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/yahoo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davgmx) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/gmx16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davcustom) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/sabredav16.png");
+}
+
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davgoogle"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/google16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davweb"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/web16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davfruux"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/fruux16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davposteo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/posteo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davmbo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/mbo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davicloud"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/icloud16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davyahoo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/yahoo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davgmx"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/gmx16.png");
+}
+
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davcustom"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/sabredav16.png");
+}
diff --git a/content/skin/arrow.down10.png b/content/skin/arrow.down10.png
new file mode 100644
index 0000000..82eccb5
--- /dev/null
+++ b/content/skin/arrow.down10.png
Binary files differ
diff --git a/content/skin/arrow.up10.png b/content/skin/arrow.up10.png
new file mode 100644
index 0000000..db87a3d
--- /dev/null
+++ b/content/skin/arrow.up10.png
Binary files differ
diff --git a/content/skin/dragdrop.png b/content/skin/dragdrop.png
new file mode 100644
index 0000000..a3797da
--- /dev/null
+++ b/content/skin/dragdrop.png
Binary files differ
diff --git a/content/skin/fruux16.png b/content/skin/fruux16.png
new file mode 100644
index 0000000..61d25f2
--- /dev/null
+++ b/content/skin/fruux16.png
Binary files differ
diff --git a/content/skin/fruux32.png b/content/skin/fruux32.png
new file mode 100644
index 0000000..2d37e6b
--- /dev/null
+++ b/content/skin/fruux32.png
Binary files differ
diff --git a/content/skin/fruux48.png b/content/skin/fruux48.png
new file mode 100644
index 0000000..c2b9aca
--- /dev/null
+++ b/content/skin/fruux48.png
Binary files differ
diff --git a/content/skin/gmx16.png b/content/skin/gmx16.png
new file mode 100644
index 0000000..1672e4a
--- /dev/null
+++ b/content/skin/gmx16.png
Binary files differ
diff --git a/content/skin/gmx32.png b/content/skin/gmx32.png
new file mode 100644
index 0000000..bb2dd43
--- /dev/null
+++ b/content/skin/gmx32.png
Binary files differ
diff --git a/content/skin/gmx48.png b/content/skin/gmx48.png
new file mode 100644
index 0000000..d23c125
--- /dev/null
+++ b/content/skin/gmx48.png
Binary files differ
diff --git a/content/skin/icloud16.png b/content/skin/icloud16.png
new file mode 100644
index 0000000..4399f6a
--- /dev/null
+++ b/content/skin/icloud16.png
Binary files differ
diff --git a/content/skin/icloud32.png b/content/skin/icloud32.png
new file mode 100644
index 0000000..6691ebc
--- /dev/null
+++ b/content/skin/icloud32.png
Binary files differ
diff --git a/content/skin/icloud48.png b/content/skin/icloud48.png
new file mode 100644
index 0000000..2171c1d
--- /dev/null
+++ b/content/skin/icloud48.png
Binary files differ
diff --git a/content/skin/ics16.png b/content/skin/ics16.png
new file mode 100644
index 0000000..7fc8ab6
--- /dev/null
+++ b/content/skin/ics16.png
Binary files differ
diff --git a/content/skin/mbo16.png b/content/skin/mbo16.png
new file mode 100644
index 0000000..bbf127f
--- /dev/null
+++ b/content/skin/mbo16.png
Binary files differ
diff --git a/content/skin/mbo32.png b/content/skin/mbo32.png
new file mode 100644
index 0000000..ee86331
--- /dev/null
+++ b/content/skin/mbo32.png
Binary files differ
diff --git a/content/skin/mbo48.png b/content/skin/mbo48.png
new file mode 100644
index 0000000..90c62dc
--- /dev/null
+++ b/content/skin/mbo48.png
Binary files differ
diff --git a/content/skin/posteo16.png b/content/skin/posteo16.png
new file mode 100644
index 0000000..922078b
--- /dev/null
+++ b/content/skin/posteo16.png
Binary files differ
diff --git a/content/skin/posteo32.png b/content/skin/posteo32.png
new file mode 100644
index 0000000..b296393
--- /dev/null
+++ b/content/skin/posteo32.png
Binary files differ
diff --git a/content/skin/posteo48.png b/content/skin/posteo48.png
new file mode 100644
index 0000000..a1f015d
--- /dev/null
+++ b/content/skin/posteo48.png
Binary files differ
diff --git a/content/skin/sabredav16.png b/content/skin/sabredav16.png
new file mode 100644
index 0000000..fda1f68
--- /dev/null
+++ b/content/skin/sabredav16.png
Binary files differ
diff --git a/content/skin/sabredav32.png b/content/skin/sabredav32.png
new file mode 100644
index 0000000..b0e80d6
--- /dev/null
+++ b/content/skin/sabredav32.png
Binary files differ
diff --git a/content/skin/sabredav48.png b/content/skin/sabredav48.png
new file mode 100644
index 0000000..48a9739
--- /dev/null
+++ b/content/skin/sabredav48.png
Binary files differ
diff --git a/content/skin/type.car10.png b/content/skin/type.car10.png
new file mode 100644
index 0000000..ca12b71
--- /dev/null
+++ b/content/skin/type.car10.png
Binary files differ
diff --git a/content/skin/type.car16.png b/content/skin/type.car16.png
new file mode 100644
index 0000000..db6fb9d
--- /dev/null
+++ b/content/skin/type.car16.png
Binary files differ
diff --git a/content/skin/type.cell10.png b/content/skin/type.cell10.png
new file mode 100644
index 0000000..ef5ebac
--- /dev/null
+++ b/content/skin/type.cell10.png
Binary files differ
diff --git a/content/skin/type.cell16.png b/content/skin/type.cell16.png
new file mode 100644
index 0000000..0716347
--- /dev/null
+++ b/content/skin/type.cell16.png
Binary files differ
diff --git a/content/skin/type.fax10.png b/content/skin/type.fax10.png
new file mode 100644
index 0000000..276f0d1
--- /dev/null
+++ b/content/skin/type.fax10.png
Binary files differ
diff --git a/content/skin/type.fax16.png b/content/skin/type.fax16.png
new file mode 100644
index 0000000..4b9a92c
--- /dev/null
+++ b/content/skin/type.fax16.png
Binary files differ
diff --git a/content/skin/type.home10.png b/content/skin/type.home10.png
new file mode 100644
index 0000000..8a72f0f
--- /dev/null
+++ b/content/skin/type.home10.png
Binary files differ
diff --git a/content/skin/type.home16.png b/content/skin/type.home16.png
new file mode 100644
index 0000000..84d829b
--- /dev/null
+++ b/content/skin/type.home16.png
Binary files differ
diff --git a/content/skin/type.nopref.png b/content/skin/type.nopref.png
new file mode 100644
index 0000000..27caa40
--- /dev/null
+++ b/content/skin/type.nopref.png
Binary files differ
diff --git a/content/skin/type.other10.png b/content/skin/type.other10.png
new file mode 100644
index 0000000..d05fc5f
--- /dev/null
+++ b/content/skin/type.other10.png
Binary files differ
diff --git a/content/skin/type.other16.png b/content/skin/type.other16.png
new file mode 100644
index 0000000..60a169b
--- /dev/null
+++ b/content/skin/type.other16.png
Binary files differ
diff --git a/content/skin/type.pager10.png b/content/skin/type.pager10.png
new file mode 100644
index 0000000..5c6fbe7
--- /dev/null
+++ b/content/skin/type.pager10.png
Binary files differ
diff --git a/content/skin/type.pager16.png b/content/skin/type.pager16.png
new file mode 100644
index 0000000..6a0f2b1
--- /dev/null
+++ b/content/skin/type.pager16.png
Binary files differ
diff --git a/content/skin/type.pref.png b/content/skin/type.pref.png
new file mode 100644
index 0000000..409a30c
--- /dev/null
+++ b/content/skin/type.pref.png
Binary files differ
diff --git a/content/skin/type.video10.png b/content/skin/type.video10.png
new file mode 100644
index 0000000..188cabc
--- /dev/null
+++ b/content/skin/type.video10.png
Binary files differ
diff --git a/content/skin/type.video16.png b/content/skin/type.video16.png
new file mode 100644
index 0000000..afb8c7c
--- /dev/null
+++ b/content/skin/type.video16.png
Binary files differ
diff --git a/content/skin/type.voice10.png b/content/skin/type.voice10.png
new file mode 100644
index 0000000..3ed48b6
--- /dev/null
+++ b/content/skin/type.voice10.png
Binary files differ
diff --git a/content/skin/type.voice16.png b/content/skin/type.voice16.png
new file mode 100644
index 0000000..debb016
--- /dev/null
+++ b/content/skin/type.voice16.png
Binary files differ
diff --git a/content/skin/type.work10.png b/content/skin/type.work10.png
new file mode 100644
index 0000000..10db4e7
--- /dev/null
+++ b/content/skin/type.work10.png
Binary files differ
diff --git a/content/skin/type.work16.png b/content/skin/type.work16.png
new file mode 100644
index 0000000..de3036f
--- /dev/null
+++ b/content/skin/type.work16.png
Binary files differ
diff --git a/content/skin/web16.png b/content/skin/web16.png
new file mode 100644
index 0000000..3a738a5
--- /dev/null
+++ b/content/skin/web16.png
Binary files differ
diff --git a/content/skin/web32.png b/content/skin/web32.png
new file mode 100644
index 0000000..234e2f8
--- /dev/null
+++ b/content/skin/web32.png
Binary files differ
diff --git a/content/skin/web48.png b/content/skin/web48.png
new file mode 100644
index 0000000..6909c9e
--- /dev/null
+++ b/content/skin/web48.png
Binary files differ
diff --git a/content/skin/yahoo16.png b/content/skin/yahoo16.png
new file mode 100644
index 0000000..7225a3a
--- /dev/null
+++ b/content/skin/yahoo16.png
Binary files differ
diff --git a/content/skin/yahoo32.png b/content/skin/yahoo32.png
new file mode 100644
index 0000000..054ec55
--- /dev/null
+++ b/content/skin/yahoo32.png
Binary files differ
diff --git a/content/skin/yahoo48.png b/content/skin/yahoo48.png
new file mode 100644
index 0000000..bfd7d31
--- /dev/null
+++ b/content/skin/yahoo48.png
Binary files differ