import { buildMultistatus, emptyProp, escapeXml, type PropValue, type Propstat } from "@/worker/dav/multistatus"; import { davProp, isProtectedLivePropForResource, type DavResourcePropKind } from "@/worker/dav/props"; import type { AppContext } from "@/worker/types"; import { parseProppatchXml, type PropfindRequest, type ProppatchInstruction } from "@/worker/xml/dav-request"; import { qname, sameQName, type QName } from "@/worker/xml/namespaces"; export interface DeadPropRow { nsUri: string; localName: string; xmlValue: string; } export interface ResourceWithDeadProps { href: string; deadProps: DeadPropRow[]; } export const COLLECTION_DAV_PROPS = [ davProp("resourcetype"), davProp("displayname"), davProp("owner"), davProp("current-user-privilege-set"), davProp("supported-privilege-set"), ] as const; export function csProp(localName: string): QName { return qname("http://calendarserver.org/ns/", localName); } export function hrefXml(href: string): string { return `${escapeXml(href)}`; } export function supportedPrivilegeXml(): string { return ``; } export function currentUserPrivilegeXml(canWrite: boolean): string { const read = ""; const write = ""; return read + (canWrite ? write : ""); } export function commonCollectionPropValue(prop: QName, ownerHref: string, canWrite: boolean): PropValue | null { if (sameQName(prop, davProp("owner"))) return { qname: prop, valueXml: hrefXml(ownerHref) }; if (sameQName(prop, davProp("current-user-privilege-set"))) { return { qname: prop, valueXml: currentUserPrivilegeXml(canWrite) }; } if (sameQName(prop, davProp("supported-privilege-set"))) return { qname: prop, valueXml: supportedPrivilegeXml() }; return null; } export function objectEntityPropValue( prop: QName, object: { etag: string; size: number } | null | undefined, contentType: string, ): PropValue | null { if (!object) return null; if (sameQName(prop, davProp("getetag"))) return { qname: prop, valueXml: object.etag }; if (sameQName(prop, davProp("getcontentlength"))) return { qname: prop, valueXml: String(object.size) }; if (sameQName(prop, davProp("getcontenttype"))) return { qname: prop, valueXml: contentType }; return null; } export function supportedReportSetPropValue( prop: QName, valueXml: string, objectValueXml?: string | null, ): PropValue | null { if (!sameQName(prop, davProp("supported-report-set"))) return null; return { qname: prop, valueXml: objectValueXml ?? valueXml }; } export function syncTokenPropValue(prop: QName, syncToken: string | null | undefined): PropValue | null { if (!syncToken || !sameQName(prop, davProp("sync-token"))) return null; return { qname: prop, valueXml: syncToken }; } export function getctagPropValue(prop: QName, syncSeq: number | null | undefined): PropValue | null { if (syncSeq === null || syncSeq === undefined || !sameQName(prop, csProp("getctag"))) return null; return { qname: prop, valueXml: `sync:${syncSeq}`, }; } export function deadPropValue(deadProps: DeadPropRow[], prop: QName): PropValue | null { const dead = deadProps.find((row) => row.nsUri === prop.nsUri && row.localName === prop.localName); return dead ? { qname: prop, valueXml: dead.xmlValue } : null; } export function buildPropstats( resource: TResource, request: PropfindRequest, defaultProps: (resource: TResource) => QName[], propValue: (resource: TResource, prop: QName) => PropValue | null, ): Propstat[] { const deadPropNames = resource.deadProps.map((row) => qname(row.nsUri, row.localName)); const props = request.kind === "prop" ? request.props : request.kind === "allprop" ? [...defaultProps(resource), ...request.includeProps, ...deadPropNames] : [...defaultProps(resource), ...deadPropNames]; if (request.kind === "propname") return [{ status: 200, props: props.map(emptyProp) }]; const ok: PropValue[] = []; const missing: PropValue[] = []; for (const prop of props) { const value = propValue(resource, prop) ?? deadPropValue(resource.deadProps, prop); if (value) ok.push(value); else missing.push(emptyProp(prop)); } return [ { status: 200, props: ok }, { status: 404, props: missing }, ]; } export function multistatusForResources( resources: TResource[], propstats: (resource: TResource) => Propstat[], ): string { return buildMultistatus(resources.map((resource) => ({ href: resource.href, propstats: propstats(resource) }))); } export async function parseDeadPropInstructions( c: AppContext, maxXmlBytes: number, resourceKind: DavResourcePropKind, ): Promise { const instructions = parseProppatchXml(await c.req.text(), maxXmlBytes); const protectedProp = instructions.find((instruction) => isProtectedLivePropForResource(instruction.prop, resourceKind), ); if (protectedProp) throw new Error(`Cannot set protected property ${protectedProp.prop.localName}`); return instructions; }