185 lines
4.8 KiB
TypeScript
185 lines
4.8 KiB
TypeScript
/**
|
|
* 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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
/**
|
|
* 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 };
|
|
}
|