Skip to content
File

Blob: src/worker/caldav/props.ts

typescript175 lines
1import type { DavPatAuth } from "@/worker/auth/basic";
2import { hasDavScope } from "@/worker/auth/scopes";
3import { expandCalendarData } from "@/worker/caldav/ical/render";
4import type { RecurrenceBounds } from "@/worker/caldav/ical/types";
5import type { CalendarDataRequest } from "@/worker/caldav/reports";
6import {
7 buildPropstats,
8 COLLECTION_DAV_PROPS,
9 commonCollectionPropValue,
10 csProp,
11 getctagPropValue,
12 multistatusForResources as buildResourceMultistatus,
13 objectEntityPropValue,
14 supportedReportSetPropValue,
15 syncTokenPropValue,
16} from "@/worker/dav/collection-props";
17import { escapeXml } from "@/worker/dav/multistatus";
18import { davProp } from "@/worker/dav/props";
19import type { SubjectRow } from "@/worker/db/d1/schema";
20import type { CalendarResource } from "@/worker/db/cal-dav-do/repository";
21import type { PropfindRequest } from "@/worker/xml/dav-request";
22import { CALDAV_NS, qname, sameQName, type QName } from "@/worker/xml/namespaces";
23 
24const defaultRecurrenceBounds: RecurrenceBounds = { maxYears: 5, maxInstances: 10_000 };
25 
26export interface CalendarPropRenderContext {
27 maxBytes: number;
28 allowCalendarData?: boolean;
29 calendarData?: CalendarDataRequest | null;
30 recurrenceBounds?: RecurrenceBounds;
31}
32 
33interface CompleteCalendarPropRenderContext {
34 maxBytes: number;
35 allowCalendarData: boolean;
36 calendarData: CalendarDataRequest | null;
37 recurrenceBounds: RecurrenceBounds;
38}
39 
40export function calProp(localName: string): QName {
41 return qname(CALDAV_NS, localName);
42}
43 
44function supportedCalendarDataXml(): string {
45 return `<C:supported-calendar-data xmlns:C="${CALDAV_NS}"><C:calendar-data content-type="text/calendar" version="2.0"/></C:supported-calendar-data>`;
46}
47 
48function supportedComponentSetXml(): string {
49 return `<C:supported-calendar-component-set xmlns:C="${CALDAV_NS}"><C:comp name="VEVENT"/><C:comp name="VTODO"/><C:comp name="VJOURNAL"/></C:supported-calendar-component-set>`;
50}
51 
52function supportedReportSetXml(): string {
53 return `<D:supported-report><D:report><C:calendar-query xmlns:C="${CALDAV_NS}"/></D:report></D:supported-report><D:supported-report><D:report><C:calendar-multiget xmlns:C="${CALDAV_NS}"/></D:report></D:supported-report><D:supported-report><D:report><D:sync-collection/></D:report></D:supported-report>`;
54}
55 
56function defaultProps(resource: CalendarResource): QName[] {
57 const base = [...COLLECTION_DAV_PROPS];
58 if (resource.kind === "object")
59 return [
60 ...base,
61 davProp("getetag"),
62 davProp("getcontentlength"),
63 davProp("getcontenttype"),
64 davProp("supported-report-set"),
65 ];
66 return [
67 ...base,
68 calProp("supported-calendar-data"),
69 calProp("supported-calendar-component-set"),
70 calProp("calendar-timezone"),
71 calProp("max-resource-size"),
72 csProp("getctag"),
73 davProp("supported-report-set"),
74 davProp("sync-token"),
75 ];
76}
77 
78function propValue(
79 resource: CalendarResource,
80 subject: SubjectRow,
81 auth: DavPatAuth,
82 prop: QName,
83 context: CompleteCalendarPropRenderContext,
84) {
85 if (sameQName(prop, davProp("resourcetype"))) {
86 if (resource.kind === "calendar") {
87 return { qname: prop, valueXml: `<D:collection/><C:calendar xmlns:C="${CALDAV_NS}"/>` };
88 }
89 return { qname: prop, valueXml: resource.kind === "home" ? "<D:collection/>" : "" };
90 }
91 if (sameQName(prop, davProp("displayname"))) {
92 return { qname: prop, valueXml: escapeXml(resource.calendar?.displayName ?? resource.object?.name ?? "Calendars") };
93 }
94 const common = commonCollectionPropValue(
95 prop,
96 `/principals/${subject.hostLabel}/`,
97 hasDavScope(auth.scopes, "dav:caldav:write"),
98 );
99 if (common) return common;
100 const objectEntity = objectEntityPropValue(prop, resource.object, "text/calendar; charset=utf-8");
101 if (objectEntity) return objectEntity;
102 if (sameQName(prop, calProp("calendar-data")) && resource.object && context.allowCalendarData) {
103 const body = context.calendarData?.expand
104 ? expandCalendarData(resource.object.body, context.calendarData.expand, context.recurrenceBounds)
105 : resource.object.body;
106 return {
107 qname: prop,
108 valueXml: `<C:calendar-data xmlns:C="${CALDAV_NS}">${escapeXml(body)}</C:calendar-data>`,
109 };
110 }
111 if (sameQName(prop, calProp("supported-calendar-data"))) return { qname: prop, valueXml: supportedCalendarDataXml() };
112 if (sameQName(prop, calProp("supported-calendar-component-set"))) {
113 return { qname: prop, valueXml: supportedComponentSetXml() };
114 }
115 if (sameQName(prop, calProp("calendar-timezone")) && resource.calendar?.timezoneIcal) {
116 return {
117 qname: prop,
118 valueXml: `<C:calendar-timezone xmlns:C="${CALDAV_NS}">${escapeXml(resource.calendar.timezoneIcal)}</C:calendar-timezone>`,
119 };
120 }
121 // RFC 4791 5.2.4 / RFC 6578 3.2: supported-report-set and sync-token are
122 // exposed on calendar collections; objects only advertise reports they support.
123 const supportedReportSet = supportedReportSetPropValue(
124 prop,
125 supportedReportSetXml(),
126 resource.kind === "object"
127 ? `<D:supported-report><D:report><C:calendar-multiget xmlns:C="${CALDAV_NS}"/></D:report></D:supported-report>`
128 : null,
129 );
130 if (supportedReportSet) return supportedReportSet;
131 const syncToken = syncTokenPropValue(prop, resource.syncToken);
132 if (syncToken) return syncToken;
133 if (sameQName(prop, calProp("max-resource-size"))) {
134 return {
135 qname: prop,
136 valueXml: `<C:max-resource-size xmlns:C="${CALDAV_NS}">${context.maxBytes}</C:max-resource-size>`,
137 };
138 }
139 const getctag = getctagPropValue(prop, resource.calendar?.syncSeq);
140 if (getctag) return getctag;
141 return null;
142}
143 
144function completeContext(context: CalendarPropRenderContext): CompleteCalendarPropRenderContext {
145 return {
146 maxBytes: context.maxBytes,
147 allowCalendarData: context.allowCalendarData ?? false,
148 calendarData: context.calendarData ?? null,
149 recurrenceBounds: context.recurrenceBounds ?? defaultRecurrenceBounds,
150 };
151}
152 
153export function propstats(
154 resource: CalendarResource,
155 subject: SubjectRow,
156 auth: DavPatAuth,
157 request: PropfindRequest,
158 renderContext: CalendarPropRenderContext,
159) {
160 const context = completeContext(renderContext);
161 return buildPropstats(resource, request, defaultProps, (resource, prop) =>
162 propValue(resource, subject, auth, prop, context),
163 );
164}
165 
166export function multistatusForResources(
167 resources: CalendarResource[],
168 subject: SubjectRow,
169 auth: DavPatAuth,
170 request: PropfindRequest,
171 renderContext: CalendarPropRenderContext,
172) {
173 return buildResourceMultistatus(resources, (resource) => propstats(resource, subject, auth, request, renderContext));
174}