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));
}