/*
/*
* 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 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"},
},
onChange(abItem) {
if (!this._syncOnChangeTimers)
this._syncOnChangeTimers = {};
this._syncOnChangeTimers[abItem.abDirectory.UID] = {};
this._syncOnChangeTimers[abItem.abDirectory.UID].timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
this._syncOnChangeTimers[abItem.abDirectory.UID].event = {
notify: function(timer) {
// if account is syncing, re-schedule
// if folder got synced after the start time (due to re-scheduling) abort
console.log("DONE: "+ abItem.abDirectory.UID);
}
}
this._syncOnChangeTimers[abItem.abDirectory.UID].timer.initWithCallback(
this._syncOnChangeTimers[abItem.abDirectory.UID].event,
2000,
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
},
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("", 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 = "<"+job+":" + homeset + " />"
+ (job == "cal" && options.includes("calendar-proxy") ? "" : "")
+ ""
+ "";
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("",""), 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")
? ""
: "";
//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"]]);
//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("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":
{
await dav.sync.singleFolder(syncData);
}
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;
}
},
singleFolder: async function (syncData) {
let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly");
// we have to abort sync of this folder, if it is contact, has groupSync enabled and gContactSync is enabled
let syncGroups = syncData.accountData.getAccountProperty("syncGroups");
let gContactSync = await AddonManager.getAddonByID("gContactSync@pirules.net") ;
let contactSync = (syncData.currentFolderData.getFolderProperty("type") == "carddav");
if (syncGroups && contactSync && gContactSync && gContactSync.isActive) {
throw dav.sync.finish("warning", "gContactSync");
}
await dav.sync.remoteChanges(syncData);
let numOfLocalChanges = await dav.sync.localChanges(syncData);
//revert all local changes on permission error by doing a clean sync
if (numOfLocalChanges < 0) {
dav.sync.resetFolderSyncInfo(syncData.currentFolderData);
await dav.sync.remoteChanges(syncData);
if (!downloadonly) throw dav.sync.finish("info", "info.restored");
} else if (numOfLocalChanges > 0){
//we will get back our own changes and can store etags and vcards and also get a clean ctag/token
await dav.sync.remoteChanges(syncData);
}
},
remoteChanges: async function (syncData) {
//Do we have a sync token? No? -> Initial Sync (or WebDAV sync not supported) / Yes? -> Get updates only (token only present if WebDAV sync is suported)
let token = syncData.currentFolderData.getFolderProperty("token");
if (token) {
//update via token sync
let tokenSyncSucceeded = await dav.sync.remoteChangesByTOKEN(syncData);
if (tokenSyncSucceeded) return;
//token sync failed, reset ctag and token and do a full sync
dav.sync.resetFolderSyncInfo(syncData.currentFolderData);
}
//Either token sync did not work or there is no token (initial sync)
//loop until ctag is the same before and after polling data (sane start condition)
let maxloops = 20;
for (let i=0; i <= maxloops; i++) {
if (i == maxloops)
throw dav.sync.finish("warning", "could-not-get-stable-ctag");
let ctagChanged = await dav.sync.remoteChangesByCTAG(syncData);
if (!ctagChanged) break;
}
},
remoteChangesByTOKEN: async function (syncData) {
syncData.progressData.reset();
let token = syncData.currentFolderData.getFolderProperty("token");
syncData.setSyncState("send.request.remotechanges");
let cards = await dav.network.sendRequest(""+token+"1", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {}, {softfail: [415,403,409]});
//EteSync throws 409 because it does not support sync-token
//Sabre\DAV\Exception\ReportNotSupported - Unsupported media type - returned by fruux if synctoken is 0 (empty book), 415 & 403
//https://github.com/sabre-io/dav/issues/1075
//Sabre\DAV\Exception\InvalidSyncToken (403)
if (cards && cards.softerror) {
//token sync failed, reset ctag and do a full sync
return false;
}
let tokenNode = dav.tools.evaluateNode(cards.node, [["d", "sync-token"]]);
if (tokenNode === null) {
//token sync failed, reset ctag and do a full sync
return false;
}
let vCardsDeletedOnServer = [];
let vCardsChangedOnServer = {};
let localDeletes = syncData.target.getDeletedItemsFromChangeLog();
let cardsFound = 0;
for (let c=0; c < cards.multi.length; c++) {
let id = cards.multi[c].href;
if (id !==null) {
//valid
let card = await syncData.target.getItemFromProperty("X-DAV-HREF", id);
if (cards.multi[c].status == "200") {
//MOD or ADD
let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
if (!card) {
//if the user deleted this card (not yet send to server), do not add it again
if (!localDeletes.includes(id)) {
cardsFound++;
vCardsChangedOnServer[id] = "ADD";
}
} else if (etag.textContent != card.getProperty("X-DAV-ETAG")) {
cardsFound++;
vCardsChangedOnServer[id] = "MOD";
}
} else if (cards.multi[c].responsestatus == "404" && card) {
//DEL
cardsFound++;
vCardsDeletedOnServer.push(card);
} else {
//We received something, that is not a DEL, MOD or ADD
TbSync.eventlog.add("warning", syncData.eventLogInfo, "Unknown XML", JSON.stringify(cards.multi[c]));
}
}
}
// reset sync process
syncData.progressData.reset(0, cardsFound);
//download all cards added to vCardsChangedOnServer and process changes
await dav.sync.multiget(syncData, vCardsChangedOnServer);
//delete all contacts added to vCardsDeletedOnServer
await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer);
//update token
syncData.currentFolderData.setFolderProperty("token", tokenNode.textContent);
return true;
},
remoteChangesByCTAG: async function (syncData) {
syncData.progressData.reset();
//Request ctag and token
syncData.setSyncState("send.request.remotechanges");
let response = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "0"});
syncData.setSyncState("eval.response.remotechanges");
let ctag = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["cs", "getctag"]], syncData.currentFolderData.getFolderProperty("href"));
let token = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d", "sync-token"]], syncData.currentFolderData.getFolderProperty("href"));
let localDeletes = syncData.target.getDeletedItemsFromChangeLog();
//if CTAG changed, we need to sync everything and compare
if (ctag === null || ctag != syncData.currentFolderData.getFolderProperty("ctag")) {
let vCardsFoundOnServer = [];
let vCardsChangedOnServer = {};
//get etags of all cards on server and find the changed cards
syncData.setSyncState("send.request.remotechanges");
let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
//to test other impl
//let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: []}, false);
//this is the same request, but includes getcontenttype, do we need it? icloud send contacts without
//let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
//play with filters and limits for synology
/*
let additional = "10";
additional += "";
additional += "";
additional += "bogusxy";
additional += "";
additional += "";*/
//addressbook-query does not work on older servers (zimbra)
//let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
syncData.setSyncState("eval.response.remotechanges");
let cardsFound = 0;
for (let c=0; cards.multi && c < cards.multi.length; c++) {
let id = cards.multi[c].href;
if (id == syncData.currentFolderData.getFolderProperty("href")) {
//some servers (Radicale) report the folder itself and a querry to that would return everything again
continue;
}
let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
//ctype is currently not used, because iCloud does not send one and sabre/dav documentation is not checking ctype
//let ctype = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getcontenttype"]]);
if (cards.multi[c].status == "200" && etag !== null && id !== null /* && ctype !== null */) { //we do not actually check the content of ctype - but why do we request it? iCloud seems to send cards without ctype
vCardsFoundOnServer.push(id);
let card = await syncData.target.getItemFromProperty("X-DAV-HREF", id);
if (!card) {
//if the user deleted this card (not yet send to server), do not add it again
if (!localDeletes.includes(id)) {
cardsFound++;
vCardsChangedOnServer[id] = "ADD";
}
} else if (etag.textContent != card.getProperty("X-DAV-ETAG")) {
cardsFound++;
vCardsChangedOnServer[id] = "MOD";
}
}
}
//FIND DELETES: loop over current addressbook and check each local card if it still exists on the server
let vCardsDeletedOnServer = [];
let localAdditions = syncData.target.getAddedItemsFromChangeLog();
let allItems = syncData.target.getAllItems()
for (let card of allItems) {
let id = card.getProperty("X-DAV-HREF");
if (id && !vCardsFoundOnServer.includes(id) && !localAdditions.includes(id)) {
//delete request from server
cardsFound++;
vCardsDeletedOnServer.push(card);
}
}
// reset sync process
syncData.progressData.reset(0, cardsFound);
//download all cards added to vCardsChangedOnServer and process changes
await dav.sync.multiget(syncData, vCardsChangedOnServer);
//delete all contacts added to vCardsDeletedOnServer
await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer);
//update ctag and token (if there is one)
if (ctag === null) return false; //if server does not support ctag, "it did not change"
syncData.currentFolderData.setFolderProperty("ctag", ctag);
if (token) syncData.currentFolderData.setFolderProperty("token", token);
//ctag did change
return true;
} else {
//ctag did not change
return false;
}
},
multiget: async function (syncData, vCardsChangedOnServer) {
//keep track of found mailing lists and its members
syncData.foundMailingListsDuringDownSync = {};
//download all changed cards and process changes
let cards2catch = Object.keys(vCardsChangedOnServer);
let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
for (let i=0; i < cards2catch.length; i+=maxitems) {
let request = dav.tools.getMultiGetRequest(cards2catch.slice(i, i+maxitems));
if (request) {
syncData.setSyncState("send.request.remotechanges");
let cards = await dav.network.sendRequest(request, syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1"});
syncData.setSyncState("eval.response.remotechanges");
for (let c=0; c < cards.multi.length; c++) {
syncData.progressData.inc();
let id = cards.multi[c].href;
let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]);
let data = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["card","address-data"]]);
if (cards.multi[c].status == "200" && etag !== null && data !== null && id !== null && vCardsChangedOnServer.hasOwnProperty(id)) {
switch (vCardsChangedOnServer[id]) {
case "ADD":
await dav.tools.addContact (syncData, id, data, etag);
break;
case "MOD":
await dav.tools.modifyContact (syncData, id, data, etag);
break;
}
//Feedback from users: They want to see the individual count
syncData.setSyncState("eval.response.remotechanges");
await TbSync.tools.sleep(100);
} else {
TbSync.dump("Skipped Card", [id, cards.multi[c].status == "200", etag !== null, data !== null, id !== null, vCardsChangedOnServer.hasOwnProperty(id)].join(", "));
}
}
}
}
// Feedback from users: They want to see the final count.
syncData.setSyncState("eval.response.remotechanges");
await TbSync.tools.sleep(200);
// On down sync, mailinglists need to be done at the very end so all member data is avail.
if (syncData.accountData.getAccountProperty("syncGroups")) {
let l=0;
for (let listID in syncData.foundMailingListsDuringDownSync) {
if (syncData.foundMailingListsDuringDownSync.hasOwnProperty(listID)) {
l++;
let list = await syncData.target.getItemFromProperty("X-DAV-HREF", listID);
if (!list.isMailList)
continue;
//CardInfo contains the name and the X-DAV-UID list of the members
let vCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].vCardData, syncData.target);
let oCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].oCardData, syncData.target);
// Smart merge: oCardInfo contains the state during last sync, vCardInfo is the current state.
// By comparing we can learn, which member was deleted by the server (in old but not in new),
// and which one was added (in new but not in old)
let removedMembers = oCardInfo.members.filter(e => !vCardInfo.members.includes(e));
let newMembers = vCardInfo.members.filter(e => !oCardInfo.members.includes(e));
// Check that all new members have an email address (fix for bug 1522453)
let m=0;
for (let member of newMembers) {
let card = await syncData.target.getItemFromProperty("X-DAV-UID", member);
if (card) {
let email = card.getProperty("PrimaryEmail");
if (!email) {
let email = Date.now() + "." + l + "." + m + "@bug1522453";
card.setProperty("PrimaryEmail", email);
syncData.target.modifyItem(card);
}
} else {
TbSync.dump("Member not found: " + member);
}
m++;
}
// if any of the to-be-removed members are not members of the local list, they are skipt
// if any of the to-be-added members are already members of the local list, they are skipt
list.removeListMembers("X-DAV-UID", removedMembers);
list.addListMembers("X-DAV-UID", newMembers);
syncData.target.modifyItem(list);
}
}
}
},
deleteContacts: async function (syncData, cards2delete) {
let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
// try to show a progress based on maxitens during delete and not delete all at once
for (let i=0; i < cards2delete.length; i+=maxitems) {
//get size of next block
let remain = (cards2delete.length - i);
let chunk = Math.min(remain, maxitems);
syncData.progressData.inc(chunk);
syncData.setSyncState("eval.response.remotechanges");
await TbSync.tools.sleep(200); //we want the user to see, that deletes are happening
for (let j=0; j < chunk; j++) {
syncData.target.deleteItem(cards2delete[i+j]);
}
}
},
localChanges: async function (syncData) {
//define how many entries can be send in one request
let maxitems = dav.sync.prefSettings.getIntPref("maxitems");
let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly");
let permissionErrors = 0;
let permissionError = { //keep track of permission errors - preset with downloadonly status to skip sync in that case
"added_by_user": downloadonly,
"modified_by_user": downloadonly,
"deleted_by_user": downloadonly
};
let syncGroups = syncData.accountData.getAccountProperty("syncGroups");
//access changelog to get local modifications (done and todo are used for UI to display progress)
syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length);
do {
syncData.setSyncState("prepare.request.localchanges");
//get changed items from ChangeLog
let changes = syncData.target.getItemsFromChangeLog(maxitems);
if (changes.length == 0)
break;
for (let i=0; i < changes.length; i++) {
switch (changes[i].status) {
case "added_by_user":
case "modified_by_user":
{
let isAdding = (changes[i].status == "added_by_user");
if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry
let card = await syncData.target.getItem(changes[i].itemId);
if (card) {
if (card.isMailList && !syncGroups) {
// Conditionally break out of the switch early, but do
// execute the cleanup code below the switch. A continue would
// miss that.
break;
}
let vcard = card.isMailList
? dav.tools.getVCardFromThunderbirdListCard(syncData, card, isAdding)
: dav.tools.getVCardFromThunderbirdContactCard(syncData, card, isAdding);
let headers = {"Content-Type": "text/vcard; charset=utf-8"};
//if (!isAdding) headers["If-Match"] = vcard.etag;
syncData.setSyncState("send.request.localchanges");
if (isAdding || vcard.modified) {
let response = await dav.network.sendRequest(vcard.data, card.getProperty("X-DAV-HREF"), "PUT", syncData.connectionData, headers, {softfail: [403,405]});
syncData.setSyncState("eval.response.localchanges");
if (response && response.softerror) {
permissionError[changes[i].status] = true;
TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString(isAdding ? "acl.add" : "acl.modify", "dav"));
}
}
} else {
TbSync.eventlog.add("warning", syncData.eventLogInfo, "cardnotfoundbutinchangelog::" + changes[i].itemId + "/" + changes[i].status);
}
}
if (permissionError[changes[i].status]) {
//we where not allowed to add or modify that card, remove it, we will get a fresh copy on the following revert
let card = await syncData.target.getItem(changes[i].itemId);
if (card) syncData.target.deleteItem(card);
permissionErrors++;
}
}
break;
case "deleted_by_user":
{
if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry
syncData.setSyncState("send.request.localchanges");
let response = await dav.network.sendRequest("", changes[i].itemId , "DELETE", syncData.connectionData, {}, {softfail: [403, 404, 405]});
syncData.setSyncState("eval.response.localchanges");
if (response && response.softerror) {
if (response.softerror != 404) { //we cannot do anything about a 404 on delete, the card has been deleted here and is not avail on server
permissionError[changes[i].status] = true;
TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString("acl.delete", "dav"));
}
}
}
if (permissionError[changes[i].status]) {
permissionErrors++;
}
}
break;
}
syncData.target.removeItemFromChangeLog(changes[i].itemId);
syncData.progressData.inc(); //UI feedback
}
} while (true);
//return number of modified cards or the number of permission errors (negativ)
return (permissionErrors > 0 ? 0 - permissionErrors : syncData.progressData.done);
},
}