Skip to content
File

Blob: src/worker/carddav/props.ts

typescript129 lines
1import type { DavPatAuth } from "@/worker/auth/basic";
2import { hasDavScope } from "@/worker/auth/scopes";
3import {
4 buildPropstats,
5 COLLECTION_DAV_PROPS,
6 commonCollectionPropValue,
7 csProp,
8 getctagPropValue,
9 multistatusForResources as buildResourceMultistatus,
10 objectEntityPropValue,
11 supportedReportSetPropValue,
12 syncTokenPropValue,
13} from "@/worker/dav/collection-props";
14import { escapeXml } from "@/worker/dav/multistatus";
15import { davProp } from "@/worker/dav/props";
16import type { SubjectRow } from "@/worker/db/d1/schema";
17import type { CardDavResource } from "@/worker/db/card-dav-do/repository";
18import type { PropfindRequest } from "@/worker/xml/dav-request";
19import { CARDDAV_NS, qname, sameQName, type QName } from "@/worker/xml/namespaces";
20 
21export function cardProp(localName: string): QName {
22 return qname(CARDDAV_NS, localName);
23}
24 
25function supportedAddressDataXml(): string {
26 return `<CARD:supported-address-data xmlns:CARD="${CARDDAV_NS}"><CARD:address-data-type content-type="text/vcard" version="3.0"/><CARD:address-data-type content-type="text/vcard" version="4.0"/></CARD:supported-address-data>`;
27}
28 
29function defaultProps(resource: CardDavResource): QName[] {
30 const base = [...COLLECTION_DAV_PROPS];
31 if (resource.kind === "object") {
32 return [
33 ...base,
34 davProp("getetag"),
35 davProp("getcontentlength"),
36 davProp("getcontenttype"),
37 davProp("supported-report-set"),
38 ];
39 }
40 return [
41 ...base,
42 cardProp("supported-address-data"),
43 cardProp("max-resource-size"),
44 csProp("getctag"),
45 davProp("supported-report-set"),
46 davProp("sync-token"),
47 ];
48}
49 
50function supportedReportSetXml(): string {
51 return `<D:supported-report><D:report><CARD:addressbook-query xmlns:CARD="${CARDDAV_NS}"/></D:report></D:supported-report><D:supported-report><D:report><CARD:addressbook-multiget xmlns:CARD="${CARDDAV_NS}"/></D:report></D:supported-report><D:supported-report><D:report><D:sync-collection/></D:report></D:supported-report>`;
52}
53 
54function propValue(
55 resource: CardDavResource,
56 subject: SubjectRow,
57 auth: DavPatAuth,
58 prop: QName,
59 context: { maxBytes: number; allowAddressData: boolean },
60) {
61 if (sameQName(prop, davProp("resourcetype"))) {
62 if (resource.kind === "addressbook") {
63 return { qname: prop, valueXml: `<D:collection/><CARD:addressbook xmlns:CARD="${CARDDAV_NS}"/>` };
64 }
65 return { qname: prop, valueXml: resource.kind === "home" ? "<D:collection/>" : "" };
66 }
67 if (sameQName(prop, davProp("displayname"))) {
68 return { qname: prop, valueXml: escapeXml(resource.book?.displayName ?? resource.object?.name ?? "Address Books") };
69 }
70 const common = commonCollectionPropValue(
71 prop,
72 `/principals/${subject.hostLabel}/`,
73 hasDavScope(auth.scopes, "dav:carddav:write"),
74 );
75 if (common) return common;
76 const objectEntity = objectEntityPropValue(prop, resource.object, "text/vcard; charset=utf-8");
77 if (objectEntity) return objectEntity;
78 if (sameQName(prop, cardProp("address-data")) && resource.object && context.allowAddressData) {
79 return {
80 qname: prop,
81 valueXml: `<CARD:address-data xmlns:CARD="${CARDDAV_NS}">${escapeXml(resource.object.body)}</CARD:address-data>`,
82 };
83 }
84 if (sameQName(prop, cardProp("supported-address-data"))) return { qname: prop, valueXml: supportedAddressDataXml() };
85 if (sameQName(prop, cardProp("max-resource-size"))) {
86 return {
87 qname: prop,
88 valueXml: `<CARD:max-resource-size xmlns:CARD="${CARDDAV_NS}">${context.maxBytes}</CARD:max-resource-size>`,
89 };
90 }
91 const getctag = getctagPropValue(prop, resource.book?.syncSeq);
92 if (getctag) return getctag;
93 // RFC 6578 3.2 / RFC 6352 6.2: supported-report-set and sync-token on collections.
94 const supportedReportSet = supportedReportSetPropValue(
95 prop,
96 supportedReportSetXml(),
97 resource.kind === "object"
98 ? `<D:supported-report><D:report><CARD:addressbook-multiget xmlns:CARD="${CARDDAV_NS}"/></D:report></D:supported-report>`
99 : null,
100 );
101 if (supportedReportSet) return supportedReportSet;
102 const syncToken = syncTokenPropValue(prop, resource.syncToken);
103 if (syncToken) return syncToken;
104 return null;
105}
106 
107export function propstats(
108 resource: CardDavResource,
109 subject: SubjectRow,
110 auth: DavPatAuth,
111 request: PropfindRequest,
112 maxBytes: number,
113 allowAddressData = false,
114) {
115 return buildPropstats(resource, request, defaultProps, (resource, prop) =>
116 propValue(resource, subject, auth, prop, { maxBytes, allowAddressData }),
117 );
118}
119 
120export function multistatusForResources(
121 resources: CardDavResource[],
122 subject: SubjectRow,
123 auth: DavPatAuth,
124 request: PropfindRequest,
125 maxBytes: number,
126) {
127 return buildResourceMultistatus(resources, (resource) => propstats(resource, subject, auth, request, maxBytes));
128}