Files
smartserve/ts/protocols/webdav/webdav.xml.ts

185 lines
4.8 KiB
TypeScript
Raw Normal View History

2025-11-29 15:24:00 +00:00
/**
* WebDAV XML generation utilities
*/
import type { IWebDAVResource, IWebDAVProperty, IWebDAVLock } from './webdav.types.js';
import { DAV_NAMESPACE } from './webdav.types.js';
/**
* Escape XML special characters
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Format date for WebDAV (RFC 1123)
*/
function formatDate(date: Date): string {
return date.toUTCString();
}
/**
* Format date for creationdate (ISO 8601)
*/
function formatCreationDate(date: Date): string {
return date.toISOString();
}
/**
* Generate multistatus response for PROPFIND
*/
export function generateMultistatus(resources: IWebDAVResource[]): string {
const responses = resources.map(resource => generateResponse(resource)).join('\n');
return `<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="${DAV_NAMESPACE}">
${responses}
</D:multistatus>`;
}
/**
* Generate single response element
*/
function generateResponse(resource: IWebDAVResource): string {
const props: string[] = [];
// Resource type
if (resource.isCollection) {
props.push('<D:resourcetype><D:collection/></D:resourcetype>');
} else {
props.push('<D:resourcetype/>');
}
// Display name
if (resource.displayName) {
props.push(`<D:displayname>${escapeXml(resource.displayName)}</D:displayname>`);
}
// Content type
if (resource.contentType) {
props.push(`<D:getcontenttype>${escapeXml(resource.contentType)}</D:getcontenttype>`);
}
// Content length
if (resource.contentLength !== undefined) {
props.push(`<D:getcontentlength>${resource.contentLength}</D:getcontentlength>`);
}
// Last modified
if (resource.lastModified) {
props.push(`<D:getlastmodified>${formatDate(resource.lastModified)}</D:getlastmodified>`);
}
// Creation date
if (resource.creationDate) {
props.push(`<D:creationdate>${formatCreationDate(resource.creationDate)}</D:creationdate>`);
}
// ETag
if (resource.etag) {
props.push(`<D:getetag>"${escapeXml(resource.etag)}"</D:getetag>`);
}
// Supported lock
props.push(`<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>`);
// Custom properties
for (const prop of resource.properties) {
if (prop.value) {
props.push(`<${prop.name} xmlns="${prop.namespace}">${escapeXml(prop.value)}</${prop.name}>`);
} else {
props.push(`<${prop.name} xmlns="${prop.namespace}"/>`);
}
}
return ` <D:response>
<D:href>${escapeXml(resource.href)}</D:href>
<D:propstat>
<D:prop>
${props.join('\n ')}
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>`;
}
/**
* Generate lock discovery response
*/
export function generateLockDiscovery(locks: IWebDAVLock[]): string {
if (locks.length === 0) {
return '<D:lockdiscovery/>';
}
const lockEntries = locks.map(lock => `
<D:activelock>
<D:locktype><D:write/></D:locktype>
<D:lockscope><D:${lock.scope}/></D:lockscope>
<D:depth>${lock.depth}</D:depth>
<D:owner>${escapeXml(lock.owner)}</D:owner>
<D:timeout>Second-${lock.timeout}</D:timeout>
<D:locktoken><D:href>${escapeXml(lock.token)}</D:href></D:locktoken>
</D:activelock>`).join('');
return `<D:lockdiscovery>${lockEntries}
</D:lockdiscovery>`;
}
/**
* Generate lock response
*/
export function generateLockResponse(lock: IWebDAVLock): string {
return `<?xml version="1.0" encoding="utf-8"?>
<D:prop xmlns:D="${DAV_NAMESPACE}">
${generateLockDiscovery([lock])}
</D:prop>`;
}
/**
* Generate error response
*/
export function generateError(status: number, message: string): string {
return `<?xml version="1.0" encoding="utf-8"?>
<D:error xmlns:D="${DAV_NAMESPACE}">
<D:${message.toLowerCase().replace(/\s+/g, '-')}/>
</D:error>`;
}
/**
* Parse PROPFIND request body to extract requested properties
*/
export function parsePropfindRequest(body: string): { allprop: boolean; propnames: string[] } {
// Simple XML parsing for PROPFIND
if (!body || body.includes('<allprop') || body.includes('<D:allprop')) {
return { allprop: true, propnames: [] };
}
if (body.includes('<propname') || body.includes('<D:propname')) {
return { allprop: false, propnames: [] };
}
// Extract property names
const propnames: string[] = [];
const propMatch = body.match(/<(?:D:)?prop[^>]*>([\s\S]*?)<\/(?:D:)?prop>/i);
if (propMatch) {
const propContent = propMatch[1];
const tagMatches = propContent.matchAll(/<(?:D:)?(\w+)[^>]*\/?>/gi);
for (const match of tagMatches) {
propnames.push(match[1]);
}
}
return { allprop: propnames.length === 0, propnames };
}