/* * 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.. 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("", 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(""+(aPropertyValue + "FFFFFFFF").slice(0,9)+"", 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("", 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");