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