@nextcloud/cdav-library
Version:
CalDAV and CardDAV client library for Nextcloud
1,330 lines • 114 kB
JavaScript
"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