From 6280228759446891fb2bf64986e8f87a23d54552 Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Fri, 17 Oct 2025 20:41:01 +0100 Subject: [PATCH] Fix XML parsing for WebDav This was a regression caused by the `@libs/xml` upgrade in v2.6.0 --- docker-compose.yml | 2 +- lib/utils/webdav.ts | 8 +- lib/utils/webdav_test.ts | 217 +++++++++++++++++++++++++++++++++++++++ routes/dav.tsx | 17 +-- 4 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 lib/utils/webdav_test.ts diff --git a/docker-compose.yml b/docker-compose.yml index 74be34b..e336cc6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: website: - image: ghcr.io/bewcloud/bewcloud:v2.7.0 + image: ghcr.io/bewcloud/bewcloud:v2.7.1 restart: always ports: - 127.0.0.1:8000:8000 diff --git a/lib/utils/webdav.ts b/lib/utils/webdav.ts index a196b0c..44fa658 100644 --- a/lib/utils/webdav.ts +++ b/lib/utils/webdav.ts @@ -12,7 +12,7 @@ export function addDavPrefixToKeys(object: Record, prefix = 'D:'): if (typeof object === 'object' && object !== null) { return Object.entries(object).reduce((reducedObject, [key, value]) => { - const prefixedKey = key === 'xml' || key.startsWith('#') || key.startsWith('@') ? key : `${prefix}${key}`; + const prefixedKey = key.startsWith('#') || key.startsWith('@') ? key : `${prefix}${key}`; reducedObject[prefixedKey] = addDavPrefixToKeys(value); return reducedObject; }, {} as Record); @@ -105,10 +105,8 @@ export async function buildPropFindResponse( const filePaths = await getFilePaths(join(rootPath, queryPath), depth); const response: Record = { - xml: { - '@version': '1.0', - '@encoding': 'UTF-8', - }, + '@version': '1.0', + '@encoding': 'UTF-8', multistatus: { '@xmlns:D': 'DAV:', response: [], diff --git a/lib/utils/webdav_test.ts b/lib/utils/webdav_test.ts new file mode 100644 index 0000000..1079f3c --- /dev/null +++ b/lib/utils/webdav_test.ts @@ -0,0 +1,217 @@ +import { assertEquals } from '@std/assert'; +import { parse, stringify } from '@libs/xml'; + +import { addDavPrefixToKeys, getProperDestinationPath, getPropertyNames } from './webdav.ts'; + +Deno.test('that getProperDestinationPath works', () => { + const tests: { input: string; expected?: string }[] = [ + { + input: `http://127.0.0.1/dav/12345-abcde-67890`, + expected: 'dav/12345-abcde-67890', + }, + { + input: `http://127.0.0.1/dav/spaced-%20uid`, + expected: 'dav/spaced- uid', + }, + { + input: `http://127.0.0.1/dav/something-deeper/spaced-%C3%A7uid`, + expected: 'dav/something-deeper/spaced-çuid', + }, + ]; + + for (const test of tests) { + const output = getProperDestinationPath(test.input); + if (test.expected) { + assertEquals(output, test.expected); + } + } +}); + +Deno.test('that addDavPrefixToKeys works', () => { + const tests: { + input: { object: Record | Record[]; prefix?: string }; + expected: Record; + }[] = [ + { + input: { + object: { + displayname: 'test', + }, + }, + expected: { + 'D:displayname': 'test', + }, + }, + { + input: { + object: [ + { displayname: 'test' }, + { color: 'black' }, + ], + }, + expected: [ + { 'D:displayname': 'test' }, + { 'D:color': 'black' }, + ], + }, + { + input: { + object: { '@version': '1.0', '@encoding': 'UTF-8', displayname: 'test', color: 'black' }, + }, + expected: { + '@version': '1.0', + '@encoding': 'UTF-8', + 'D:displayname': 'test', + 'D:color': 'black', + }, + }, + { + input: { + object: { displayname: 'test', color: 'black' }, + prefix: 'S:', + }, + expected: { + 'S:displayname': 'test', + 'S:color': 'black', + }, + }, + ]; + + for (const test of tests) { + const output = addDavPrefixToKeys(test.input.object, test.input.prefix); + if (test.expected) { + assertEquals(output, test.expected); + } + } +}); + +Deno.test('that getPropertyNames works', () => { + const tests: { + input: Record; + expected: string[]; + }[] = [ + { + input: { + 'D:propfind': { + 'D:prop': { + 'D:displayname': 'test', + }, + }, + }, + expected: ['displayname'], + }, + { + input: { + 'D:propfind': { + 'D:prop': { + 'D:displayname': 'test', + 'D:color': 'black', + }, + }, + }, + expected: ['displayname', 'color'], + }, + { + input: {}, + expected: ['allprop'], + }, + ]; + + for (const test of tests) { + const output = getPropertyNames(test.input); + if (test.expected) { + assertEquals(output, test.expected); + } + } +}); + +Deno.test('that @libs/xml.parse works', () => { + const tests: { input: string; expected: Record }[] = [ + { + input: ` + + + test + +`, + expected: { + '@version': '1.0', + '@encoding': 'UTF-8', + propfind: { + '@xmlns': 'DAV:', + prop: { + displayname: 'test', + }, + }, + }, + }, + { + input: ` + + + test + +`, + expected: { + '@version': '1.0', + '@encoding': 'UTF-8', + 'D:propfind': { + '@xmlns:D': 'DAV:', + 'D:prop': { + 'D:displayname': 'test', + }, + }, + }, + }, + ]; + + for (const test of tests) { + const output = parse(test.input); + assertEquals(output, test.expected); + } +}); + +Deno.test('that @libs/xml.stringify works', () => { + const tests: { input: Record; expected: string }[] = [ + { + input: { + '@version': '1.0', + '@encoding': 'UTF-8', + 'D:propfind': { + 'D:prop': { + 'D:displayname': 'test', + }, + }, + }, + expected: ` + + + test + +`, + }, + { + input: { + '@version': '1.0', + '@encoding': 'UTF-8', + 'D:propfind': { + '@xmlns:D': 'DAV:', + 'D:prop': { + 'D:displayname': 'test', + }, + }, + }, + expected: ` + + + test + +`, + }, + ]; + + for (const test of tests) { + const output = stringify(test.input); + assertEquals(output, test.expected); + } +}); diff --git a/routes/dav.tsx b/routes/dav.tsx index 432e70b..8d28027 100644 --- a/routes/dav.tsx +++ b/routes/dav.tsx @@ -168,10 +168,8 @@ export const handler: Handler = async (request, context await lock.acquire(); const responseXml: Record = { - xml: { - '@version': '1.0', - '@encoding': 'UTF-8', - }, + '@version': '1.0', + '@encoding': 'UTF-8', prop: { '@xmlns:D': 'DAV:', lockdiscovery: { @@ -218,10 +216,17 @@ export const handler: Handler = async (request, context const depthString = request.headers.get('depth'); const depth = depthString ? parseInt(depthString, 10) : null; const xml = await request.clone().text(); + let properties: string[] = []; - const parsedXml = parse(xml); + try { + const parsedXml = parse(xml) as Record; - const properties = getPropertyNames(parsedXml); + properties = getPropertyNames(parsedXml); + } catch (error) { + console.error('Error parsing XML: ', error); + + properties = ['allprop']; + } await ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);