nephele
Version:
Highly customizable and extensible WebDAV server for Node.js and Express.
931 lines • 40.5 kB
JavaScript
import zlib from 'node:zlib';
import { pipeline, Readable } from 'node:stream';
import path from 'node:path';
import * as xml2js from 'xml2js';
import contentType from 'content-type';
import { splitn } from '@sciactive/splitn';
import vary from 'vary';
import { BadRequestError, EncodingNotSupportedError, MediaTypeNotSupportedError, MethodNotSupportedError, PreconditionFailedError, ResourceNotFoundError, ResourceNotModifiedError, UnauthorizedError, } from '../Errors/index.js';
import { getAdapter, _getAdapter } from '../Options.js';
const matchResource = /^<.+?>\s*/;
const matchList = /^\([^\)]+?(?:"[^"]+"[^\)]*?)*\)\s*/;
const matchNot = /^Not\s*/;
const matchNolock = /^<DAV:no-lock>\s*/;
const matchToken = /^<[^>]+>\s*/;
const matchEtag = /^\[(?:W\/)?"[^"]+"\]\s*/;
export class Method {
constructor(opts) {
this.DEV = process.env.NODE_ENV !== 'production';
this.xmlParser = new xml2js.Parser({
xmlns: true,
});
this.xmlBuilder = new xml2js.Builder({
xmldec: { version: '1.0', encoding: 'UTF-8' },
...(this.DEV
? {
renderOpts: {
pretty: true,
},
}
: {
renderOpts: {
indent: '',
newline: '',
pretty: false,
},
}),
});
this.opts = opts;
}
async runPlugins(request, response, event, data = {}) {
let ended = false;
for (let plugin of response.locals.plugins) {
if (event in plugin) {
const fn = plugin[event];
if (fn) {
const result = await fn.bind(plugin)(request, response, data);
if (result === false) {
ended = true;
}
}
}
}
return ended;
}
async run(request, _response) {
throw new MethodNotSupportedError(`${request.method} is not supported on this server.`);
}
async checkAuthorization(request, response, method, url) {
await this.runPlugins(request, response, 'beforeCheckAuthorization', {
method: this,
methodName: method,
url,
});
if (!(await response.locals.adapter.isAuthorized(url ||
new URL(request.originalUrl, `${request.protocol}://${request.headers.host}`), method || request.method, response.locals.baseUrl, response.locals.user))) {
throw new UnauthorizedError('Unauthorized.');
}
await this.runPlugins(request, response, 'afterCheckAuthorization', {
method: this,
methodName: method,
url,
});
}
async getAdapter(request, response, unencodedPath) {
const { adapter } = await getAdapter(unencodedPath.replace(/\/?$/, () => '/'), response.locals.adapterConfig, { request, response });
return adapter;
}
async getAdapterBaseUrl(response, unencodedPath) {
const { baseUrl } = _getAdapter(unencodedPath.replace(/\/?$/, () => '/'), response.locals.adapterConfig);
return baseUrl;
}
async pathsHaveSameAdapter(response, unencodedPathA, unencodedPathB) {
return ((await this.getAdapterBaseUrl(response, unencodedPathA)) ===
(await this.getAdapterBaseUrl(response, unencodedPathB)));
}
async isAdapterRoot(request, response, url) {
if (url.pathname.replace(/\/?$/, () => '/') ===
request.baseUrl.replace(/\/?$/, () => '/')) {
return true;
}
const resourceAdapter = _getAdapter(decodeURIComponent(new URL(url.toString().replace(/\/?$/, () => '/')).pathname.substring(request.baseUrl.length)), response.locals.adapterConfig).adapter;
const parentAdapter = _getAdapter(decodeURIComponent(path
.dirname(new URL(url.toString().replace(/\/?$/, () => '/')).pathname.substring(request.baseUrl.length))
.replace(/\/?$/, () => '/')), response.locals.adapterConfig).adapter;
return resourceAdapter !== parentAdapter;
}
async getParentResource(request, response, resource) {
const url = await resource.getCanonicalUrl();
if (url.pathname === '/' || url.pathname === request.baseUrl) {
return undefined;
}
const parentPath = decodeURIComponent(path.dirname(url.pathname));
const { adapter: rawParentAdapter, baseUrl: parentBaseUrl } = await getAdapter(parentPath.replace(/\/?$/, () => '/'), response.locals.adapterConfig, { request, response });
const splitPath = url.pathname.replace(/\/?$/, '').split('/');
const newPath = splitPath
.slice(0, -1)
.join('/')
.replace(/\/?$/, () => '/');
if (!newPath.startsWith(request.baseUrl.replace(/\/?$/, () => '/'))) {
return undefined;
}
const parentAdapter = parentBaseUrl === response.locals.baseUrl.pathname
? response.locals.adapter
: rawParentAdapter;
return await parentAdapter.getResource(new URL(newPath, `${request.protocol}://${request.headers.host}`), new URL(path.join(request.baseUrl || '/', parentBaseUrl), `${request.protocol}://${request.headers.host}`));
}
async removeAndDeleteTimedOutLocks(locks) {
const currentLocks = [];
for (let lock of locks) {
if (lock.date.getTime() + lock.timeout <= new Date().getTime()) {
try {
await lock.delete();
}
catch (e) {
}
}
else {
currentLocks.push(lock);
}
}
return currentLocks;
}
async getCurrentResourceLocks(resource) {
const locks = await resource.getLocks();
return await this.removeAndDeleteTimedOutLocks(locks);
}
async getCurrentResourceLocksByUser(resource, user) {
const locks = await resource.getLocksByUser(user);
return await this.removeAndDeleteTimedOutLocks(locks);
}
async getLocksGeneral(request, response, resource, getLocks) {
const resourceLocks = await getLocks(resource);
const locks = {
all: [...resourceLocks],
resource: resourceLocks,
depthZero: [],
depthInfinity: [],
};
let parent = await this.getParentResource(request, response, resource);
let firstLevelParent = true;
while (parent) {
const parentLocks = await getLocks(parent);
for (let lock of parentLocks) {
if (lock.depth === 'infinity') {
locks.depthInfinity.push(lock);
locks.all.push(lock);
}
else if (firstLevelParent && lock.depth === '0') {
locks.depthZero.push(lock);
locks.all.push(lock);
}
}
parent = await this.getParentResource(request, response, parent);
firstLevelParent = false;
}
return locks;
}
async getLocks(request, response, resource) {
return await this.getLocksGeneral(request, response, resource, async (resource) => (await this.getCurrentResourceLocks(resource)).filter((lock) => !lock.provisional));
}
async getLocksByUser(request, response, resource, user) {
return await this.getLocksGeneral(request, response, resource, async (resource) => (await this.getCurrentResourceLocksByUser(resource, user)).filter((lock) => !lock.provisional));
}
async getProvisionalLocks(request, response, resource) {
return await this.getLocksGeneral(request, response, resource, async (resource) => (await this.getCurrentResourceLocks(resource)).filter((lock) => lock.provisional));
}
async getLockPermission(request, response, resource, user) {
const locks = await this.getLocks(request, response, resource);
const lockTokens = this.getRequestLockTockens(request);
if (!locks.all.length) {
return 2;
}
const userLocks = await this.getLocksByUser(request, response, resource, user);
const lockTokenSet = new Set(lockTokens);
if (userLocks.all.find((userLock) => lockTokenSet.has(userLock.token))) {
return 2;
}
if (request.method === 'LOCK') {
let code = 0;
for (let lock of locks.resource) {
if (lock.scope === 'exclusive') {
return 0;
}
else if (lock.scope === 'shared') {
code = 3;
}
}
for (let lock of locks.depthInfinity) {
if (lock.scope === 'exclusive') {
return 0;
}
else if (lock.scope === 'shared') {
code = 3;
}
}
for (let lock of locks.depthZero) {
if (lock.scope === 'exclusive') {
return 1;
}
else if (lock.scope === 'shared') {
code = 3;
}
}
return code;
}
else {
if (locks.depthInfinity.length || locks.resource.length) {
return 0;
}
if (locks.depthZero.length) {
return 1;
}
return 0;
}
}
getRequestLockTockens(request) {
const lockTokens = [];
const ifHeader = request.get('If') || '';
const matches = ifHeader.match(/<urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}>/g);
if (matches) {
for (let match of matches) {
lockTokens.push(match.slice(1, -1));
}
}
return lockTokens;
}
async checkIfHeader(request, response) {
let ifHeader = request.get('If')?.trim().replace(/\n/g, ' ');
if (ifHeader == null) {
return;
}
if (ifHeader === '') {
throw new BadRequestError('The If header, if provided, must not be empty.');
}
const requestURL = this.getRequestUrl(request);
const parsedHeader = {};
let currentResource = requestURL.toString();
const startedWithResource = ifHeader.startsWith('<');
while (ifHeader.length) {
const resourceMatch = ifHeader.match(matchResource);
const listMatch = ifHeader.match(matchList);
if (resourceMatch) {
if (!startedWithResource) {
throw new BadRequestError('Tagged-lists and no-tag-lists must not be mixed in the If header.');
}
const resource = resourceMatch[0].trim();
currentResource = resource.slice(1, -1);
if (currentResource.match(/(?:^\/)\.\.?(?:$|\/)/)) {
throw new BadRequestError('Resource URIs in the If header must not contain dot segments.');
}
ifHeader = ifHeader.replace(matchResource, '');
}
else if (listMatch) {
let list = listMatch[0].trim().slice(1, -1).trim();
const listObj = {
tokens: [],
etags: [],
nolock: false,
notTokens: [],
notEtags: [],
notNolock: false,
};
if (list === '') {
throw new BadRequestError('All lists in the If header must have at least one condition.');
}
while (list.length) {
const notMatch = list.match(matchNot);
if (notMatch) {
list = list.replace(matchNot, '');
}
const nolockMatch = list.match(matchNolock);
const tokenMatch = list.match(matchToken);
const etagMatch = list.match(matchEtag);
if (nolockMatch) {
if (notMatch) {
listObj.notNolock = true;
}
else {
listObj.nolock = true;
}
list = list.replace(matchNolock, '');
}
else if (tokenMatch) {
let token = tokenMatch[0].trim().slice(1, -1);
if (notMatch) {
listObj.notTokens.push(token);
}
else {
listObj.tokens.push(token);
}
list = list.replace(matchToken, '');
}
else if (etagMatch) {
let etag = etagMatch[0]
.trim()
.replace(/^\[(?:W\/)?"/, '')
.slice(0, -2);
if (notMatch) {
listObj.notEtags.push(etag);
}
else {
listObj.etags.push(etag);
}
list = list.replace(matchEtag, '');
}
else {
throw new BadRequestError("The server doesn't recognize the submitted If header.");
}
}
if (!parsedHeader[currentResource]) {
parsedHeader[currentResource] = [];
}
parsedHeader[currentResource].push(listObj);
ifHeader = ifHeader.replace(matchList, '');
}
else {
throw new BadRequestError("The server doesn't recognize the submitted If header.");
}
}
if (Object.keys(parsedHeader).length === 0) {
throw new BadRequestError('The If header, if provided, must contain at least one list with a condition.');
}
for (let [resourceUri, lists] of Object.entries(parsedHeader)) {
const url = new URL(resourceUri, requestURL);
let etag = '';
let tokens = [];
const [needEtag, needTokens] = lists.reduce(([needEtag, needTokens], list) => [
!!(needEtag || list.etags.length || list.notEtags.length),
!!(needTokens || list.tokens.length || list.notTokens.length),
], [false, false]);
if (needEtag || needTokens) {
try {
await this.checkAuthorization(request, response, 'GET', url);
const { adapter: newAdapter, baseUrl } = await getAdapter(decodeURIComponent(url.pathname).replace(/\/?$/, () => '/'), response.locals.adapterConfig, { request, response });
const adapter = baseUrl === response.locals.baseUrl.pathname
? response.locals.adapter
: newAdapter;
const resource = await adapter.getResource(url, new URL(`${request.protocol}://${request.headers.host}${path.join(request.baseUrl || '/', baseUrl)}`));
if (needEtag) {
etag = await resource.getEtag();
}
if (needTokens) {
tokens = (await this.getLocks(request, response, resource)).all.map((lock) => lock.token);
}
}
catch (e) {
if (e instanceof UnauthorizedError) {
throw new PreconditionFailedError('If header check failed.');
}
if (!(e instanceof ResourceNotFoundError)) {
throw e;
}
}
}
listLoop: for (let list of lists) {
if (list.nolock) {
continue;
}
for (let curEtag of list.etags) {
if (etag === '' || etag !== curEtag) {
continue listLoop;
}
}
for (let curEtag of list.notEtags) {
if (etag !== '' && etag === curEtag) {
continue listLoop;
}
}
for (let curToken of list.tokens) {
if (!tokens.includes(curToken)) {
continue listLoop;
}
}
for (let curToken of list.notTokens) {
if (tokens.includes(curToken)) {
continue listLoop;
}
}
return;
}
}
throw new PreconditionFailedError('If header check failed.');
}
async checkConditionalHeaders(request, response) {
const requestURL = this.getRequestUrl(request);
let resource;
let newResource = false;
try {
resource = await response.locals.adapter.getResource(requestURL, response.locals.baseUrl);
}
catch (e) {
if (e instanceof ResourceNotFoundError) {
resource = await response.locals.adapter.newResource(requestURL, response.locals.baseUrl);
newResource = true;
}
else {
throw e;
}
}
const ifMatch = request.get('If-Match')?.trim();
const ifMatchEtags = (ifMatch || '').split(',').map((value) => value
.trim()
.replace(/^(?:W\/)?["']/, '')
.replace(/["']$/, ''));
const ifNoneMatch = request.get('If-None-Match')?.trim();
const ifNoneMatchEtags = (ifNoneMatch || '').split(',').map((value) => value
.trim()
.replace(/^(?:W\/)?["']/, '')
.replace(/["']$/, ''));
const ifUnmodifiedSince = request.get('If-Unmodified-Since')?.trim();
const ifModifiedSince = request.get('If-Modified-Since')?.trim();
let etag = '';
let lastModified = new Date(0);
if (!newResource) {
const properties = await resource.getProperties();
etag = await resource.getEtag();
const lastModifiedString = await properties.get('getlastmodified');
if (typeof lastModifiedString !== 'string') {
throw new Error('Last modified date property is not a string.');
}
lastModified = new Date(lastModifiedString);
}
if (ifMatch != null &&
((ifMatch === '*' && newResource) ||
(ifMatch !== '*' && (etag === '' || !ifMatchEtags.includes(etag))))) {
throw new PreconditionFailedError('If-Match header check failed.');
}
if (ifUnmodifiedSince != null &&
new Date(ifUnmodifiedSince) < lastModified) {
throw new PreconditionFailedError('If-Unmodified-Since header check failed.');
}
let mustIgnoreIfModifiedSince = false;
if (ifNoneMatch != null) {
if ((ifNoneMatch === '*' && !newResource) ||
(ifNoneMatch !== '*' && etag !== '' && ifNoneMatchEtags.includes(etag))) {
if (request.method === 'GET' || request.method === 'HEAD') {
const cacheControl = this.getCacheControl(request);
if (!cacheControl['no-cache'] && cacheControl['max-age'] !== 0) {
throw new ResourceNotModifiedError(newResource ? undefined : etag, newResource ? undefined : lastModified);
}
}
else {
throw new PreconditionFailedError('If-None-Match header check failed.');
}
}
else {
mustIgnoreIfModifiedSince = true;
}
}
if (!mustIgnoreIfModifiedSince &&
(request.method === 'GET' || request.method === 'HEAD') &&
ifModifiedSince != null &&
new Date(ifModifiedSince) >= lastModified) {
const cacheControl = this.getCacheControl(request);
if (!cacheControl['no-cache'] && cacheControl['max-age'] !== 0) {
throw new ResourceNotModifiedError(newResource ? undefined : etag, newResource ? undefined : lastModified);
}
}
await this.checkIfHeader(request, response);
}
getRequestUrl(request) {
return new URL(request.originalUrl, `${request.protocol}://${request.headers.host}`);
}
getRequestedEncoding(request, response) {
const acceptEncoding = request.get('Accept-Encoding') || 'identity, *;q=0.5';
const supported = ['gzip', 'deflate', 'br', 'identity'];
const encodings = acceptEncoding
.split(',')
.map((value) => value.trim().split(';'))
.map((value) => [
value[0],
parseFloat(value[1]?.replace(/^q=/, '') || '1.0'),
]);
encodings.sort((a, b) => b[1] - a[1]);
let encoding = '';
while (![...supported, 'x-gzip', '*'].includes(encoding)) {
if (!encodings.length) {
throw new EncodingNotSupportedError('Requested content encoding is not supported.');
}
encoding = encodings.splice(0, 1)[0][0];
}
if (encoding === '*') {
encoding =
supported.find((check) => encodings.find(([check2]) => check === check2) == null) || 'gzip';
}
response.locals.debug(`Requested encoding: ${encoding}.`);
return encoding;
}
getCacheControl(request) {
const cacheControlHeader = request.get('Cache-Control') || '*';
const cacheControl = {};
cacheControlHeader.split(',').forEach((directive) => {
if (directive.startsWith('max-age=') ||
directive.startsWith('s-maxage=') ||
directive.startsWith('stale-while-revalidate=') ||
directive.startsWith('stale-if-error=') ||
directive.startsWith('max-stale=') ||
directive.startsWith('min-fresh=')) {
const [name, value] = directive.split('=');
cacheControl[name] = parseInt(value);
}
else {
cacheControl[directive] = true;
}
});
return cacheControl;
}
getRequestData(request, response) {
const url = this.getRequestUrl(request);
const encoding = this.getRequestedEncoding(request, response);
const cacheControl = this.getCacheControl(request);
return { url, encoding, cacheControl };
}
getRequestDestination(request) {
const destinationHeader = request.get('Destination');
let destination = undefined;
if (destinationHeader != null) {
if (destinationHeader.match(/(?:^\/)\.\.?(?:$|\/)/)) {
throw new BadRequestError('Destination header must not contain dot segments.');
}
try {
destination = new URL(destinationHeader, new URL(request.originalUrl, `${request.protocol}://${request.headers.host}`));
}
catch (e) {
throw new BadRequestError('Destination header must be a valid URI.');
}
}
return destination;
}
async getBodyStream(request, response) {
if (request.get('Content-Length') === '0') {
return Readable.from(Buffer.from([]));
}
response.locals.debug('Getting body stream.');
let stream = request;
let encoding = request.get('Content-Encoding');
switch (encoding) {
case 'gzip':
case 'x-gzip':
stream = pipeline(request, zlib.createGunzip(), (e) => {
if (e) {
throw new Error('Compression pipeline failed: ' + e);
}
});
break;
case 'deflate':
stream = pipeline(request, zlib.createInflate(), (e) => {
if (e) {
throw new Error('Compression pipeline failed: ' + e);
}
});
break;
case 'br':
stream = pipeline(request, zlib.createBrotliDecompress(), (e) => {
if (e) {
throw new Error('Compression pipeline failed: ' + e);
}
});
break;
case 'identity':
break;
default:
if (encoding != null) {
throw new MediaTypeNotSupportedError('Provided content encoding is not supported.');
}
break;
}
return stream;
}
async sendBodyContent(response, content, encoding) {
vary(response, 'Accept-Encoding');
const cacheControl = response.getHeader('Cache-Control');
const noTransform = typeof cacheControl === 'string' &&
cacheControl.match(/(?:^|,)\s*?no-transform\s*?(?:,|$)/);
if (!this.opts.compression || encoding === 'identity' || noTransform) {
response.locals.debug(`Response encoding: identity`);
const unencodedContent = Buffer.from(content, 'utf-8');
response.set({
'Content-Length': unencodedContent.byteLength,
});
response.send(unencodedContent);
}
else {
response.locals.debug(`Response encoding: ${encoding}`);
let transform = (content) => content;
switch (encoding) {
case 'gzip':
case 'x-gzip':
transform = (content) => zlib.gzipSync(content);
break;
case 'deflate':
transform = (content) => zlib.deflateSync(content);
break;
case 'br':
transform = (content) => zlib.brotliCompressSync(content);
break;
}
const unencodedContent = Buffer.from(content, 'utf-8');
const encodedContent = transform(unencodedContent);
response.set({
'Content-Encoding': encoding,
'Content-Length': encodedContent.byteLength,
});
response.send(encodedContent);
}
}
async getBodyXML(request, response) {
const stream = await this.getBodyStream(request, response);
const contentTypeHeader = request.get('Content-Type');
const contentLengthHeader = request.get('Content-Length');
const transferEncoding = request.get('Transfer-Encoding');
if (transferEncoding === 'chunked') {
response.locals.debug('Request transfer encoding is chunked.');
}
if (contentTypeHeader == null && contentLengthHeader === '0') {
return null;
}
const requestType = contentType.parse(contentTypeHeader || 'application/xml');
if (requestType.type !== 'text/xml' &&
requestType.type !== 'application/xml') {
throw new MediaTypeNotSupportedError('Provided content type is not supported.');
}
if (![
'ascii',
'utf8',
'utf-8',
'utf16le',
'ucs2',
'ucs-2',
'base64',
'base64url',
'latin1',
'binary',
'hex',
].includes(requestType?.parameters?.charset || 'utf-8')) {
throw new MediaTypeNotSupportedError('Provided content charset is not supported.');
}
const encoding = (requestType?.parameters?.charset ||
'utf-8');
let xml = await new Promise((resolve, reject) => {
const buffers = [];
stream.on('data', (chunk) => {
buffers.push(chunk);
});
stream.on('end', () => {
resolve(Buffer.concat(buffers).toString(encoding));
});
stream.on('error', (e) => {
reject(e);
});
});
if (xml.trim() === '') {
return null;
}
return xml;
}
async parseXml(xml) {
let parsed = await this.xmlParser.parseStringPromise(xml);
let prefixes = {};
const rewriteAttributes = (input, namespace) => {
const output = {};
for (let name in input) {
if (input[name].uri === 'http://www.w3.org/2000/xmlns/' ||
input[name].uri === 'http://www.w3.org/XML/1998/namespace') {
output[name] = input[name].value;
}
else if (input[name].uri === 'DAV:' ||
(input[name].uri === '' && namespace === 'DAV:')) {
output[input[name].local] = input[name].value;
}
else {
output[`${input[name].uri || namespace}%%${input[name].local}`] =
input[name].value;
}
}
return output;
};
const extractNamespaces = (input) => {
const output = {};
for (let name in input) {
if (input[name].uri === 'http://www.w3.org/2000/xmlns/' &&
input[name].local !== '' &&
input[name].value !== 'DAV:') {
output[input[name].local] = input[name].value;
}
}
return output;
};
const recursivelyRewrite = (input, lang, element = '', prefix = '', namespaces = {}, includeLang = false) => {
if (Array.isArray(input)) {
return input.map((value) => recursivelyRewrite(value, lang, element, prefix, namespaces, includeLang));
}
else if (typeof input === 'object') {
const output = {};
let curLang = lang;
let curNamespaces = { ...namespaces };
if ('$' in input) {
if ('xml:lang' in input.$) {
curLang = input.$['xml:lang'].value;
}
output.$ = rewriteAttributes(input.$, input.$ns.uri);
curNamespaces = {
...curNamespaces,
...extractNamespaces(input.$),
};
}
if (curLang != null && includeLang) {
output.$ = output.$ || {};
output.$['xml:lang'] = curLang;
}
if (element.includes('%%') && prefix !== '') {
const uri = element.split('%%', 1)[0];
if (prefix in curNamespaces && curNamespaces[prefix] === uri) {
output.$ = output.$ || {};
output.$[`xmlns:${prefix}`] = curNamespaces[prefix];
}
}
for (let name in input) {
if (name === '$ns' || name === '$') {
continue;
}
const ns = (Array.isArray(input[name])
? input[name][0].$ns
: input[name].$ns) || { local: name, uri: 'DAV:' };
let prefix = '';
if (name.includes(':')) {
prefix = name.split(':', 1)[0];
if (!(prefix in prefixes)) {
prefixes[prefix] = ns.uri;
}
}
const el = ns.uri === 'DAV:' ? ns.local : `${ns.uri}%%${ns.local}`;
output[el] = recursivelyRewrite(input[name], curLang, el, prefix, curNamespaces, element === 'prop');
}
return output;
}
else {
return input;
}
};
const output = recursivelyRewrite(parsed);
return { output, prefixes };
}
async renderXml(xml, prefixes = {}) {
let topLevelObject = undefined;
const prefixEntries = Object.entries(prefixes);
const davPrefix = (prefixEntries.find(([_prefix, value]) => value === 'DAV:') || ['', 'DAV:'])[0];
const recursivelyRewrite = (input, namespacePrefixes = {}, element = '', currentUri = 'DAV:', addNamespace) => {
if (Array.isArray(input)) {
return input.map((value) => recursivelyRewrite(value, namespacePrefixes, element, currentUri, addNamespace));
}
else if (typeof input === 'object') {
const output = element === ''
? {}
: {
$: {
...(addNamespace == null ? {} : { xmlns: addNamespace }),
},
};
const curNamespacePrefixes = { ...namespacePrefixes };
if ('$' in input) {
for (let attr in input.$) {
if (attr.includes('%%') ||
(currentUri !== 'DAV:' && !attr.includes(':') && attr !== 'xmlns')) {
const [uri, name] = attr.includes('%%')
? splitn(attr, '%%', 2)
: ['DAV:', attr];
if (currentUri === uri) {
output.$[name] = input.$[attr];
}
else {
const xmlns = Object.entries(input.$).find(([name, value]) => name.startsWith('xmlns:') && value === uri);
if (xmlns) {
const [_dec, prefix] = splitn(xmlns[0], ':', 2);
output.$[`${prefix}:${name}`] = input.$[attr];
}
else {
const prefixEntry = Object.entries(curNamespacePrefixes).find(([_prefix, value]) => value === uri);
output.$[`${prefixEntry ? prefixEntry[0] + ':' : ''}${name}`] = input.$[attr];
}
}
}
else {
if (attr.startsWith('xmlns:')) {
if (curNamespacePrefixes[attr.substring(6)] === input.$[attr]) {
continue;
}
curNamespacePrefixes[attr.substring(6)] = input.$[attr];
}
output.$[attr] = input.$[attr];
}
}
}
const curNamespacePrefixEntries = Object.entries(curNamespacePrefixes);
for (let name in input) {
if (name === '$') {
continue;
}
let el = name;
let prefix = davPrefix;
let namespaceToAdd = undefined;
let uri = 'DAV:';
let local = el;
if (name.includes('%%')) {
[uri, local] = splitn(name, '%%', 2);
prefix = '';
const curPrefixEntry = curNamespacePrefixEntries.find(([_prefix, value]) => value === uri);
if (curPrefixEntry) {
prefix = curPrefixEntry[0];
}
const child = Array.isArray(input[name])
? input[name][0]
: input[name];
if (typeof child === 'object' && '$' in child) {
let foundPrefix = '';
for (let attr in child.$) {
if (attr.startsWith('xmlns:') && child.$[attr] === uri) {
foundPrefix = attr.substring(6);
break;
}
}
if (foundPrefix) {
if (Array.isArray(input[name])) {
let prefixIsGood = true;
for (let child of input[name]) {
if (typeof child !== 'object' ||
!('$' in child) ||
child.$[`xmlns:${foundPrefix}`] !== uri) {
prefixIsGood = false;
break;
}
}
if (prefixIsGood) {
prefix = foundPrefix;
}
}
else {
prefix = foundPrefix;
}
}
}
if (prefix) {
el = `${prefix}:${local}`;
}
else {
namespaceToAdd = uri;
el = local;
}
}
let setTopLevel = false;
if (topLevelObject == null) {
setTopLevel = true;
}
output[el] = recursivelyRewrite(input[name], curNamespacePrefixes, el, uri, namespaceToAdd);
if (setTopLevel) {
topLevelObject = output[el];
}
}
return output;
}
else {
if (addNamespace != null) {
return {
$: { xmlns: addNamespace },
_: input,
};
}
return input;
}
};
const obj = recursivelyRewrite(xml, prefixes);
if (topLevelObject != null) {
const obj = topLevelObject;
obj.$.xmlns = 'DAV:';
for (let prefix in prefixes) {
obj.$[`xmlns:${prefix}`] = prefixes[prefix];
}
}
return this.xmlBuilder.buildObject(obj);
}
async formatLocks(locks) {
const xml = { activelock: [] };
if (locks != null) {
for (let lock of locks) {
const secondsLeft = lock.timeout === Infinity
? Infinity
: (lock.date.getTime() + lock.timeout - new Date().getTime()) /
1000;
if (secondsLeft <= 0) {
continue;
}
xml.activelock.push({
locktype: {
write: {},
},
lockscope: {
[lock.scope]: {},
},
depth: {
_: `${lock.depth}`,
},
owner: lock.owner,
timeout: secondsLeft === Infinity
? { _: 'Infinite' }
: { _: `Second-${secondsLeft}` },
locktoken: { href: { _: lock.token } },
lockroot: {
href: {
_: (await lock.resource.getCanonicalUrl()).pathname,
},
},
});
}
}
if (!xml.activelock.length) {
return {};
}
return xml;
}
}
//# sourceMappingURL=Method.js.map