Fix XML parsing for WebDav

This was a regression caused by the `@libs/xml` upgrade in v2.6.0
This commit is contained in:
Bruno Bernardino
2025-10-17 20:41:01 +01:00
parent 8d78e1f25c
commit 6280228759
4 changed files with 232 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
services: services:
website: website:
image: ghcr.io/bewcloud/bewcloud:v2.7.0 image: ghcr.io/bewcloud/bewcloud:v2.7.1
restart: always restart: always
ports: ports:
- 127.0.0.1:8000:8000 - 127.0.0.1:8000:8000

View File

@@ -12,7 +12,7 @@ export function addDavPrefixToKeys(object: Record<string, any>, prefix = 'D:'):
if (typeof object === 'object' && object !== null) { if (typeof object === 'object' && object !== null) {
return Object.entries(object).reduce((reducedObject, [key, value]) => { 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); reducedObject[prefixedKey] = addDavPrefixToKeys(value);
return reducedObject; return reducedObject;
}, {} as Record<string, any>); }, {} as Record<string, any>);
@@ -105,10 +105,8 @@ export async function buildPropFindResponse(
const filePaths = await getFilePaths(join(rootPath, queryPath), depth); const filePaths = await getFilePaths(join(rootPath, queryPath), depth);
const response: Record<string, any> = { const response: Record<string, any> = {
xml: { '@version': '1.0',
'@version': '1.0', '@encoding': 'UTF-8',
'@encoding': 'UTF-8',
},
multistatus: { multistatus: {
'@xmlns:D': 'DAV:', '@xmlns:D': 'DAV:',
response: [], response: [],

217
lib/utils/webdav_test.ts Normal file
View File

@@ -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<string, any> | Record<string, any>[]; prefix?: string };
expected: Record<string, any>;
}[] = [
{
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<string, any>;
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<string, any> }[] = [
{
input: `<?xml version="1.0" encoding="UTF-8"?>
<propfind xmlns="DAV:">
<prop>
<displayname>test</displayname>
</prop>
</propfind>`,
expected: {
'@version': '1.0',
'@encoding': 'UTF-8',
propfind: {
'@xmlns': 'DAV:',
prop: {
displayname: 'test',
},
},
},
},
{
input: `<?xml version="1.0" encoding="UTF-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:displayname>test</D:displayname>
</D:prop>
</D:propfind>`,
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<string, any>; expected: string }[] = [
{
input: {
'@version': '1.0',
'@encoding': 'UTF-8',
'D:propfind': {
'D:prop': {
'D:displayname': 'test',
},
},
},
expected: `<?xml version="1.0" encoding="UTF-8"?>
<D:propfind>
<D:prop>
<D:displayname>test</D:displayname>
</D:prop>
</D:propfind>`,
},
{
input: {
'@version': '1.0',
'@encoding': 'UTF-8',
'D:propfind': {
'@xmlns:D': 'DAV:',
'D:prop': {
'D:displayname': 'test',
},
},
},
expected: `<?xml version="1.0" encoding="UTF-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:displayname>test</D:displayname>
</D:prop>
</D:propfind>`,
},
];
for (const test of tests) {
const output = stringify(test.input);
assertEquals(output, test.expected);
}
});

View File

@@ -168,10 +168,8 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
await lock.acquire(); await lock.acquire();
const responseXml: Record<string, any> = { const responseXml: Record<string, any> = {
xml: { '@version': '1.0',
'@version': '1.0', '@encoding': 'UTF-8',
'@encoding': 'UTF-8',
},
prop: { prop: {
'@xmlns:D': 'DAV:', '@xmlns:D': 'DAV:',
lockdiscovery: { lockdiscovery: {
@@ -218,10 +216,17 @@ export const handler: Handler<Data, FreshContextState> = async (request, context
const depthString = request.headers.get('depth'); const depthString = request.headers.get('depth');
const depth = depthString ? parseInt(depthString, 10) : null; const depth = depthString ? parseInt(depthString, 10) : null;
const xml = await request.clone().text(); const xml = await request.clone().text();
let properties: string[] = [];
const parsedXml = parse(xml); try {
const parsedXml = parse(xml) as Record<string, any>;
const properties = getPropertyNames(parsedXml); properties = getPropertyNames(parsedXml);
} catch (error) {
console.error('Error parsing XML: ', error);
properties = ['allprop'];
}
await ensureUserPathIsValidAndSecurelyAccessible(userId, filePath); await ensureUserPathIsValidAndSecurelyAccessible(userId, filePath);