Skip to content
File

Blob: src/worker/dav/collection-props.ts

typescript140 lines
1import { buildMultistatus, emptyProp, escapeXml, type PropValue, type Propstat } from "@/worker/dav/multistatus";
2import { davProp, isProtectedLivePropForResource, type DavResourcePropKind } from "@/worker/dav/props";
3import type { AppContext } from "@/worker/types";
4import { parseProppatchXml, type PropfindRequest, type ProppatchInstruction } from "@/worker/xml/dav-request";
5import { qname, sameQName, type QName } from "@/worker/xml/namespaces";
6 
7export interface DeadPropRow {
8 nsUri: string;
9 localName: string;
10 xmlValue: string;
11}
12 
13export interface ResourceWithDeadProps {
14 href: string;
15 deadProps: DeadPropRow[];
16}
17 
18export const COLLECTION_DAV_PROPS = [
19 davProp("resourcetype"),
20 davProp("displayname"),
21 davProp("owner"),
22 davProp("current-user-privilege-set"),
23 davProp("supported-privilege-set"),
24] as const;
25 
26export function csProp(localName: string): QName {
27 return qname("http://calendarserver.org/ns/", localName);
28}
29 
30export function hrefXml(href: string): string {
31 return `<D:href>${escapeXml(href)}</D:href>`;
32}
33 
34export function supportedPrivilegeXml(): string {
35 return `<D:supported-privilege><D:privilege><D:all/></D:privilege><D:supported-privilege><D:privilege><D:read/></D:privilege><D:supported-privilege><D:privilege><D:read-current-user-privilege-set/></D:privilege></D:supported-privilege></D:supported-privilege><D:supported-privilege><D:privilege><D:write/></D:privilege><D:supported-privilege><D:privilege><D:write-content/></D:privilege></D:supported-privilege><D:supported-privilege><D:privilege><D:write-properties/></D:privilege></D:supported-privilege><D:supported-privilege><D:privilege><D:bind/></D:privilege></D:supported-privilege><D:supported-privilege><D:privilege><D:unbind/></D:privilege></D:supported-privilege></D:supported-privilege></D:supported-privilege>`;
36}
37 
38export function currentUserPrivilegeXml(canWrite: boolean): string {
39 const read = "<D:privilege><D:read/></D:privilege><D:privilege><D:read-current-user-privilege-set/></D:privilege>";
40 const write =
41 "<D:privilege><D:write/></D:privilege><D:privilege><D:write-content/></D:privilege><D:privilege><D:write-properties/></D:privilege><D:privilege><D:bind/></D:privilege><D:privilege><D:unbind/></D:privilege>";
42 return read + (canWrite ? write : "");
43}
44 
45export function commonCollectionPropValue(prop: QName, ownerHref: string, canWrite: boolean): PropValue | null {
46 if (sameQName(prop, davProp("owner"))) return { qname: prop, valueXml: hrefXml(ownerHref) };
47 if (sameQName(prop, davProp("current-user-privilege-set"))) {
48 return { qname: prop, valueXml: currentUserPrivilegeXml(canWrite) };
49 }
50 if (sameQName(prop, davProp("supported-privilege-set"))) return { qname: prop, valueXml: supportedPrivilegeXml() };
51 return null;
52}
53 
54export function objectEntityPropValue(
55 prop: QName,
56 object: { etag: string; size: number } | null | undefined,
57 contentType: string,
58): PropValue | null {
59 if (!object) return null;
60 if (sameQName(prop, davProp("getetag"))) return { qname: prop, valueXml: object.etag };
61 if (sameQName(prop, davProp("getcontentlength"))) return { qname: prop, valueXml: String(object.size) };
62 if (sameQName(prop, davProp("getcontenttype"))) return { qname: prop, valueXml: contentType };
63 return null;
64}
65 
66export function supportedReportSetPropValue(
67 prop: QName,
68 valueXml: string,
69 objectValueXml?: string | null,
70): PropValue | null {
71 if (!sameQName(prop, davProp("supported-report-set"))) return null;
72 return { qname: prop, valueXml: objectValueXml ?? valueXml };
73}
74 
75export function syncTokenPropValue(prop: QName, syncToken: string | null | undefined): PropValue | null {
76 if (!syncToken || !sameQName(prop, davProp("sync-token"))) return null;
77 return { qname: prop, valueXml: syncToken };
78}
79 
80export function getctagPropValue(prop: QName, syncSeq: number | null | undefined): PropValue | null {
81 if (syncSeq === null || syncSeq === undefined || !sameQName(prop, csProp("getctag"))) return null;
82 return {
83 qname: prop,
84 valueXml: `<CS:getctag xmlns:CS="http://calendarserver.org/ns/">sync:${syncSeq}</CS:getctag>`,
85 };
86}
87 
88export function deadPropValue(deadProps: DeadPropRow[], prop: QName): PropValue | null {
89 const dead = deadProps.find((row) => row.nsUri === prop.nsUri && row.localName === prop.localName);
90 return dead ? { qname: prop, valueXml: dead.xmlValue } : null;
91}
92 
93export function buildPropstats<TResource extends ResourceWithDeadProps>(
94 resource: TResource,
95 request: PropfindRequest,
96 defaultProps: (resource: TResource) => QName[],
97 propValue: (resource: TResource, prop: QName) => PropValue | null,
98): Propstat[] {
99 const deadPropNames = resource.deadProps.map((row) => qname(row.nsUri, row.localName));
100 const props =
101 request.kind === "prop"
102 ? request.props
103 : request.kind === "allprop"
104 ? [...defaultProps(resource), ...request.includeProps, ...deadPropNames]
105 : [...defaultProps(resource), ...deadPropNames];
106 if (request.kind === "propname") return [{ status: 200, props: props.map(emptyProp) }];
107 
108 const ok: PropValue[] = [];
109 const missing: PropValue[] = [];
110 for (const prop of props) {
111 const value = propValue(resource, prop) ?? deadPropValue(resource.deadProps, prop);
112 if (value) ok.push(value);
113 else missing.push(emptyProp(prop));
114 }
115 return [
116 { status: 200, props: ok },
117 { status: 404, props: missing },
118 ];
119}
120 
121export function multistatusForResources<TResource extends ResourceWithDeadProps>(
122 resources: TResource[],
123 propstats: (resource: TResource) => Propstat[],
124): string {
125 return buildMultistatus(resources.map((resource) => ({ href: resource.href, propstats: propstats(resource) })));
126}
127 
128export async function parseDeadPropInstructions(
129 c: AppContext,
130 maxXmlBytes: number,
131 resourceKind: DavResourcePropKind,
132): Promise<ProppatchInstruction[]> {
133 const instructions = parseProppatchXml(await c.req.text(), maxXmlBytes);
134 const protectedProp = instructions.find((instruction) =>
135 isProtectedLivePropForResource(instruction.prop, resourceKind),
136 );
137 if (protectedProp) throw new Error(`Cannot set protected property ${protectedProp.prop.localName}`);
138 return instructions;
139}