import type { DavPatAuth } from "@/worker/auth/basic"; import { hasDavScope } from "@/worker/auth/scopes"; 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 { CardDavResource } from "@/worker/db/card-dav-do/repository"; import type { PropfindRequest } from "@/worker/xml/dav-request"; import { CARDDAV_NS, qname, sameQName, type QName } from "@/worker/xml/namespaces"; export function cardProp(localName: string): QName { return qname(CARDDAV_NS, localName); } function supportedAddressDataXml(): string { return ``; } function defaultProps(resource: CardDavResource): QName[] { const base = [...COLLECTION_DAV_PROPS]; if (resource.kind === "object") { return [ ...base, davProp("getetag"), davProp("getcontentlength"), davProp("getcontenttype"), davProp("supported-report-set"), ]; } return [ ...base, cardProp("supported-address-data"), cardProp("max-resource-size"), csProp("getctag"), davProp("supported-report-set"), davProp("sync-token"), ]; } function supportedReportSetXml(): string { return ``; } function propValue( resource: CardDavResource, subject: SubjectRow, auth: DavPatAuth, prop: QName, context: { maxBytes: number; allowAddressData: boolean }, ) { if (sameQName(prop, davProp("resourcetype"))) { if (resource.kind === "addressbook") { return { qname: prop, valueXml: `` }; } return { qname: prop, valueXml: resource.kind === "home" ? "" : "" }; } if (sameQName(prop, davProp("displayname"))) { return { qname: prop, valueXml: escapeXml(resource.book?.displayName ?? resource.object?.name ?? "Address Books") }; } const common = commonCollectionPropValue( prop, `/principals/${subject.hostLabel}/`, hasDavScope(auth.scopes, "dav:carddav:write"), ); if (common) return common; const objectEntity = objectEntityPropValue(prop, resource.object, "text/vcard; charset=utf-8"); if (objectEntity) return objectEntity; if (sameQName(prop, cardProp("address-data")) && resource.object && context.allowAddressData) { return { qname: prop, valueXml: `${escapeXml(resource.object.body)}`, }; } if (sameQName(prop, cardProp("supported-address-data"))) return { qname: prop, valueXml: supportedAddressDataXml() }; if (sameQName(prop, cardProp("max-resource-size"))) { return { qname: prop, valueXml: `${context.maxBytes}`, }; } const getctag = getctagPropValue(prop, resource.book?.syncSeq); if (getctag) return getctag; // RFC 6578 3.2 / RFC 6352 6.2: supported-report-set and sync-token on collections. const supportedReportSet = supportedReportSetPropValue( prop, supportedReportSetXml(), resource.kind === "object" ? `` : null, ); if (supportedReportSet) return supportedReportSet; const syncToken = syncTokenPropValue(prop, resource.syncToken); if (syncToken) return syncToken; return null; } export function propstats( resource: CardDavResource, subject: SubjectRow, auth: DavPatAuth, request: PropfindRequest, maxBytes: number, allowAddressData = false, ) { return buildPropstats(resource, request, defaultProps, (resource, prop) => propValue(resource, subject, auth, prop, { maxBytes, allowAddressData }), ); } export function multistatusForResources( resources: CardDavResource[], subject: SubjectRow, auth: DavPatAuth, request: PropfindRequest, maxBytes: number, ) { return buildResourceMultistatus(resources, (resource) => propstats(resource, subject, auth, request, maxBytes)); }