Skip to content
File

Blob: src/worker/dav/multistatus.ts

typescript88 lines
1import type { QName } from "@/worker/xml/namespaces";
2import { DAV_NS } from "@/worker/xml/namespaces";
3 
4export interface PropValue {
5 qname: QName;
6 valueXml: string;
7}
8 
9export interface Propstat {
10 status: number;
11 props: PropValue[];
12}
13 
14export interface MultistatusResponse {
15 href: string;
16 status?: number;
17 propstats: Propstat[];
18}
19 
20const REASON: Record<number, string> = {
21 200: "OK",
22 201: "Created",
23 204: "No Content",
24 207: "Multi-Status",
25 400: "Bad Request",
26 403: "Forbidden",
27 404: "Not Found",
28 409: "Conflict",
29 412: "Precondition Failed",
30 423: "Locked",
31 424: "Failed Dependency",
32 507: "Insufficient Storage",
33};
34 
35export function escapeXml(value: string): string {
36 return value
37 .replaceAll("&", "&amp;")
38 .replaceAll("<", "&lt;")
39 .replaceAll(">", "&gt;")
40 .replaceAll('"', "&quot;")
41 .replaceAll("'", "&apos;");
42}
43 
44export function statusLine(status: number): string {
45 return `HTTP/1.1 ${status} ${REASON[status] ?? "Status"}`;
46}
47 
48function prefixedName(qname: QName): string {
49 return qname.nsUri === DAV_NS ? `D:${qname.localName}` : `x:${qname.localName}`;
50}
51 
52function namespaceAttribute(qname: QName): string {
53 return qname.nsUri === DAV_NS ? "" : ` xmlns:x="${escapeXml(qname.nsUri)}"`;
54}
55 
56function propXml(prop: PropValue): string {
57 if (prop.valueXml === "") {
58 return `<${prefixedName(prop.qname)}${namespaceAttribute(prop.qname)}/>`;
59 }
60 if (prop.qname.nsUri === DAV_NS) return `<D:${prop.qname.localName}>${prop.valueXml}</D:${prop.qname.localName}>`;
61 return prop.valueXml;
62}
63 
64export function buildMultistatus(responses: MultistatusResponse[], options: { syncToken?: string } = {}): string {
65 const body = responses
66 .map((response) => {
67 const status = response.status ? `<D:status>${statusLine(response.status)}</D:status>` : "";
68 const propstats = response.propstats
69 .filter((propstat) => propstat.props.length > 0)
70 .map(
71 (propstat) =>
72 `<D:propstat><D:prop>${propstat.props.map(propXml).join("")}</D:prop><D:status>${statusLine(
73 propstat.status,
74 )}</D:status></D:propstat>`,
75 )
76 .join("");
77 return `<D:response><D:href>${escapeXml(response.href)}</D:href>${status}${propstats}</D:response>`;
78 })
79 .join("");
80 // RFC 6578 3.4: sync-token is emitted after all response elements inside multistatus.
81 const syncToken = options.syncToken ? `<D:sync-token>${escapeXml(options.syncToken)}</D:sync-token>` : "";
82 return `<?xml version="1.0" encoding="utf-8"?><D:multistatus xmlns:D="DAV:">${body}${syncToken}</D:multistatus>`;
83}
84 
85export function emptyProp(qname: QName): PropValue {
86 return { qname, valueXml: "" };
87}