"use strict"; Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); class Parser { /** * */ constructor() { this._parser = {}; this._registerDefaultParsers(); } /** * checks if a parser exists for a given property name * * @param {string} propertyName * @return {boolean} */ canParse(propertyName) { return Object.prototype.hasOwnProperty.call(this._parser, propertyName); } /** * parses a single prop Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ parse(document2, node, resolver) { const propertyName = "{".concat(node.namespaceURI, "}").concat(node.localName); if (!this.canParse(propertyName)) { throw new Error('Unable to parse unknown property "'.concat(propertyName, '"')); } return this._parser[propertyName](document2, node, resolver); } /** * registers a parser for propertyName * * @param {string} propertyName * @param {Function} parser */ registerParser(propertyName, parser) { this._parser[propertyName] = parser; } /** * unregisters a parser for propertyName * * @param {string} propertyName */ unregisterParser(propertyName) { delete this._parser[propertyName]; } /** * registers the predefined parsers * * @private */ _registerDefaultParsers() { this.registerParser("{DAV:}displayname", Parser.text); this.registerParser("{DAV:}creationdate", Parser.text); this.registerParser("{DAV:}getcontentlength", Parser.decInt); this.registerParser("{DAV:}getcontenttype", Parser.text); this.registerParser("{DAV:}getcontentlanguage", Parser.text); this.registerParser("{DAV:}getlastmodified", Parser.rfc1123Date); this.registerParser("{DAV:}getetag", Parser.text); this.registerParser("{DAV:}resourcetype", Parser.resourceType); this.registerParser("{DAV:}inherited-acl-set", Parser.hrefs); this.registerParser("{DAV:}group", Parser.href); this.registerParser("{DAV:}owner", Parser.href); this.registerParser("{DAV:}current-user-privilege-set", Parser.privileges); this.registerParser("{DAV:}principal-collection-set", Parser.hrefs); this.registerParser("{DAV:}principal-URL", Parser.href); this.registerParser("{DAV:}alternate-URI-set", Parser.hrefs); this.registerParser("{DAV:}group-member-set", Parser.hrefs); this.registerParser("{DAV:}group-membership", Parser.hrefs); this.registerParser("{DAV:}current-user-principal", Parser.currentUserPrincipal); this.registerParser("{DAV:}sync-token", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:carddav}address-data", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:carddav}addressbook-description", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:carddav}supported-address-data", Parser.addressDataTypes); this.registerParser("{urn:ietf:params:xml:ns:carddav}max-resource-size", Parser.decInt); this.registerParser("{urn:ietf:params:xml:ns:carddav}addressbook-home-set", Parser.hrefs); this.registerParser("{urn:ietf:params:xml:ns:carddav}principal-address", Parser.href); this.registerParser("{urn:ietf:params:xml:ns:carddav}supported-collation-set", Parser.supportedCardDAVCollations); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-data", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-home-set", Parser.hrefs); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-description", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-timezone", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set", Parser.calendarComps); this.registerParser("{urn:ietf:params:xml:ns:caldav}supported-calendar-data", Parser.calendarDatas); this.registerParser("{urn:ietf:params:xml:ns:caldav}max-resource-size", Parser.decInt); this.registerParser("{urn:ietf:params:xml:ns:caldav}min-date-time", Parser.iCalendarTimestamp); this.registerParser("{urn:ietf:params:xml:ns:caldav}max-date-time", Parser.iCalendarTimestamp); this.registerParser("{urn:ietf:params:xml:ns:caldav}max-instances", Parser.decInt); this.registerParser("{urn:ietf:params:xml:ns:caldav}max-attendees-per-instance", Parser.decInt); this.registerParser("{urn:ietf:params:xml:ns:caldav}supported-collation-set", Parser.supportedCalDAVCollations); this.registerParser("{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL", Parser.href); this.registerParser("{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL", Parser.href); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-user-address-set", Parser.hrefs); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-user-type", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp", Parser.scheduleCalendarTransp); this.registerParser("{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL", Parser.href); this.registerParser("{urn:ietf:params:xml:ns:caldav}schedule-tag", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}timezone-service-set", Parser.hrefs); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-timezone-id", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}calendar-availability", Parser.text); this.registerParser("{http://apple.com/ns/ical/}calendar-order", Parser.decInt); this.registerParser("{http://apple.com/ns/ical/}calendar-color", Parser.color); this.registerParser("{http://calendarserver.org/ns/}source", Parser.href); this.registerParser("{urn:ietf:params:xml:ns:caldav}default-alarm-vevent-datetime", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}default-alarm-vevent-date", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}default-alarm-vtodo-datetime", Parser.text); this.registerParser("{urn:ietf:params:xml:ns:caldav}default-alarm-vtodo-date", Parser.text); this.registerParser("{http://calendarserver.org/ns/}getctag", Parser.text); this.registerParser("{http://calendarserver.org/ns/}calendar-proxy-read-for", Parser.hrefs); this.registerParser("{http://calendarserver.org/ns/}calendar-proxy-write-for", Parser.hrefs); this.registerParser("{http://calendarserver.org/ns/}allowed-sharing-modes", Parser.allowedSharingModes); this.registerParser("{http://calendarserver.org/ns/}shared-url", Parser.href); this.registerParser("{http://sabredav.org/ns}owner-principal", Parser.href); this.registerParser("{http://sabredav.org/ns}read-only", Parser.bool); this.registerParser("{http://calendarserver.org/ns/}pre-publish-url", Parser.href); this.registerParser("{http://calendarserver.org/ns/}publish-url", Parser.href); this.registerParser("{http://owncloud.org/ns}invite", Parser.ocInvite); this.registerParser("{http://owncloud.org/ns}calendar-enabled", Parser.bool); this.registerParser("{http://owncloud.org/ns}enabled", Parser.bool); this.registerParser("{http://owncloud.org/ns}read-only", Parser.bool); this.registerParser("{http://nextcloud.com/ns}owner-displayname", Parser.text); this.registerParser("{http://nextcloud.com/ns}deleted-at", Parser.iso8601DateTime); this.registerParser("{http://nextcloud.com/ns}calendar-uri", Parser.text); this.registerParser("{http://nextcloud.com/ns}has-photo", Parser.bool); this.registerParser("{http://nextcloud.com/ns}trash-bin-retention-duration", Parser.decInt); this.registerParser("{http://nextcloud.com/ns}language", Parser.text); this.registerParser("{http://nextcloud.com/ns}room-type", Parser.text); this.registerParser("{http://nextcloud.com/ns}room-seating-capacity", Parser.decInt); this.registerParser("{http://nextcloud.com/ns}room-building-address", Parser.text); this.registerParser("{http://nextcloud.com/ns}room-building-story", Parser.text); this.registerParser("{http://nextcloud.com/ns}room-building-room-number", Parser.text); this.registerParser("{http://nextcloud.com/ns}room-features", Parser.text); this.registerParser("{http://sabredav.org/ns}email-address", Parser.text); } /** * returns text value of Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string} */ static text(document2, node, resolver) { return document2.evaluate("string(.)", node, resolver, XPathResult.ANY_TYPE, null).stringValue; } /** * returns boolean value of Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {boolean} */ static bool(document2, node, resolver) { return Parser.text(document2, node, resolver) === "1"; } /** * returns decimal integer value of Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {number} */ static decInt(document2, node, resolver) { return parseInt(Parser.text(document2, node, resolver), 10); } /** * returns Date value of Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {Date} */ static rfc1123Date(document2, node, resolver) { const text = Parser.text(document2, node, resolver); return new Date(text); } /** * returns Date from an ISO8601 string * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {Date} */ static iso8601DateTime(document2, node, resolver) { const text = Parser.text(document2, node, resolver); return new Date(text); } /** * returns Date value of Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {Date} */ static iCalendarTimestamp(document2, node, resolver) { const text = Parser.text(document2, node, resolver); const year = parseInt(text.slice(0, 4), 10); const month = parseInt(text.slice(4, 6), 10) - 1; const date = parseInt(text.slice(6, 8), 10); const hour = parseInt(text.slice(9, 11), 10); const minute = parseInt(text.slice(11, 13), 10); const second = parseInt(text.slice(13, 15), 10); const dateObj = /* @__PURE__ */ new Date(); dateObj.setUTCFullYear(year, month, date); dateObj.setUTCHours(hour, minute, second, 0); return dateObj; } /** * parses a {DAV:}resourcetype Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static resourceType(document2, node, resolver) { const result = []; const children = document2.evaluate("*", node, resolver, XPathResult.ANY_TYPE, null); let childNode; while ((childNode = children.iterateNext()) !== null) { const ns = document2.evaluate("namespace-uri(.)", childNode, resolver, XPathResult.ANY_TYPE, null).stringValue; const local = document2.evaluate("local-name(.)", childNode, resolver, XPathResult.ANY_TYPE, null).stringValue; result.push("{".concat(ns, "}").concat(local)); } return result; } /** * parses a node with one href nodes as child * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string} */ static href(document2, node, resolver) { return document2.evaluate("string(d:href)", node, resolver, XPathResult.ANY_TYPE, null).stringValue; } /** * parses a node with multiple href nodes as children * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static hrefs(document2, node, resolver) { const result = []; const hrefs = document2.evaluate("d:href", node, resolver, XPathResult.ANY_TYPE, null); let hrefNode; while ((hrefNode = hrefs.iterateNext()) !== null) { result.push(document2.evaluate("string(.)", hrefNode, resolver, XPathResult.ANY_TYPE, null).stringValue); } return result; } /** * Parses a set of {DAV:}privilege Nodes * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static privileges(document2, node, resolver) { const result = []; const privileges = document2.evaluate("d:privilege/*", node, resolver, XPathResult.ANY_TYPE, null); let privilegeNode; while ((privilegeNode = privileges.iterateNext()) !== null) { const ns = document2.evaluate("namespace-uri(.)", privilegeNode, resolver, XPathResult.ANY_TYPE, null).stringValue; const local = document2.evaluate("local-name(.)", privilegeNode, resolver, XPathResult.ANY_TYPE, null).stringValue; result.push("{".concat(ns, "}").concat(local)); } return result; } /** * parses the {DAV:}current-user-principal Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {object} * @property {string} type * @property {string} href */ static currentUserPrincipal(document2, node, resolver) { const unauthenticatedCount = document2.evaluate("count(d:unauthenticated)", node, resolver, XPathResult.ANY_TYPE, null).numberValue; if (unauthenticatedCount !== 0) { return { type: "unauthenticated", href: null }; } else { return { type: "href", href: Parser.href(...arguments) }; } } /** * Parses a {urn:ietf:params:xml:ns:carddav}supported-address-data Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ static addressDataTypes(document2, node, resolver) { const result = []; const addressDatas = document2.evaluate("cr:address-data-type", node, resolver, XPathResult.ANY_TYPE, null); let addressDataNode; while ((addressDataNode = addressDatas.iterateNext()) !== null) { result.push({ "content-type": document2.evaluate("string(@content-type)", addressDataNode, resolver, XPathResult.ANY_TYPE, null).stringValue, version: document2.evaluate("string(@version)", addressDataNode, resolver, XPathResult.ANY_TYPE, null).stringValue }); } return result; } /** * Parses a {urn:ietf:params:xml:ns:carddav}supported-collation-set Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ static supportedCardDAVCollations(document2, node, resolver) { const result = []; const collations = document2.evaluate("cr:supported-collation", node, resolver, XPathResult.ANY_TYPE, null); let collationNode; while ((collationNode = collations.iterateNext()) !== null) { result.push(document2.evaluate("string(.)", collationNode, resolver, XPathResult.ANY_TYPE, null).stringValue); } return result; } /** * Parses a {urn:ietf:params:xml:ns:caldav}supported-collation-set Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ static supportedCalDAVCollations(document2, node, resolver) { const result = []; const collations = document2.evaluate("cl:supported-collation", node, resolver, XPathResult.ANY_TYPE, null); let collationNode; while ((collationNode = collations.iterateNext()) !== null) { result.push(document2.evaluate("string(.)", collationNode, resolver, XPathResult.ANY_TYPE, null).stringValue); } return result; } /** * Parses a {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static calendarComps(document2, node, resolver) { const result = []; const comps = document2.evaluate("cl:comp", node, resolver, XPathResult.ANY_TYPE, null); let compNode; while ((compNode = comps.iterateNext()) !== null) { result.push(document2.evaluate("string(@name)", compNode, resolver, XPathResult.ANY_TYPE, null).stringValue); } return result; } /** * Parses a {urn:ietf:params:xml:ns:caldav}supported-calendar-data Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ static calendarDatas(document2, node, resolver) { const result = []; const calendarDatas = document2.evaluate("cl:calendar-data", node, resolver, XPathResult.ANY_TYPE, null); let calendarDataNode; while ((calendarDataNode = calendarDatas.iterateNext()) !== null) { result.push({ "content-type": document2.evaluate("string(@content-type)", calendarDataNode, resolver, XPathResult.ANY_TYPE, null).stringValue, version: document2.evaluate("string(@version)", calendarDataNode, resolver, XPathResult.ANY_TYPE, null).stringValue }); } return result; } /** * Parses a {urn:ietf:params:xml:ns:caldav}schedule-calendar-transp Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string} */ static scheduleCalendarTransp(document2, node, resolver) { const children = document2.evaluate("cl:opaque | cl:transparent", node, resolver, XPathResult.ANY_TYPE, null); const childNode = children.iterateNext(); if (childNode) { return document2.evaluate("local-name(.)", childNode, resolver, XPathResult.ANY_TYPE, null).stringValue; } } /** * Parses a {http://apple.com/ns/ical/}calendar-color Node * strips the alpha value of RGB values * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string} */ static color(document2, node, resolver) { const text = Parser.text(document2, node, resolver); if (text.length === 9) { return text.slice(0, 7); } return text; } /** * Parses a {http://calendarserver.org/ns/}allowed-sharing-modes Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static allowedSharingModes(document2, node, resolver) { const result = []; const children = document2.evaluate("cs:can-be-shared | cs:can-be-published", node, resolver, XPathResult.ANY_TYPE, null); let childNode; while ((childNode = children.iterateNext()) !== null) { const ns = document2.evaluate("namespace-uri(.)", childNode, resolver, XPathResult.ANY_TYPE, null).stringValue; const local = document2.evaluate("local-name(.)", childNode, resolver, XPathResult.ANY_TYPE, null).stringValue; result.push("{".concat(ns, "}").concat(local)); } return result; } /** * Parses a {http://owncloud.org/ns}invite Node * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {*} */ static ocInvite(document2, node, resolver) { const result = []; const users = document2.evaluate("oc:user", node, resolver, XPathResult.ANY_TYPE, null); let userNode; while ((userNode = users.iterateNext()) !== null) { result.push({ href: Parser.href(document2, userNode, resolver), "common-name": document2.evaluate("string(oc:common-name)", userNode, resolver, XPathResult.ANY_TYPE, null).stringValue, "invite-accepted": document2.evaluate("count(oc:invite-accepted)", userNode, resolver, XPathResult.ANY_TYPE, null).numberValue === 1, access: Parser.ocAccess(document2, userNode, resolver) }); } return result; } /** * Parses a set of {http://owncloud.org/ns}access Nodes * * @param {Document} document * @param {Node} node * @param {XPathNSResolver} resolver * @return {string[]} */ static ocAccess(document2, node, resolver) { const result = []; const privileges = document2.evaluate("oc:access/*", node, resolver, XPathResult.ANY_TYPE, null); let privilegeNode; while ((privilegeNode = privileges.iterateNext()) !== null) { const ns = document2.evaluate("namespace-uri(.)", privilegeNode, resolver, XPathResult.ANY_TYPE, null).stringValue; const local = document2.evaluate("local-name(.)", privilegeNode, resolver, XPathResult.ANY_TYPE, null).stringValue; result.push("{".concat(ns, "}").concat(local)); } return result; } } const DAV = "DAV:"; const IETF_CALDAV = "urn:ietf:params:xml:ns:caldav"; const IETF_CARDDAV = "urn:ietf:params:xml:ns:carddav"; const OWNCLOUD = "http://owncloud.org/ns"; const NEXTCLOUD = "http://nextcloud.com/ns"; const APPLE = "http://apple.com/ns/ical/"; const CALENDARSERVER = "http://calendarserver.org/ns/"; const SABREDAV = "http://sabredav.org/ns"; const NS_MAP = { d: DAV, cl: IETF_CALDAV, cr: IETF_CARDDAV, oc: OWNCLOUD, nc: NEXTCLOUD, aapl: APPLE, cs: CALENDARSERVER, sd: SABREDAV }; function resolve(short) { return NS_MAP[short] || null; } const namespaceUtility = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, APPLE, CALENDARSERVER, DAV, IETF_CALDAV, IETF_CARDDAV, NEXTCLOUD, NS_MAP, OWNCLOUD, SABREDAV, resolve }, Symbol.toStringTag, { value: "Module" })); const serializer = new XMLSerializer(); let prefixMap = {}; function getRootSkeleton() { if (arguments.length === 0) { return [{}, null]; } const skeleton = { name: arguments[0], children: [] }; let childrenWrapper = skeleton.children; const args = Array.prototype.slice.call(arguments, 1); args.forEach(function(argument) { const level = { name: argument, children: [] }; childrenWrapper.push(level); childrenWrapper = level.children; }); return [skeleton, childrenWrapper]; } function serialize(json) { json = json || {}; if (typeof json !== "object" || !Object.prototype.hasOwnProperty.call(json, "name")) { return ""; } const root = document.implementation.createDocument("", "", null); xmlify(root, root, json); return serializer.serializeToString(root); } function xmlify(xmlDoc, parent, json) { const [ns, localName] = json.name; const element = xmlDoc.createElementNS(ns, getPrefixedNameForNamespace(ns, localName)); json.attributes = json.attributes || []; json.attributes.forEach((attribute) => { if (attribute.length === 2) { const [name, value] = attribute; element.setAttribute(name, value); } else { const [namespace, localName2, value] = attribute; element.setAttributeNS(namespace, localName2, value); } }); if (json.value) { element.textContent = json.value; } else if (json.children) { json.children.forEach((child) => { xmlify(xmlDoc, element, child); }); } parent.appendChild(element); } function getPrefixedNameForNamespace(ns, localName) { if (!Object.prototype.hasOwnProperty.call(prefixMap, ns)) { prefixMap[ns] = "x" + Object.keys(prefixMap).length; } return prefixMap[ns] + ":" + localName; } class AttachError extends Error { /** * * @param {object} attach */ constructor(attach) { super(); Object.assign(this, attach); } } class NetworkRequestAbortedError extends AttachError { } class NetworkRequestError extends AttachError { } class NetworkRequestHttpError extends AttachError { } class NetworkRequestServerError extends NetworkRequestHttpError { } class NetworkRequestClientError extends NetworkRequestHttpError { } class Request { /** * Creates a new Request object * * @param {string} baseUrl - root url of DAV server, use OC.remote('dav') * @param {Parser} parser - instance of Parser class * @param {Function} xhrProvider - Function that returns new XMLHttpRequest objects */ constructor(baseUrl, parser, xhrProvider = () => new XMLHttpRequest()) { this.baseUrl = baseUrl; this.parser = parser; this.xhrProvider = xhrProvider; } /** * sends a GET request * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async get(url, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("GET", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a PATCH request * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async patch(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("PATCH", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a POST request * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async post(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("POST", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a PUT request * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async put(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("PUT", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a DELETE request * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async delete(url, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("DELETE", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a COPY request * https://tools.ietf.org/html/rfc4918#section-9.8 * * @param {string} url - URL to do the request on * @param {string} destination - place to copy the object/collection to * @param {number | string} depth - 0 = copy collection without content, Infinity = copy collection with content * @param {boolean} overwrite - whether or not to overwrite destination if existing * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async copy(url, destination, depth = 0, overwrite = false, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { headers.Destination = destination; headers.Depth = depth; headers.Overwrite = overwrite ? "T" : "F"; return this.request("COPY", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a MOVE request * https://tools.ietf.org/html/rfc4918#section-9.9 * * @param {string} url - URL to do the request on * @param {string} destination - place to move the object/collection to * @param {boolean} overwrite - whether or not to overwrite destination if existing * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async move(url, destination, overwrite = false, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { headers.Destination = destination; headers.Depth = "Infinity"; headers.Overwrite = overwrite ? "T" : "F"; return this.request("MOVE", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a LOCK request * https://tools.ietf.org/html/rfc4918#section-9.10 * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async lock(url, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("LOCK", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends an UNLOCK request * https://tools.ietf.org/html/rfc4918#section-9.11 * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async unlock(url, headers = {}, body = null, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("UNLOCK", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a PROPFIND request * https://tools.ietf.org/html/rfc4918#section-9.1 * * @param {string} url - URL to do the request on * @param {string[][]} properties - list of properties to search for, formatted as [namespace, localName] * @param {number | string} depth - Depth header to send * @param {object} headers - additional HTTP headers to send * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async propFind(url, properties, depth = 0, headers = {}, beforeRequestHandler = () => null, afterRequestHandler = () => null) { headers.Depth = depth; const [skeleton, dPropChildren] = getRootSkeleton([DAV, "propfind"], [DAV, "prop"]); dPropChildren.push(...properties.map((p) => ({ name: p }))); const body = serialize(skeleton); return this.request("PROPFIND", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a PROPPATCH request * https://tools.ietf.org/html/rfc4918#section-9.2 * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async propPatch(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("PROPPATCH", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a MKCOL request * https://tools.ietf.org/html/rfc4918#section-9.3 * https://tools.ietf.org/html/rfc5689 * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async mkCol(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("MKCOL", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends a REPORT request * https://tools.ietf.org/html/rfc3253#section-3.6 * * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async report(url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { return this.request("REPORT", url, headers, body, beforeRequestHandler, afterRequestHandler); } /** * sends generic request * * @param {string} method - HTTP Method name * @param {string} url - URL to do the request on * @param {object} headers - additional HTTP headers to send * @param {string} body - request body * @param {Function} beforeRequestHandler - custom function to be called before the request is made * @param {Function} afterRequestHandler - custom function to be called after the request was made * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async request(method, url, headers, body, beforeRequestHandler = () => null, afterRequestHandler = () => null) { const xhr = this.xhrProvider(); const assignHeaders = Object.assign({}, getDefaultHeaders(), headers); xhr.open(method, this.absoluteUrl(url), true); for (const header in assignHeaders) { xhr.setRequestHeader(header, assignHeaders[header]); } beforeRequestHandler(xhr); if (body === null || body === void 0) { xhr.send(); } else { xhr.send(body); } return new Promise((resolve2, reject) => { xhr.onreadystatechange = () => { if (xhr.readyState !== 4) { return; } afterRequestHandler(xhr); let responseBody = xhr.response; if (!wasRequestSuccessful(xhr.status)) { if (xhr.status >= 400 && xhr.status < 500) { reject(new NetworkRequestClientError({ body: responseBody, status: xhr.status, xhr })); return; } if (xhr.status >= 500 && xhr.status < 600) { reject(new NetworkRequestServerError({ body: responseBody, status: xhr.status, xhr })); return; } reject(new NetworkRequestHttpError({ body: responseBody, status: xhr.status, xhr })); return; } if (xhr.status === 207) { responseBody = this._parseMultiStatusResponse(responseBody); if (parseInt(assignHeaders.Depth, 10) === 0 && method === "PROPFIND") { responseBody = responseBody[Object.keys(responseBody)[0]]; } } resolve2({ body: responseBody, status: xhr.status, xhr }); }; xhr.onerror = () => reject(new NetworkRequestError({ body: null, status: -1, xhr })); xhr.onabort = () => reject(new NetworkRequestAbortedError({ body: null, status: -1, xhr })); }); } /** * returns name of file / folder of a url * * @param url * @params {string} url * @return {string} */ filename(url) { let pathname = this.pathname(url); if (pathname.slice(-1) === "/") { pathname = pathname.slice(0, -1); } const slashPos = pathname.lastIndexOf("/"); return pathname.slice(slashPos); } /** * returns pathname for a URL * * @param url * @params {string} url * @return {string} */ pathname(url) { const urlObject = new URL(url, this.baseUrl); return urlObject.pathname; } /** * returns absolute url * * @param {string} url * @return {string} */ absoluteUrl(url) { const urlObject = new URL(url, this.baseUrl); return urlObject.href; } /** * parses a multi status response (207), sorts them by path * and drops all unsuccessful responses * * @param {string} body * @return {object} * @private */ _parseMultiStatusResponse(body) { const result = {}; const domParser = new DOMParser(); const document2 = domParser.parseFromString(body, "application/xml"); const responses = document2.evaluate("/d:multistatus/d:response", document2, resolve, XPathResult.ANY_TYPE, null); let responseNode; while ((responseNode = responses.iterateNext()) !== null) { const href = document2.evaluate("string(d:href)", responseNode, resolve, XPathResult.ANY_TYPE, null).stringValue; const parsedProperties = {}; const propStats = document2.evaluate("d:propstat", responseNode, resolve, XPathResult.ANY_TYPE, null); let propStatNode; while ((propStatNode = propStats.iterateNext()) !== null) { const status = document2.evaluate("string(d:status)", propStatNode, resolve, XPathResult.ANY_TYPE, null).stringValue; if (!wasRequestSuccessful(getStatusCodeFromString(status))) { continue; } const props = document2.evaluate("d:prop/*", propStatNode, resolve, XPathResult.ANY_TYPE, null); let propNode; while ((propNode = props.iterateNext()) !== null) { if (this.parser.canParse("{".concat(propNode.namespaceURI, "}").concat(propNode.localName))) { parsedProperties["{".concat(propNode.namespaceURI, "}").concat(propNode.localName)] = this.parser.parse(document2, propNode, resolve); } } } result[href] = parsedProperties; } return result; } } function wasRequestSuccessful(status) { return status >= 200 && status < 300; } function getStatusCodeFromString(status) { return parseInt(status.split(" ")[1], 10); } function getDefaultHeaders() { return { Depth: "0", "Content-Type": "application/xml; charset=utf-8" }; } function uuidv4() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16).toUpperCase(); }); } function uid(prefix, suffix) { prefix = prefix || ""; suffix = suffix || ""; if (prefix !== "") { prefix += "-"; } if (suffix !== "") { suffix = "." + suffix; } return prefix + uuidv4() + suffix; } function uri(start, isAvailable) { start = start || ""; let uri2 = start.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/--+/g, "-").replace(/^-+/, "").replace(/-+$/, ""); if (uri2 === "") { uri2 = "-"; } if (isAvailable(uri2)) { return uri2; } if (uri2.indexOf("-") === -1) { uri2 = uri2 + "-1"; if (isAvailable(uri2)) { return uri2; } } do { const positionLastDash = uri2.lastIndexOf("-"); const firstPart = uri2.slice(0, positionLastDash); let lastPart = uri2.slice(positionLastDash + 1); if (lastPart.match(/^\d+$/)) { lastPart = parseInt(lastPart); lastPart++; uri2 = firstPart + "-" + lastPart; } else { uri2 = uri2 + "-1"; } } while (isAvailable(uri2) === false); return uri2; } class DAVEventListener { constructor() { this._eventListeners = {}; } /** * adds an event listener * * @param {string} type * @param {Function} listener * @param {object} options */ addEventListener(type, listener, options = null) { this._eventListeners[type] = this._eventListeners[type] || []; this._eventListeners[type].push({ listener, options }); } /** * removes an event listener * * @param {string} type * @param {Function} dListener */ removeEventListener(type, dListener) { if (!this._eventListeners[type]) { return; } const index = this._eventListeners[type].findIndex(({ listener }) => listener === dListener); if (index === -1) { return; } this._eventListeners[type].splice(index, 1); } /** * dispatch event on object * * @param {string} type * @param {DAVEvent} event */ dispatchEvent(type, event) { if (!this._eventListeners[type]) { return; } const listenersToCall = []; const listenersToCallAndRemove = []; this._eventListeners[type].forEach(({ listener, options }) => { if (options && options.once) { listenersToCallAndRemove.push(listener); } else { listenersToCall.push(listener); } }); listenersToCallAndRemove.forEach((listener) => { this.removeEventListener(type, listener); listener(event); }); listenersToCall.forEach((listener) => { listener(event); }); } } function debugFactory(context) { return (...args) => { if (debugFactory.enabled) { console.debug(context, ...args); } }; } debugFactory.enabled = false; function davCollectionPropSet(props) { const xmlified = []; Object.entries(props).forEach(([key, value]) => { switch (key) { case "{DAV:}displayname": xmlified.push({ name: [DAV, "displayname"], value }); break; } }); return xmlified; } const debug$8 = debugFactory("DavObject"); class DavObject extends DAVEventListener { /** * @param {DavCollection} parent - The parent collection this DavObject is a child of * @param {Request} request - The request object initialized by DavClient * @param {string} url - Full url of this DavObject * @param {object} props - Properties including etag, content-type, etc. * @param {boolean} isPartial - Are we dealing with the complete or just partial addressbook / calendar data */ constructor(parent, request, url, props, isPartial = false) { super(); Object.assign(this, { // parameters _parent: parent, _request: request, _url: url, _props: props, // housekeeping _isPartial: isPartial, _isDirty: false }); this._exposeProperty("etag", DAV, "getetag", true); this._exposeProperty("contenttype", DAV, "getcontenttype"); Object.defineProperty(this, "url", { get: () => this._url }); } /** * gets unfiltered data for this object * * @param {boolean} forceReFetch Always refetch data, even if not partial * @return {Promise} */ async fetchCompleteData(forceReFetch = false) { if (!forceReFetch && !this.isPartial()) { return; } const request = await this._request.propFind(this._url, this.constructor.getPropFindList(), 0); this._props = request.body; this._isDirty = false; this._isPartial = false; } /** * copies a DavObject to a different DavCollection * @param {DavCollection} collection * @param {boolean} overwrite * @param headers * @return {Promise} Promise that resolves to the copied DavObject */ async copy(collection, overwrite = false, headers = {}) { debug$8("copying ".concat(this.url, " from ").concat(this._parent.url, " to ").concat(collection.url)); if (this._parent === collection) { throw new Error("Copying an object to the collection it's already part of is not supported"); } if (!this._parent.isSameCollectionTypeAs(collection)) { throw new Error("Copying an object to a collection of a different type is not supported"); } if (!collection.isWriteable()) { throw new Error("Can not copy object into read-only destination collection"); } const uri2 = this.url.split("/").splice(-1, 1)[0]; const destination = collection.url + uri2; await this._request.copy(this.url, destination, 0, overwrite, headers); return collection.find(uri2); } /** * moves a DavObject to a different DavCollection * @param {DavCollection} collection * @param {boolean} overwrite * @param headers * @return {Promise} */ async move(collection, overwrite = false, headers = {}) { debug$8("moving ".concat(this.url, " from ").concat(this._parent.url, " to ").concat(collection.url)); if (this._parent === collection) { throw new Error("Moving an object to the collection it's already part of is not supported"); } if (!this._parent.isSameCollectionTypeAs(collection)) { throw new Error("Moving an object to a collection of a different type is not supported"); } if (!collection.isWriteable()) { throw new Error("Can not move object into read-only destination collection"); } const uri2 = this.url.split("/").splice(-1, 1)[0]; const destination = collection.url + uri2; await this._request.move(this.url, destination, overwrite, headers); this._parent = collection; this._url = destination; } /** * updates the DavObject on the server * @return {Promise} */ async update() { if (this.isPartial() || !this.isDirty() || !this.data) { return; } const headers = {}; if (this.contenttype) { headers["Content-Type"] = "".concat(this.contenttype, "; charset=utf-8"); } if (this.etag) { headers["If-Match"] = this.etag; } return this._request.put(this.url, headers, this.data).then((res) => { this._isDirty = false; this._props["{DAV:}getetag"] = res.xhr.getResponseHeader("etag"); }).catch((ex) => { this._isDirty = true; if (ex instanceof NetworkRequestClientError && ex.status === 412) { this._isPartial = true; } throw ex; }); } /** * deletes the DavObject on the server * * @param headers * @return {Promise} */ async delete(headers = {}) { return this._request.delete(this.url, headers); } /** * returns whether the data in this DavObject is the result of a partial retrieval * * @return {boolean} */ isPartial() { return this._isPartial; } /** * returns whether the data in this DavObject contains unsynced changes * * @return {boolean} */ isDirty() { return this._isDirty; } /** * @protected * @param {string} localName * @param {string} xmlNamespace * @param {string} xmlName * @param {boolean} mutable * @return void */ _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { if (mutable) { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)], set: (val) => { this._isDirty = true; this._props["{".concat(xmlNamespace, "}").concat(xmlName)] = val; } }); } else { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)] }); } } /** * A list of all property names that should be included * in propfind requests that may include this object * * @return {string[][]} */ static getPropFindList() { return [ [DAV, "getcontenttype"], [DAV, "getetag"], [DAV, "resourcetype"] ]; } } const debug$7 = debugFactory("DavCollection"); class DavCollection extends DAVEventListener { /** * @param {object} parent * @param {Request} request * @param {string} url * @param {object} props */ constructor(parent, request, url, props) { super(); if (url.slice(-1) !== "/") { url += "/"; } Object.assign(this, { // parameters _parent: parent, _request: request, _url: url, _props: props, // constructors _collectionFactoryMapper: {}, _objectFactoryMapper: {}, // house keeping _updatedProperties: [], _childrenNames: [], // parsers / factories _propFindList: [], _propSetFactory: [] }); this._registerPropSetFactory(davCollectionPropSet); this._exposeProperty("displayname", DAV, "displayname", true); this._exposeProperty("owner", DAV, "owner"); this._exposeProperty("resourcetype", DAV, "resourcetype"); this._exposeProperty("syncToken", DAV, "sync-token"); this._exposeProperty("currentUserPrivilegeSet", DAV, "current-user-privilege-set"); Object.defineProperty(this, "url", { get: () => this._url }); this._propFindList.push(...DavObject.getPropFindList()); this._propFindList.push(...DavCollection.getPropFindList()); } /** * finds all children of a collection * * @return {Promise} */ async findAll() { const response = await this._request.propFind(this._url, this._propFindList, 1); return this._handleMultiStatusResponse(response, false); } /** * finds all children of a collection filtered by filter * * @param {Function} filter * @return {Promise} */ async findAllByFilter(filter) { const all = await this.findAll(); return all.filter(filter); } /** * find one object by its uri * * @param {string} uri * @return {Promise} */ async find(uri2) { const response = await this._request.propFind(this._url + uri2, this._propFindList, 0); response.body = { [this._url + uri2]: response.body }; return this._handleMultiStatusResponse(response, false)[0]; } /** * creates a new webdav collection * https://tools.ietf.org/html/rfc5689 * * You usually don't want to call this method directly * but instead use: * - AddressBookHome->createAddressBookCollection * - CalendarHome->createCalendarCollection * - CalendarHome->createSubscribedCollection * * @param {string} name * @param {?Array} props * @return {Promise} */ async createCollection(name, props = null) { debug$7("creating a collection"); if (!props) { props = [{ name: [DAV, "resourcetype"], children: [{ name: [DAV, "collection"] }] }]; } const [skeleton, dPropChildren] = getRootSkeleton( [DAV, "mkcol"], [DAV, "set"], [DAV, "prop"] ); dPropChildren.push(...props); const uri2 = this._getAvailableNameFromToken(name); const data = serialize(skeleton); await this._request.mkCol(this.url + uri2, {}, data); return this.find(uri2 + "/"); } /** * creates a new webdav object inside this collection * * You usually don't want to call this method directly * but instead use: * - AddressBook->createVCard * - Calendar->createVObject * * @param {string} name * @param {object} headers * @param {string} data * @return {Promise} */ async createObject(name, headers, data) { debug$7("creating an object"); await this._request.put(this.url + name, headers, data); return this.find(name); } /** * sends a PropPatch request to update the collections' properties * The request is only made if properties actually changed * * @return {Promise} */ async update() { if (this._updatedProperties.length === 0) { return; } const properties = {}; this._updatedProperties.forEach((updatedProperty) => { properties[updatedProperty] = this._props[updatedProperty]; }); const propSet = this._propSetFactory.reduce((arr, p) => [...arr, ...p(properties)], []); const [skeleton, dPropSet] = getRootSkeleton( [DAV, "propertyupdate"], [DAV, "set"], [DAV, "prop"] ); dPropSet.push(...propSet); const body = serialize(skeleton); await this._request.propPatch(this._url, {}, body); } /** * deletes the DavCollection on the server * * @param {object} headers - additional HTTP headers to send * @return {Promise} */ async delete(headers = {}) { await this._request.delete(this._url, headers); } /** * * @return {boolean} */ isReadable() { return this.currentUserPrivilegeSet.includes("{DAV:}read"); } /** * * @return {boolean} */ isWriteable() { return this.currentUserPrivilegeSet.includes("{DAV:}write"); } /** * checks whether this is of the same type as another collection * @param {DavCollection} collection */ isSameCollectionTypeAs(collection) { const ownResourceType = this.resourcetype; const foreignResourceType = collection.resourcetype; const ownDiff = ownResourceType.find((r) => foreignResourceType.indexOf(r) === -1); const foreignDiff = foreignResourceType.find((r) => ownResourceType.indexOf(r) === -1); return ownDiff === void 0 && foreignDiff === void 0; } /** * @protected * @param {string} identifier * @param {Function} factory * @return void */ _registerCollectionFactory(identifier, factory) { this._collectionFactoryMapper[identifier] = factory; if (typeof factory.getPropFindList === "function") { this._propFindList.push(...factory.getPropFindList()); } } /** * @protected * @param {string} identifier * @param {Function} factory * @return void */ _registerObjectFactory(identifier, factory) { this._objectFactoryMapper[identifier] = factory; if (typeof factory.getPropFindList === "function") { this._propFindList.push(...factory.getPropFindList()); } } /** * @protected * @param factory * @return void */ _registerPropSetFactory(factory) { this._propSetFactory.push(factory); } /** * @protected * @param {string} localName * @param {string} xmlNamespace * @param {string} xmlName * @param {boolean} mutable * @return void */ _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { if (mutable) { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)], set: (val) => { this._props["{".concat(xmlNamespace, "}").concat(xmlName)] = val; if (this._updatedProperties.indexOf("{".concat(xmlNamespace, "}").concat(xmlName)) === -1) { this._updatedProperties.push("{".concat(xmlNamespace, "}").concat(xmlName)); } } }); } else { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)] }); } } /** * @protected * @param {string} token * @return {string} */ _getAvailableNameFromToken(token) { return uri(token, (name) => { return this._childrenNames.indexOf(this._url + name) === -1 && this._childrenNames.indexOf(this._url + name + "/") === -1; }); } /** * get updated properties for this collection from server * @protected * @return {object} */ async _updatePropsFromServer() { const response = await this._request.propFind(this.url, this.constructor.getPropFindList()); this._props = response.body; } /** * @param {object} response * @param {boolean} isPartial * @return {DavObject[]|DavCollection[]} * @protected */ _handleMultiStatusResponse(response, isPartial = false) { const index = []; const children = []; Object.entries(response.body).forEach(([path, props]) => { if (path === this._url || path + "/" === this.url) { return; } index.push(path); const url = this._request.pathname(path); if ((!props["{DAV:}resourcetype"] || props["{DAV:}resourcetype"].length === 0) && props["{DAV:}getcontenttype"]) { debug$7("".concat(path, " was identified as a file")); const contentType = props["{DAV:}getcontenttype"].split(";")[0]; if (!this._objectFactoryMapper[contentType]) { debug$7("No constructor for content-type ".concat(contentType, " (").concat(path, ") registered, treating as generic object")); children.push(new DavObject(this, this._request, url, props)); return; } children.push(new this._objectFactoryMapper[contentType](this, this._request, url, props, isPartial)); } else { debug$7("".concat(path, " was identified as a collection")); const collectionType = props["{DAV:}resourcetype"].find((r) => { return r !== "{".concat(DAV, "}collection"); }); if (!collectionType) { debug$7("Collection-type of ".concat(path, " was not specified, treating as generic collection")); children.push(new DavCollection(this, this._request, url, props)); return; } if (!this._collectionFactoryMapper[collectionType]) { debug$7("No constructor for collection-type ".concat(collectionType, " (").concat(path, ") registered, treating as generic collection")); children.push(new DavCollection(this, this._request, url, props)); return; } children.push(new this._collectionFactoryMapper[collectionType](this, this._request, url, props)); } }); this._childrenNames.push(...index); return children; } /** * A list of all property names that should be included * in propfind requests that may include this collection * * @return {string[][]} */ static getPropFindList() { return [ [DAV, "displayname"], [DAV, "owner"], [DAV, "resourcetype"], [DAV, "sync-token"], [DAV, "current-user-privilege-set"] ]; } } const debug$6 = debugFactory("DavCollectionPublishable"); function davCollectionPublishable(Base) { return class extends Base { /** * @inheritDoc */ constructor(...args) { super(...args); super._exposeProperty("publishURL", CALENDARSERVER, "publish-url"); } /** * publishes the DavCollection * * @return {Promise} */ async publish() { debug$6("Publishing ".concat(this.url)); const [skeleton] = getRootSkeleton( [CALENDARSERVER, "publish-calendar"] ); const xml = serialize(skeleton); await this._request.post(this._url, { "Content-Type": "application/xml; charset=utf-8" }, xml); await this._updatePropsFromServer(); } /** * unpublishes the DavCollection * * @return {Promise} */ async unpublish() { debug$6("Unpublishing ".concat(this.url)); const [skeleton] = getRootSkeleton( [CALENDARSERVER, "unpublish-calendar"] ); const xml = serialize(skeleton); await this._request.post(this._url, { "Content-Type": "application/xml; charset=utf-8" }, xml); delete this._props["{http://calendarserver.org/ns/}publish-url"]; } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [CALENDARSERVER, "publish-url"] ]); } }; } const debug$5 = debugFactory("DavCollectionShareable"); function davCollectionShareable(Base) { return class extends Base { /** * @inheritDoc */ constructor(...args) { super(...args); super._exposeProperty("shares", OWNCLOUD, "invite"); super._exposeProperty("allowedSharingModes", CALENDARSERVER, "allowed-sharing-modes"); } /** * shares a DavCollection * * @param {string} principalScheme * @param {boolean} writeable * @param {string} summary * @return {Promise} */ async share(principalScheme, writeable = false, summary = "") { debug$5("Sharing ".concat(this.url, " with ").concat(principalScheme)); const [skeleton, setProp] = getRootSkeleton( [OWNCLOUD, "share"], [OWNCLOUD, "set"] ); setProp.push({ name: [DAV, "href"], value: principalScheme }); if (writeable) { setProp.push({ name: [OWNCLOUD, "read-write"] }); } if (summary !== "") { setProp.push({ name: [OWNCLOUD, "summary"], value: summary }); } const xml = serialize(skeleton); return this._request.post(this._url, { "Content-Type": "application/xml; charset=utf-8" }, xml).then(() => { const index = this.shares.findIndex((e) => e.href === principalScheme); if (index === -1) { this.shares.push({ href: principalScheme, access: [writeable ? "{http://owncloud.org/ns}read-write" : "{http://owncloud.org/ns}read"], "common-name": null, "invite-accepted": true }); } else { this.shares[index].access = [writeable ? "{http://owncloud.org/ns}read-write" : "{http://owncloud.org/ns}read"]; } }); } /** * unshares a DAVCollection * * @param {string} principalScheme * @return {Promise} */ async unshare(principalScheme) { debug$5("Unsharing ".concat(this.url, " with ").concat(principalScheme)); const [skeleton, oSetChildren] = getRootSkeleton( [OWNCLOUD, "share"], [OWNCLOUD, "remove"] ); oSetChildren.push({ name: [DAV, "href"], value: principalScheme }); const xml = serialize(skeleton); return this._request.post(this._url, { "Content-Type": "application/xml; charset=utf-8" }, xml).then(() => { const index = this.shares.findIndex((e) => e.href === principalScheme); if (index === -1) { return; } this.shares.splice(index, 1); }); } /** * checks whether a collection is shareable * * @return {boolean} */ isShareable() { if (!Array.isArray(this.allowedSharingModes)) { return false; } return this.allowedSharingModes.includes("{".concat(CALENDARSERVER, "}can-be-shared")); } /** * checks whether a collection is publishable * * @return {boolean} */ isPublishable() { if (!Array.isArray(this.allowedSharingModes)) { return false; } return this.allowedSharingModes.includes("{".concat(CALENDARSERVER, "}can-be-published")); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [OWNCLOUD, "invite"], [CALENDARSERVER, "allowed-sharing-modes"] ]); } }; } class VObject extends DavObject { /** * Creates a VObject that is supposed to store calendar-data * as specified in RFC 5545. * * https://tools.ietf.org/html/rfc5545 * * @inheritDoc */ constructor(...args) { super(...args); super._exposeProperty("data", IETF_CALDAV, "calendar-data", true); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [IETF_CALDAV, "calendar-data"] ]); } } function calendarPropSet$1(props) { const xmlified = []; Object.entries(props).forEach(([key, value]) => { switch (key) { case "{http://apple.com/ns/ical/}calendar-order": xmlified.push({ name: [APPLE, "calendar-order"], value: value.toString() }); break; case "{http://apple.com/ns/ical/}calendar-color": xmlified.push({ name: [APPLE, "calendar-color"], value }); break; case "{http://calendarserver.org/ns/}source": xmlified.push({ name: [CALENDARSERVER, "source"], children: [{ name: [DAV, "href"], value }] }); break; case "{urn:ietf:params:xml:ns:caldav}calendar-description": xmlified.push({ name: [IETF_CALDAV, "calendar-description"], value }); break; case "{urn:ietf:params:xml:ns:caldav}calendar-timezone": xmlified.push({ name: [IETF_CALDAV, "calendar-timezone"], value }); break; case "{http://owncloud.org/ns}calendar-enabled": xmlified.push({ name: [OWNCLOUD, "calendar-enabled"], value: value ? "1" : "0" }); break; case "{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": xmlified.push({ name: [IETF_CALDAV, "schedule-calendar-transp"], children: [{ name: [IETF_CALDAV, value] }] }); break; } }); return xmlified; } const debug$4 = debugFactory("Calendar"); class Calendar extends davCollectionPublishable(davCollectionShareable(DavCollection)) { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerObjectFactory("text/calendar", VObject); super._registerPropSetFactory(calendarPropSet$1); super._exposeProperty("color", APPLE, "calendar-color", true); super._exposeProperty("enabled", OWNCLOUD, "calendar-enabled", true); super._exposeProperty("order", APPLE, "calendar-order", true); super._exposeProperty("timezone", IETF_CALDAV, "calendar-timezone", true); super._exposeProperty("components", IETF_CALDAV, "supported-calendar-component-set"); super._exposeProperty("transparency", IETF_CALDAV, "schedule-calendar-transp", true); } /** * finds all VObjects in this calendar * * @return {Promise} */ async findAllVObjects() { return super.findAllByFilter((elm) => elm instanceof VObject); } /** * find all VObjects filtered by type * * @param {string} type * @return {Promise} */ async findByType(type) { return this.calendarQuery([{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", "VCALENDAR"] ], children: [{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", type] ] }] }]); } /** * find all VObjects in a time-range filtered by type * * @param {number} type * @param {Date} from * @param {Date} to * @return {Promise} */ async findByTypeInTimeRange(type, from, to) { return this.calendarQuery([{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", "VCALENDAR"] ], children: [{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", type] ], children: [{ name: [IETF_CALDAV, "time-range"], attributes: [ ["start", Calendar._getICalendarDateTimeFromDateObject(from)], ["end", Calendar._getICalendarDateTimeFromDateObject(to)] ] }] }] }]); } /** * create a VObject inside this calendar * * @param data * @return {Promise} */ async createVObject(data) { const name = uid("", "ics"); const headers = { "Content-Type": "text/calendar; charset=utf-8" }; return super.createObject(name, headers, data); } /** * sends a calendar query as defined in * https://tools.ietf.org/html/rfc4791#section-7.8 * * @param {object[]} filter * @param {object[]} prop * @param {string} timezone * @return {Promise} */ async calendarQuery(filter, prop = null, timezone = null) { debug$4("sending an calendar-query request"); const [skeleton] = getRootSkeleton( [IETF_CALDAV, "calendar-query"] ); if (!prop) { skeleton.children.push({ name: [DAV, "prop"], children: this._propFindList.map((p) => ({ name: p })) }); } else { skeleton.children.push({ name: [DAV, "prop"], children: prop }); } if (filter) { skeleton.children.push({ name: [IETF_CALDAV, "filter"], children: filter }); } if (timezone) { skeleton.children.push({ name: [IETF_CALDAV, "timezone"], value: timezone }); } const headers = { Depth: "1" }; const body = serialize(skeleton); const response = await this._request.report(this.url, headers, body); return super._handleMultiStatusResponse(response, Calendar._isRetrievalPartial(prop)); } /** * sends a calendar multiget query as defined in * https://tools.ietf.org/html/rfc4791#section-7.9 * * @param {string[]} hrefs * @param {object[]} prop * @return {Promise} */ async calendarMultiget(hrefs = [], prop) { debug$4("sending an calendar-multiget request"); if (hrefs.length === 0) { return []; } const [skeleton] = getRootSkeleton( [IETF_CALDAV, "calendar-multiget"] ); if (!prop) { skeleton.children.push({ name: [DAV, "prop"], children: this._propFindList.map((p) => ({ name: p })) }); } else { skeleton.children.push({ name: [DAV, "prop"], children: prop }); } hrefs.forEach((href) => { skeleton.children.push({ name: [DAV, "href"], value: href }); }); const headers = { Depth: "1" }; const body = serialize(skeleton); const response = await this._request.report(this.url, headers, body); return super._handleMultiStatusResponse(response, Calendar._isRetrievalPartial(prop)); } /** * sends a calendar free-busy-query as defined in * https://tools.ietf.org/html/rfc4791#section-7.10 * * @param {Date} from * @param {Date} to * @return {Promise} */ async freeBusyQuery(from, to) { } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [APPLE, "calendar-order"], [APPLE, "calendar-color"], [CALENDARSERVER, "getctag"], [IETF_CALDAV, "calendar-description"], [IETF_CALDAV, "calendar-timezone"], [IETF_CALDAV, "supported-calendar-component-set"], [IETF_CALDAV, "supported-calendar-data"], [IETF_CALDAV, "max-resource-size"], [IETF_CALDAV, "min-date-time"], [IETF_CALDAV, "max-date-time"], [IETF_CALDAV, "max-instances"], [IETF_CALDAV, "max-attendees-per-instance"], [IETF_CALDAV, "supported-collation-set"], [IETF_CALDAV, "calendar-free-busy-set"], [IETF_CALDAV, "schedule-calendar-transp"], [IETF_CALDAV, "schedule-default-calendar-URL"], [OWNCLOUD, "calendar-enabled"], [NEXTCLOUD, "owner-displayname"], [NEXTCLOUD, "trash-bin-retention-duration"], [NEXTCLOUD, "deleted-at"] ]); } /** * checks if the prop part of a report requested partial data * * @param {object[]} prop * @return {boolean} * @private */ static _isRetrievalPartial(prop) { if (!prop) { return false; } const addressBookDataProperty = prop.find((p) => { return p.name[0] === IETF_CALDAV && p.name[1] === "calendar-data"; }); if (!addressBookDataProperty) { return false; } return !!addressBookDataProperty.children; } /** * creates an iCalendar formatted DATE-TIME string from a date object * * @param {Date} date * @return {string} * @private */ static _getICalendarDateTimeFromDateObject(date) { return [ date.getUTCFullYear(), ("0" + (date.getUTCMonth() + 1)).slice(-2), ("0" + date.getUTCDate()).slice(-2), "T", ("0" + date.getUTCHours()).slice(-2), ("0" + date.getUTCMinutes()).slice(-2), ("0" + date.getUTCSeconds()).slice(-2), "Z" ].join(""); } } class Subscription extends Calendar { /** * @inheritDoc */ constructor(...args) { super(...args); super._exposeProperty("source", CALENDARSERVER, "source", true); super._exposeProperty("refreshRate", APPLE, "refreshrate", true); super._exposeProperty("stripTodos", CALENDARSERVER, "subscribed-strip-todos", true); super._exposeProperty("stripAlarms", CALENDARSERVER, "subscribed-strip-alarms", true); super._exposeProperty("stripAttachments", CALENDARSERVER, "subscribed-strip-attachments", true); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [CALENDARSERVER, "source"], [APPLE, "refreshrate"], [CALENDARSERVER, "subscribed-strip-todos"], [CALENDARSERVER, "subscribed-strip-alarms"], [CALENDARSERVER, "subscribed-strip-attachments"] ]); } } function calendarPropSet(props) { const xmlified = []; Object.entries(props).forEach(([key, value]) => { switch (key) { case "{urn:ietf:params:xml:ns:caldav}calendar-availability": xmlified.push({ name: [IETF_CALDAV, "calendar-availability"], value: value.toString() }); break; } }); return xmlified; } class ScheduleInbox extends Calendar { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerPropSetFactory(calendarPropSet); super._exposeProperty("availability", IETF_CALDAV, "calendar-availability", true); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [IETF_CALDAV, "calendar-availability"] ]); } } class ScheduleOutbox extends DavCollection { /** * Sends a free-busy-request for the scheduling outbox * The data is required to be a valid iTIP data. * For an example, see https://tools.ietf.org/html/rfc6638#appendix-B.5 * * @param {string} data iTIP with VFREEBUSY component and METHOD:REQUEST * @return {Promise} */ async freeBusyRequest(data) { const result = {}; const response = await this._request.post(this.url, { "Content-Type": 'text/calendar; charset="utf-8"' }, data); const domParser = new DOMParser(); const document2 = domParser.parseFromString(response.body, "application/xml"); const responses = document2.evaluate("/cl:schedule-response/cl:response", document2, resolve, XPathResult.ANY_TYPE, null); let responseNode; while ((responseNode = responses.iterateNext()) !== null) { const recipient = document2.evaluate("string(cl:recipient/d:href)", responseNode, resolve, XPathResult.ANY_TYPE, null).stringValue; const status = document2.evaluate("string(cl:request-status)", responseNode, resolve, XPathResult.ANY_TYPE, null).stringValue; const calendarData = document2.evaluate("string(cl:calendar-data)", responseNode, resolve, XPathResult.ANY_TYPE, null).stringValue; const success = /^2.\d(;.+)?$/.test(status); result[recipient] = { calendarData, status, success }; } return result; } } class CalendarTrashBin extends DavCollection { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerObjectFactory("text/calendar", VObject); super._exposeProperty("retentionDuration", NEXTCLOUD, "trash-bin-retention-duration"); } async findDeletedObjects() { const [skeleton] = getRootSkeleton( [IETF_CALDAV, "calendar-query"] ); skeleton.children.push({ name: [DAV, "prop"], children: VObject.getPropFindList().map((p) => ({ name: p })).concat([ { name: [NEXTCLOUD, "calendar-uri"] }, { name: [NEXTCLOUD, "deleted-at"] } ]) }); skeleton.children.push({ name: [IETF_CALDAV, "filter"], children: [{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", "VCALENDAR"] ], children: [{ name: [IETF_CALDAV, "comp-filter"], attributes: [ ["name", "VEVENT"] ], children: [] }] }] }); const headers = { Depth: "1" }; const body = serialize(skeleton); const response = await this._request.report(this._url + "objects", headers, body); return super._handleMultiStatusResponse(response); } async restore(uri2) { await this._request.move(uri2, this._url + "restore/file"); } } class DeletedCalendar extends Calendar { } const debug$3 = debugFactory("CalendarHome"); class CalendarHome extends DavCollection { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerCollectionFactory("{" + IETF_CALDAV + "}calendar", Calendar); super._registerCollectionFactory("{" + NEXTCLOUD + "}deleted-calendar", DeletedCalendar); super._registerCollectionFactory("{" + CALENDARSERVER + "}subscribed", Subscription); super._registerCollectionFactory("{" + IETF_CALDAV + "}schedule-inbox", ScheduleInbox); super._registerCollectionFactory("{" + IETF_CALDAV + "}schedule-outbox", ScheduleOutbox); super._registerCollectionFactory("{" + NEXTCLOUD + "}trash-bin", CalendarTrashBin); } /** * Finds all CalDAV-specific collections in this calendar home * * @return {Promise<(Calendar|Subscription|ScheduleInbox|ScheduleOutbox|CalendarTrashBin|DeletedCalendar)[]>} */ async findAllCalDAVCollections() { return super.findAllByFilter((elm) => elm instanceof Calendar || elm instanceof CalendarTrashBin || elm instanceof Subscription || elm instanceof ScheduleInbox || elm instanceof ScheduleOutbox || elm instanceof DeletedCalendar); } /** * Finds all CalDAV-specific collections in this calendar home, grouped by type * * @return {Promise<{ calendars: Calendar[], deletedCalendars: DeletedCalendar[], trashBins: CalendarTrashBin[], subscriptions: Subscription[], scheduleInboxes: ScheduleInbox[], scheduleOutboxes: ScheduleOutbox[], }>} */ async findAllCalDAVCollectionsGrouped() { const collections = await super.findAll(); return { calendars: collections.filter((c) => c instanceof Calendar && !(c instanceof ScheduleInbox) && !(c instanceof Subscription) && !(c instanceof DeletedCalendar)), deletedCalendars: collections.filter((c) => c instanceof DeletedCalendar), trashBins: collections.filter((c) => c instanceof CalendarTrashBin), subscriptions: collections.filter((c) => c instanceof Subscription), scheduleInboxes: collections.filter((c) => c instanceof ScheduleInbox), scheduleOutboxes: collections.filter((c) => c instanceof ScheduleOutbox) }; } /** * finds all calendars in this calendar home * * @return {Promise} */ async findAllCalendars() { return super.findAllByFilter((elm) => elm instanceof Calendar && !(elm instanceof ScheduleInbox) && !(elm instanceof Subscription) && !(elm instanceof DeletedCalendar)); } /** * Finds all deleted calendars in this calendar home * * @return {Promise} */ async findAllDeletedCalendars() { return super.findAllByFilter((elm) => elm instanceof DeletedCalendar); } /** * finds all subscriptions in this calendar home * * @return {Promise} */ async findAllSubscriptions() { return super.findAllByFilter((elm) => elm instanceof Subscription); } /** * finds all schedule inboxes in this calendar home * * @return {Promise} */ async findAllScheduleInboxes() { return super.findAllByFilter((elm) => elm instanceof ScheduleInbox); } /** * finds all schedule outboxes in this calendar home * * @return {Promise} */ async findAllScheduleOutboxes() { return super.findAllByFilter((elm) => elm instanceof ScheduleOutbox); } /** * creates a new calendar collection * * @param {string} displayname * @param {string} color * @param {string[]} supportedComponentSet * @param {number} order * @param {string=} timezone * @return {Promise} */ async createCalendarCollection(displayname, color, supportedComponentSet = null, order = null, timezone = null) { debug$3("creating a calendar collection"); const props = [{ name: [DAV, "resourcetype"], children: [{ name: [DAV, "collection"] }, { name: [IETF_CALDAV, "calendar"] }] }, { name: [DAV, "displayname"], value: displayname }, { name: [APPLE, "calendar-color"], value: color }, { name: [OWNCLOUD, "calendar-enabled"], value: "1" }]; if (timezone) { props.push({ name: [IETF_CALDAV, "calendar-timezone"], value: timezone }); } if (supportedComponentSet) { props.push({ name: [IETF_CALDAV, "supported-calendar-component-set"], children: supportedComponentSet.map((supportedComponent) => { return { name: [IETF_CALDAV, "comp"], attributes: [ ["name", supportedComponent] ] }; }) }); } if (order) { props.push({ name: [APPLE, "calendar-order"], value: order }); } const name = super._getAvailableNameFromToken(displayname); return super.createCollection(name, props); } /** * creates a new subscription * * @param {string} displayname * @param {string} color * @param {string} source * @param {number} order * @return {Promise} */ async createSubscribedCollection(displayname, color, source, order = null) { debug$3("creating a subscribed collection"); const props = [{ name: [DAV, "resourcetype"], children: [{ name: [DAV, "collection"] }, { name: [CALENDARSERVER, "subscribed"] }] }, { name: [DAV, "displayname"], value: displayname }, { name: [APPLE, "calendar-color"], value: color }, { name: [OWNCLOUD, "calendar-enabled"], value: "1" }, { name: [CALENDARSERVER, "source"], children: [{ name: [DAV, "href"], value: source }] }]; if (order) { props.push({ name: [APPLE, "calendar-order"], value: order }); } const name = super._getAvailableNameFromToken(displayname); return super.createCollection(name, props); } /** * Search all calendars the user has access to * This method makes use of Nextcloud's custom * calendar Search API * * Documentation about that API can be found at: ... * * @return {Promise} */ async search() { } /** * enables the birthday calendar for the Calendar Home that belongs to this user * * @return {Promise} */ async enableBirthdayCalendar() { const [skeleton] = getRootSkeleton( [NEXTCLOUD, "enable-birthday-calendar"] ); const xmlBody = serialize(skeleton); await this._request.post(this.url, {}, xmlBody); } } function addressBookPropSet(props) { const xmlified = []; Object.entries(props).forEach(([key, value]) => { switch (key) { case "{urn:ietf:params:xml:ns:carddav}addressbook-description": xmlified.push({ name: [IETF_CARDDAV, "addressbook-description"], value }); break; case "{http://owncloud.org/ns}enabled": xmlified.push({ name: [OWNCLOUD, "enabled"], value: value ? "1" : "0" }); break; } }); return xmlified; } class VCard extends DavObject { /** * Creates a VCard that is supposed to store address-data * as specified in RFC 6350. * * https://tools.ietf.org/html/rfc6350 * * @inheritDoc */ constructor(...args) { super(...args); super._exposeProperty("data", IETF_CARDDAV, "address-data", true); super._exposeProperty("hasphoto", NEXTCLOUD, "has-photo", false); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [IETF_CARDDAV, "address-data"] ]); } } const debug$2 = debugFactory("AddressBook"); class AddressBook extends davCollectionShareable(DavCollection) { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerObjectFactory("text/vcard", VCard); super._registerPropSetFactory(addressBookPropSet); super._exposeProperty("description", IETF_CARDDAV, "addressbook-description", true); super._exposeProperty("enabled", OWNCLOUD, "enabled", true); super._exposeProperty("readOnly", OWNCLOUD, "read-only"); } /** * finds all VCards in this address book * * @return {Promise} */ findAllVCards() { return super.findAllByFilter((elm) => elm instanceof VCard); } /** * finds all contacts in an address-book, but with filtered data. * * Example use: * findAllAndFilterBySimpleProperties(['EMAIL', 'UID', 'CATEGORIES', 'FN', 'TEL', 'NICKNAME', 'N']) * * @param {string[]} props * @return {Promise} */ async findAllAndFilterBySimpleProperties(props) { const children = []; props.forEach((prop) => { children.push({ name: [IETF_CARDDAV, "prop"], attributes: [["name", prop]] }); }); return this.addressbookQuery(null, [{ name: [DAV, "getetag"] }, { name: [DAV, "getcontenttype"] }, { name: [DAV, "resourcetype"] }, { name: [IETF_CARDDAV, "address-data"], children }, { name: [NEXTCLOUD, "has-photo"] }]); } /** * creates a new VCard object in this address book * * @param {string} data * @return {Promise} */ async createVCard(data) { debug$2("creating VCard object"); const name = uid("", "vcf"); const headers = { "Content-Type": "text/vcard; charset=utf-8" }; return super.createObject(name, headers, data); } /** * sends an addressbook query as defined in * https://tools.ietf.org/html/rfc6352#section-8.6 * * @param {object[]} filter * @param {object[]} prop * @param {number} limit * @param {string} test Either anyof or allof * @return {Promise} */ async addressbookQuery(filter, prop = null, limit = null, test = "anyof") { debug$2("sending an addressbook-query request"); const [skeleton] = getRootSkeleton( [IETF_CARDDAV, "addressbook-query"] ); if (!prop) { skeleton.children.push({ name: [DAV, "prop"], children: this._propFindList.map((p) => ({ name: p })) }); } else { skeleton.children.push({ name: [DAV, "prop"], children: prop }); } if (filter) { skeleton.children.push({ name: [IETF_CARDDAV, "filter"], attributes: [ ["test", test] ], children: filter }); } if (limit) { skeleton.children.push({ name: [IETF_CARDDAV, "limit"], children: [{ name: [IETF_CARDDAV, "nresults"], value: limit }] }); } const headers = { Depth: "1" }; const body = serialize(skeleton); const response = await this._request.report(this.url, headers, body); return super._handleMultiStatusResponse(response, AddressBook._isRetrievalPartial(prop)); } /** * sends an addressbook multiget query as defined in * https://tools.ietf.org/html/rfc6352#section-8.7 * * @param {string[]} hrefs * @param {object[]} prop * @return {Promise} */ async addressbookMultiget(hrefs = [], prop) { debug$2("sending an addressbook-multiget request"); if (hrefs.length === 0) { return []; } const headers = { Depth: "1" }; const body = this._buildMultiGetBody(hrefs, prop); const response = await this._request.report(this.url, headers, body); return super._handleMultiStatusResponse(response, AddressBook._isRetrievalPartial(prop)); } /** * sends an addressbook multiget query as defined in * https://tools.ietf.org/html/rfc6352#section-8.7 * and requests a download of the result * * @param {string[]} hrefs * @param {object[]} prop * @return {Promise<{Object}>} * @property {string | object} body * @property {number} status * @property {XMLHttpRequest} xhr */ async addressbookMultigetExport(hrefs = [], prop) { debug$2("sending an addressbook-multiget request and request download"); if (hrefs.length === 0) { return ""; } const headers = { Depth: "1" }; const body = this._buildMultiGetBody(hrefs, prop); return this._request.report(this.url + "?export", headers, body); } /** * * @param {string[]} hrefs * @param {object[]} prop * @return String * @private */ _buildMultiGetBody(hrefs, prop) { const [skeleton] = getRootSkeleton( [IETF_CARDDAV, "addressbook-multiget"] ); if (!prop) { skeleton.children.push({ name: [DAV, "prop"], children: this._propFindList.map((p) => ({ name: p })) }); } else { skeleton.children.push({ name: [DAV, "prop"], children: prop }); } hrefs.forEach((href) => { skeleton.children.push({ name: [DAV, "href"], value: href }); }); return serialize(skeleton); } /** * @inheritDoc */ static getPropFindList() { return super.getPropFindList().concat([ [IETF_CARDDAV, "addressbook-description"], [IETF_CARDDAV, "supported-address-data"], [IETF_CARDDAV, "max-resource-size"], [CALENDARSERVER, "getctag"], [OWNCLOUD, "enabled"], [OWNCLOUD, "read-only"] ]); } /** * checks if the prop part of a report requested partial data * * @param {object[]} prop * @return {boolean} * @private */ static _isRetrievalPartial(prop) { if (!prop) { return false; } const addressBookDataProperty = prop.find((p) => { return p.name[0] === IETF_CARDDAV && p.name[1] === "address-data"; }); if (!addressBookDataProperty) { return false; } return !!addressBookDataProperty.children; } } const debug$1 = debugFactory("AddressBookHome"); class AddressBookHome extends DavCollection { /** * @inheritDoc */ constructor(...args) { super(...args); super._registerCollectionFactory("{" + IETF_CARDDAV + "}addressbook", AddressBook); } /** * finds all address books in this address book home * * @return {Promise} */ async findAllAddressBooks() { return super.findAllByFilter((elm) => elm instanceof AddressBook); } /** * creates a new address book collection * * @param {string} displayname * @return {Promise} */ async createAddressBookCollection(displayname) { debug$1("creating an addressbook collection"); const props = [{ name: [DAV, "resourcetype"], children: [{ name: [DAV, "collection"] }, { name: [IETF_CARDDAV, "addressbook"] }] }, { name: [DAV, "displayname"], value: displayname }]; const name = super._getAvailableNameFromToken(displayname); return super.createCollection(name, props); } } /** * CDAV Library * * This library is part of the Nextcloud project * * @copyright Copyright (c) 2024 Richard Steinmetz * * @author Richard Steinmetz * * @license AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ function prinicipalPropSet(props) { const xmlified = []; Object.entries(props).forEach(([key, value]) => { switch (key) { case "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL": xmlified.push({ name: [IETF_CALDAV, "schedule-default-calendar-URL"], children: [ { name: ["DAV:", "href"], value } ] }); break; } }); return xmlified; } class Principal extends DavObject { /** * Creates an object that represents a single principal * as specified in RFC 3744 * * https://tools.ietf.org/html/rfc3744#section-2 * * @inheritDoc */ constructor(...args) { super(...args); Object.assign(this, { // house keeping _updatedProperties: [], // parsers / factories _propSetFactory: [] }); this._registerPropSetFactory(prinicipalPropSet); this._exposeProperty("displayname", DAV, "displayname"); this._exposeProperty("calendarUserType", IETF_CALDAV, "calendar-user-type"); this._exposeProperty("calendarUserAddressSet", IETF_CALDAV, "calendar-user-address-set"); this._exposeProperty("principalUrl", DAV, "principal-URL"); this._exposeProperty("email", SABREDAV, "email-address"); this._exposeProperty("language", NEXTCLOUD, "language"); this._exposeProperty("calendarHomes", IETF_CALDAV, "calendar-home-set"); this._exposeProperty("scheduleInbox", IETF_CALDAV, "schedule-inbox-URL"); this._exposeProperty("scheduleOutbox", IETF_CALDAV, "schedule-outbox-URL"); this._exposeProperty("scheduleDefaultCalendarUrl", IETF_CALDAV, "schedule-default-calendar-URL", true); this._exposeProperty("addressBookHomes", IETF_CARDDAV, "addressbook-home-set"); this._exposeProperty("roomType", NEXTCLOUD, "room-type"); this._exposeProperty("roomSeatingCapacity", NEXTCLOUD, "room-seating-capacity"); this._exposeProperty("roomBuildingAddress", NEXTCLOUD, "room-building-address"); this._exposeProperty("roomBuildingStory", NEXTCLOUD, "room-building-story"); this._exposeProperty("roomBuildingRoomNumber", NEXTCLOUD, "room-building-room-number"); this._exposeProperty("roomFeatures", NEXTCLOUD, "room-features"); Object.defineProperties(this, { principalScheme: { get: () => { const baseUrl = this._request.pathname(this._request.baseUrl); let principalURI = this.url.slice(baseUrl.length); if (principalURI.slice(-1) === "/") { principalURI = principalURI.slice(0, -1); } return "principal:" + principalURI; } }, userId: { get: () => { if (this.calendarUserType !== "INDIVIDUAL") { return null; } return this.url.split("/").splice(-2, 2)[this.url.endsWith("/") ? 0 : 1]; } }, groupId: { get: () => { if (this.calendarUserType !== "GROUP") { return null; } return this.url.split("/").splice(-2, 2)[this.url.endsWith("/") ? 0 : 1]; } }, resourceId: { get: () => { if (this.calendarUserType !== "RESOURCE") { return null; } return this.url.split("/").splice(-2, 2)[this.url.endsWith("/") ? 0 : 1]; } }, roomId: { get: () => { if (this.calendarUserType !== "ROOM") { return null; } return this.url.split("/").splice(-2, 2)[this.url.endsWith("/") ? 0 : 1]; } }, roomAddress: { get: () => { const data = [ this.roomBuildingRoomNumber, this.roomBuildingStory, this.roomBuildingAddress ]; return data.filter((value) => !!value).join(", "); } } }); } /** * Expose property to the outside and track changes if it's mutable * * @protected * @param {string} localName * @param {string} xmlNamespace * @param {string} xmlName * @param {boolean} mutable * @return void */ _exposeProperty(localName, xmlNamespace, xmlName, mutable = false) { if (mutable) { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)], set: (val) => { this._props["{".concat(xmlNamespace, "}").concat(xmlName)] = val; if (this._updatedProperties.indexOf("{".concat(xmlNamespace, "}").concat(xmlName)) === -1) { this._updatedProperties.push("{".concat(xmlNamespace, "}").concat(xmlName)); } } }); } else { Object.defineProperty(this, localName, { get: () => this._props["{".concat(xmlNamespace, "}").concat(xmlName)] }); } } /** * @protected * @param factory * @return void */ _registerPropSetFactory(factory) { this._propSetFactory.push(factory); } /** * @inheritDoc * * @param {PrincipalPropfindOptions} options */ static getPropFindList(options = {}) { const list = [ [DAV, "displayname"], [IETF_CALDAV, "calendar-user-type"], [IETF_CALDAV, "calendar-user-address-set"], [DAV, "principal-URL"], [DAV, "alternate-URI-set"], [SABREDAV, "email-address"], [NEXTCLOUD, "language"] ]; if (options.enableCalDAV) { list.push( [IETF_CALDAV, "calendar-home-set"], [IETF_CALDAV, "schedule-inbox-URL"], [IETF_CALDAV, "schedule-outbox-URL"], [IETF_CALDAV, "schedule-default-calendar-URL"] ); } if (options.enableCalDAVResourceBooking || options.enableCalDAV) { list.push( // Room and Resource booking related [NEXTCLOUD, "resource-type"], [NEXTCLOUD, "resource-vehicle-type"], [NEXTCLOUD, "resource-vehicle-make"], [NEXTCLOUD, "resource-vehicle-model"], [NEXTCLOUD, "resource-vehicle-is-electric"], [NEXTCLOUD, "resource-vehicle-range"], [NEXTCLOUD, "resource-vehicle-seating-capacity"], [NEXTCLOUD, "resource-contact-person"], [NEXTCLOUD, "resource-contact-person-vcard"], [NEXTCLOUD, "room-type"], [NEXTCLOUD, "room-seating-capacity"], [NEXTCLOUD, "room-building-address"], [NEXTCLOUD, "room-building-story"], [NEXTCLOUD, "room-building-room-number"], [NEXTCLOUD, "room-features"] ); } if (options.enableCardDAV) { list.push( [IETF_CARDDAV, "addressbook-home-set"] ); } return list; } /** * Sends a PropPatch request to update the principal's properties. * The request is only made if properties actually changed. * * @return {Promise} */ async update() { if (this._updatedProperties.length === 0) { return; } const properties = {}; this._updatedProperties.forEach((updatedProperty) => { properties[updatedProperty] = this._props[updatedProperty]; }); const propSet = this._propSetFactory.reduce((arr, p) => [...arr, ...p(properties)], []); const [skeleton, dPropSet] = getRootSkeleton( [DAV, "propertyupdate"], [DAV, "set"], [DAV, "prop"] ); dPropSet.push(...propSet); const body = serialize(skeleton); await this._request.propPatch(this._url, {}, body); } } const debug = debugFactory("index.js"); class DavClient { /** * @param {object} options * @param {string} options.rootUrl * @param {Function} xhrProvider * @param {object} factories */ constructor(options, xhrProvider = null, factories = {}) { this.rootUrl = null; if (options.rootUrl.slice(-1) !== "/") { options.rootUrl += "/"; } Object.assign(this, options); this.advertisedFeatures = []; this.currentUserPrincipal = null; this.principalCollections = []; this.calendarHomes = []; this.publicCalendarHome = null; this.addressBookHomes = []; this.parser = new Parser(); this._isConnected = false; this._request = new Request(this.rootUrl, this.parser, xhrProvider); } /** * initializes the DAVClient * @param {object} options * @return {Promise} */ async connect(options = { enableCalDAV: false, enableCardDAV: false }) { if (this._isConnected) { return this; } if (!this.rootUrl) { throw new Error("No rootUrl configured"); } const principalUrl = await this._discoverPrincipalUri(); debug("PrincipalURL: ".concat(principalUrl)); const propFindList = Principal.getPropFindList(options); if (options.enableCalDAV || options.enableCardDAV) { propFindList.push( [DAV, "principal-collection-set"], [DAV, "supported-report-set"] ); } const response = await this._request.propFind(principalUrl, propFindList); this.currentUserPrincipal = new Principal(null, this._request, principalUrl, response.body); this._extractAdvertisedDavFeatures(response.xhr); this._extractAddressBookHomes(response.body); this._extractCalendarHomes(response.body); this._extractPrincipalCollectionSets(response.body); this._createPublicCalendarHome(); this._isConnected = true; return this; } // /** // * @returns {Promise<[any , any , any , any , any , any , any , any , any , any]>} // */ // async sync() { // const promises = []; // // // Ideally we would also check for new calendar-homes and // // new addressbook-homes as well, but then Nextcloud will // // ever only send provide one each, so we omit this step // // to cut down network traffic // // this.calendarHomes.forEach((calendarHome) => { // promises.push(calendarHome.sync()); // }); // this.addressbookHomes.forEach((addressbookHome) => { // promises.push(addressbookHome.sync()); // }); // // return Promise.all(promises); // } /** * performs a principal property search based on a principal's displayname * * @param {string} name * @return {Promise} */ async principalPropertySearchByDisplayname(name) { return this.principalPropertySearch([ { name: [DAV, "displayname"] } ], name); } /** * performs a principal property search based on a principal's displayname OR email address * * @param {string} value * @return {Promise} */ async principalPropertySearchByDisplaynameOrEmail(value) { return this.principalPropertySearch([ { name: [DAV, "displayname"] }, { name: [SABREDAV, "email-address"] } ], value, "anyof"); } /** * Performs a principal property based on the address of a room * * @param {string} address Address of the building the room is in * @return {Promise} */ async principalPropertySearchByAddress(address) { return this.principalPropertySearch([ { name: [NEXTCLOUD, "room-building-address"] } ], address); } /** * Performs a principal property search based on the address and story of a room * * @param {string} address Address of the building the room is in * @param {string} story Story inside the building the room is in * @return {Promise<[]>} */ async principalPropertySearchByAddressAndStory(address, story) { const [skeleton] = getRootSkeleton( [DAV, "principal-property-search"] ); skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [{ name: [NEXTCLOUD, "room-building-address"] }] }, { name: [DAV, "match"], value: address }] }); skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [{ name: [NEXTCLOUD, "room-building-story"] }] }, { name: [DAV, "match"], value: story }] }); skeleton.children.push({ name: [DAV, "prop"], children: Principal.getPropFindList({ enableCalDAV: true }).map((propFindListItem) => ({ name: propFindListItem })) }); skeleton.children.push({ name: [DAV, "apply-to-principal-collection-set"] }); const xml = serialize(skeleton); return this._request.report(this.rootUrl, { Depth: 0 }, xml).then((response) => { const result = []; Object.entries(response.body).forEach(([path, props]) => { const url = this._request.pathname(path); result.push(new Principal(null, this._request, url, props)); }); return result; }); } /** * Performs a principal property search based on multiple advanced filters * * @param {object} query The destructuring query object * @param {string=} query.displayName The display name to filter by * @param {number=} query.capacity The minimum required seating capacity * @param {string[]=} query.features The features to filter by * @param {string=} query.roomType The room type to filter by * @return {Promise} */ async advancedPrincipalPropertySearch(query) { const [skeleton] = getRootSkeleton([DAV, "principal-property-search"]); skeleton.attributes = [ ["test", "allof"] ]; const { displayName, capacity, features, roomType } = query; if (displayName) { skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [ { name: [DAV, "displayname"] } ] }, { name: [DAV, "match"], value: displayName }] }); } if (capacity) { skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [{ name: [NEXTCLOUD, "room-seating-capacity"] }] }, { name: [DAV, "match"], value: capacity }] }); } if (features && features.length > 0) { skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [{ name: [NEXTCLOUD, "room-features"] }] }, { name: [DAV, "match"], value: features.join(",") }] }); } if (roomType) { skeleton.children.push({ name: [DAV, "property-search"], children: [{ name: [DAV, "prop"], children: [{ name: [NEXTCLOUD, "room-type"] }] }, { name: [DAV, "match"], value: roomType }] }); } if (skeleton.children.length === 0) { return []; } skeleton.children.push({ name: [DAV, "prop"], children: Principal.getPropFindList({ enableCalDAV: true }).map((propFindListItem) => ({ name: propFindListItem })) }); skeleton.children.push({ name: [DAV, "apply-to-principal-collection-set"] }); const xml = serialize(skeleton); const response = await this._request.report(this.rootUrl, { Depth: 0 }, xml); return Object.entries(response.body).map(([path, props]) => { const url = this._request.pathname(path); return new Principal(null, this._request, url, props); }); } /** * performs a principal property search * @see https://tools.ietf.org/html/rfc3744#section-9.4 * * @param {Array} props * @param {string} match * @param {string} test 'anyof', 'allof' or none * @return {Promise} */ async principalPropertySearch(props, match, test) { const [skeleton, propSearch] = getRootSkeleton( [DAV, "principal-property-search"], [DAV, "property-search"] ); if (test) { skeleton.attributes = [ ["test", test] ]; } propSearch.push({ name: [DAV, "prop"], children: props }, { name: [DAV, "match"], value: match }); skeleton.children.push({ name: [DAV, "prop"], children: Principal.getPropFindList({ enableCalDAV: true }).map((propFindListItem) => ({ name: propFindListItem })) }); skeleton.children.push({ name: [DAV, "apply-to-principal-collection-set"] }); const xml = serialize(skeleton); return this._request.report(this.rootUrl, { Depth: 0 }, xml).then((response) => { const result = []; Object.entries(response.body).forEach(([path, props2]) => { const url = this._request.pathname(path); result.push(new Principal(null, this._request, url, props2)); }); return result; }); } /** * finds one principal at a given principalUrl * * @param {string} principalUrl * @return {Promise} */ async findPrincipal(principalUrl) { return this._request.propFind(principalUrl, Principal.getPropFindList()).then(({ body }) => { return new Principal(null, this._request, principalUrl, body); }).catch((err) => { console.debug(err); }); } /** * finds all principals in a collection at a given principalCollectionUrl * * @param {string} principalCollectionUrl * @param {import('./models/principal.js').PrincipalPropfindOptions} options Passed to Principal.getPropFindList() * @return {Promise} */ async findPrincipalsInCollection(principalCollectionUrl, options = {}) { try { const { body } = await this._request.propFind( principalCollectionUrl, Principal.getPropFindList(options), 1 ); const principals = Object.entries(body).filter(([principalUrl]) => !principalCollectionUrl.endsWith(principalUrl)).map(([principalUrl, principal]) => new Principal( null, this._request, principalUrl, principal )); return principals; } catch (err) { console.debug(err); } } /** * discovers the accounts principal uri solely based on rootURL * * @return {Promise} * @private */ async _discoverPrincipalUri() { const response = await this._request.propFind(this.rootUrl, [ [DAV, "current-user-principal"] ], 0); if (!response.body["{DAV:}current-user-principal"]) { throw new Error("Error retrieving current user principal"); } if (response.body["{DAV:}current-user-principal"].type === "unauthenticated") { throw new Error("Current user is not authenticated"); } return this._request.pathname(response.body["{DAV:}current-user-principal"].href); } /** * discovers all calendar-homes in this account, all principal collections * and advertised features * * a user will most commonly only have one calendar-home, * the CalDAV standard allows multiple calendar-homes though * * @param {object} props * @return void * @private */ async _extractCalendarHomes(props) { const calendarHomes = props["{".concat(IETF_CALDAV, "}calendar-home-set")]; if (!calendarHomes) { return; } this.calendarHomes = calendarHomes.map((calendarHome) => { const url = this._request.pathname(calendarHome); return new CalendarHome(this, this._request, url, props); }); } /** * discovers all address-book-homes in this account, all principal collections * and advertised features * * a user will most commonly only have one address-book-home, * the CardDAV standard allows multiple address-book-homes though * * @param {object} props * @return void * @private */ async _extractAddressBookHomes(props) { const addressBookHomes = props["{".concat(IETF_CARDDAV, "}addressbook-home-set")]; if (!addressBookHomes) { return; } this.addressBookHomes = addressBookHomes.map((addressbookHome) => { const url = this._request.pathname(addressbookHome); return new AddressBookHome(this, this._request, url, props); }); } /** * extracts principalCollection Information from an existing props object * returned from the server * * @param {object} props * @return void * @private */ _extractPrincipalCollectionSets(props) { const principalCollectionSets = props["{".concat(DAV, "}principal-collection-set")]; this.principalCollections = principalCollectionSets.map((principalCollection) => { return this._request.pathname(principalCollection); }); } /** * extracts the advertised features supported by the DAV server * * @param {XMLHttpRequest} xhr * @return void * @private */ _extractAdvertisedDavFeatures(xhr) { const dav = xhr.getResponseHeader("DAV"); this.advertisedFeatures.push(...dav.split(",").map((s) => s.trim())); } /** * Creates a public calendar home * * @return void * @private */ _createPublicCalendarHome() { const url = this._request.pathname(this.rootUrl) + "public-calendars/"; this.publicCalendarHome = new CalendarHome(this, this._request, url, {}); } } exports.debug = debugFactory; exports.default = DavClient; exports.namespaces = namespaceUtility;