diff options
Diffstat (limited to '')
-rw-r--r-- | content/provider.js | 691 |
1 files changed, 691 insertions, 0 deletions
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"); |