File
Blob: src/worker/carddav/props.ts
| 1 | import type { DavPatAuth } from "@/worker/auth/basic"; |
| 2 | import { hasDavScope } from "@/worker/auth/scopes"; |
| 3 | import { |
| 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"; |
| 14 | import { escapeXml } from "@/worker/dav/multistatus"; |
| 15 | import { davProp } from "@/worker/dav/props"; |
| 16 | import type { SubjectRow } from "@/worker/db/d1/schema"; |
| 17 | import type { CardDavResource } from "@/worker/db/card-dav-do/repository"; |
| 18 | import type { PropfindRequest } from "@/worker/xml/dav-request"; |
| 19 | import { CARDDAV_NS, qname, sameQName, type QName } from "@/worker/xml/namespaces"; |
| 20 | |
| 21 | export function cardProp(localName: string): QName { |
| 22 | return qname(CARDDAV_NS, localName); |
| 23 | } |
| 24 | |
| 25 | function 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 | |
| 29 | function 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 | |
| 50 | function 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 | |
| 54 | function 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 | |
| 107 | export 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 | |
| 120 | export 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 | } |