initial
This commit is contained in:
184
ts/protocols/webdav/webdav.xml.ts
Normal file
184
ts/protocols/webdav/webdav.xml.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user