summaryrefslogtreecommitdiffstats
path: root/content/includes/sync.js
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/includes/sync.js
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/includes/sync.js')
-rw-r--r--content/includes/sync.js462
1 files changed, 462 insertions, 0 deletions
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;
+ }
+ },
+
+}