UNPKG

nephele

Version:

Highly customizable and extensible WebDAV server for Node.js and Express.

931 lines 40.5 kB
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