File
Blob: src/worker/dav/multistatus.ts
| 1 | import type { QName } from "@/worker/xml/namespaces"; |
| 2 | import { DAV_NS } from "@/worker/xml/namespaces"; |
| 3 | |
| 4 | export interface PropValue { |
| 5 | qname: QName; |
| 6 | valueXml: string; |
| 7 | } |
| 8 | |
| 9 | export interface Propstat { |
| 10 | status: number; |
| 11 | props: PropValue[]; |
| 12 | } |
| 13 | |
| 14 | export interface MultistatusResponse { |
| 15 | href: string; |
| 16 | status?: number; |
| 17 | propstats: Propstat[]; |
| 18 | } |
| 19 | |
| 20 | const 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 | |
| 35 | export function escapeXml(value: string): string { |
| 36 | return value |
| 37 | .replaceAll("&", "&") |
| 38 | .replaceAll("<", "<") |
| 39 | .replaceAll(">", ">") |
| 40 | .replaceAll('"', """) |
| 41 | .replaceAll("'", "'"); |
| 42 | } |
| 43 | |
| 44 | export function statusLine(status: number): string { |
| 45 | return `HTTP/1.1 ${status} ${REASON[status] ?? "Status"}`; |
| 46 | } |
| 47 | |
| 48 | function prefixedName(qname: QName): string { |
| 49 | return qname.nsUri === DAV_NS ? `D:${qname.localName}` : `x:${qname.localName}`; |
| 50 | } |
| 51 | |
| 52 | function namespaceAttribute(qname: QName): string { |
| 53 | return qname.nsUri === DAV_NS ? "" : ` xmlns:x="${escapeXml(qname.nsUri)}"`; |
| 54 | } |
| 55 | |
| 56 | function 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 | |
| 64 | export 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 | |
| 85 | export function emptyProp(qname: QName): PropValue { |
| 86 | return { qname, valueXml: "" }; |
| 87 | } |