UNPKG

@nextcloud/cdav-library

Version:

CalDAV and CardDAV client library for Nextcloud

1,330 lines 114 kB
"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 = `{${node.namespaceURI}}${node.localName}`; if (!this.canParse(propertyName)) { throw new Error(`Unable to parse unknown property "${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(`{${ns}}${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(`{${ns}}${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(`{${ns}}${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(`{${ns}}${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(`{${propNode.namespaceURI}}${propNode.localName}`)) { parsedProperties[`{${propNode.namespaceURI}}${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<void>} */ 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<DavObject>} Promise that resolves to the copied DavObject */ async copy(collection, overwrite = false, headers = {}) { debug$8(`copying ${this.url} from ${this._parent.url} to ${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<void>} */ async move(collection, overwrite = false, headers = {}) { debug$8(`moving ${this.url} from ${this._parent.url} to ${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<void>} */ async update() { if (this.isPartial() || !this.isDirty() || !this.data) { return; } const headers = {}; if (this.contenttype) { headers["Content-Type"] = `${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<void>} */ 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[`{${xmlNamespace}}${xmlName}`], set: (val) => { this._isDirty = true; this._props[`{${xmlNamespace}}${xmlName}`] = val; } }); } else { Object.defineProperty(this, localName, { get: () => this._props[`{${xmlNamespace}}${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, p