import type { DavPatAuth } from "@/worker/auth/basic"; import { hasDavScope } from "@/worker/auth/scopes"; import { expandCalendarData } from "@/worker/caldav/ical/render"; import type { RecurrenceBounds } from "@/worker/caldav/ical/types"; import type { CalendarDataRequest } from "@/worker/caldav/reports"; import { buildPropstats, COLLECTION_DAV_PROPS, commonCollectionPropValue, csProp, getctagPropValue, multistatusForResources as buildResourceMultistatus, objectEntityPropValue, supportedReportSetPropValue, syncTokenPropValue, } from "@/worker/dav/collection-props"; import { escapeXml } from "@/worker/dav/multistatus"; import { davProp } from "@/worker/dav/props"; import type { SubjectRow } from "@/worker/db/d1/schema"; import type { CalendarResource } from "@/worker/db/cal-dav-do/repository"; import type { PropfindRequest } from "@/worker/xml/dav-request"; import { CALDAV_NS, qname, sameQName, type QName } from "@/worker/xml/namespaces"; const defaultRecurrenceBounds: RecurrenceBounds = { maxYears: 5, maxInstances: 10_000 }; export interface CalendarPropRenderContext { maxBytes: number; allowCalendarData?: boolean; calendarData?: CalendarDataRequest | null; recurrenceBounds?: RecurrenceBounds; } interface CompleteCalendarPropRenderContext { maxBytes: number; allowCalendarData: boolean; calendarData: CalendarDataRequest | null; recurrenceBounds: RecurrenceBounds; } export function calProp(localName: string): QName { return qname(CALDAV_NS, localName); } function supportedCalendarDataXml(): string { return ``; } function supportedComponentSetXml(): string { return ``; } function supportedReportSetXml(): string { return ``; } function defaultProps(resource: CalendarResource): QName[] { const base = [...COLLECTION_DAV_PROPS]; if (resource.kind === "object") return [ ...base, davProp("getetag"), davProp("getcontentlength"), davProp("getcontenttype"), davProp("supported-report-set"), ]; return [ ...base, calProp("supported-calendar-data"), calProp("supported-calendar-component-set"), calProp("calendar-timezone"), calProp("max-resource-size"), csProp("getctag"), davProp("supported-report-set"), davProp("sync-token"), ]; } function propValue( resource: CalendarResource, subject: SubjectRow, auth: DavPatAuth, prop: QName, context: CompleteCalendarPropRenderContext, ) { if (sameQName(prop, davProp("resourcetype"))) { if (resource.kind === "calendar") { return { qname: prop, valueXml: `` }; } return { qname: prop, valueXml: resource.kind === "home" ? "" : "" }; } if (sameQName(prop, davProp("displayname"))) { return { qname: prop, valueXml: escapeXml(resource.calendar?.displayName ?? resource.object?.name ?? "Calendars") }; } const common = commonCollectionPropValue( prop, `/principals/${subject.hostLabel}/`, hasDavScope(auth.scopes, "dav:caldav:write"), ); if (common) return common; const objectEntity = objectEntityPropValue(prop, resource.object, "text/calendar; charset=utf-8"); if (objectEntity) return objectEntity; if (sameQName(prop, calProp("calendar-data")) && resource.object && context.allowCalendarData) { const body = context.calendarData?.expand ? expandCalendarData(resource.object.body, context.calendarData.expand, context.recurrenceBounds) : resource.object.body; return { qname: prop, valueXml: `${escapeXml(body)}`, }; } if (sameQName(prop, calProp("supported-calendar-data"))) return { qname: prop, valueXml: supportedCalendarDataXml() }; if (sameQName(prop, calProp("supported-calendar-component-set"))) { return { qname: prop, valueXml: supportedComponentSetXml() }; } if (sameQName(prop, calProp("calendar-timezone")) && resource.calendar?.timezoneIcal) { return { qname: prop, valueXml: `${escapeXml(resource.calendar.timezoneIcal)}`, }; } // RFC 4791 5.2.4 / RFC 6578 3.2: supported-report-set and sync-token are // exposed on calendar collections; objects only advertise reports they support. const supportedReportSet = supportedReportSetPropValue( prop, supportedReportSetXml(), resource.kind === "object" ? `` : null, ); if (supportedReportSet) return supportedReportSet; const syncToken = syncTokenPropValue(prop, resource.syncToken); if (syncToken) return syncToken; if (sameQName(prop, calProp("max-resource-size"))) { return { qname: prop, valueXml: `${context.maxBytes}`, }; } const getctag = getctagPropValue(prop, resource.calendar?.syncSeq); if (getctag) return getctag; return null; } function completeContext(context: CalendarPropRenderContext): CompleteCalendarPropRenderContext { return { maxBytes: context.maxBytes, allowCalendarData: context.allowCalendarData ?? false, calendarData: context.calendarData ?? null, recurrenceBounds: context.recurrenceBounds ?? defaultRecurrenceBounds, }; } export function propstats( resource: CalendarResource, subject: SubjectRow, auth: DavPatAuth, request: PropfindRequest, renderContext: CalendarPropRenderContext, ) { const context = completeContext(renderContext); return buildPropstats(resource, request, defaultProps, (resource, prop) => propValue(resource, subject, auth, prop, context), ); } export function multistatusForResources( resources: CalendarResource[], subject: SubjectRow, auth: DavPatAuth, request: PropfindRequest, renderContext: CalendarPropRenderContext, ) { return buildResourceMultistatus(resources, (resource) => propstats(resource, subject, auth, request, renderContext)); }