Unified multistatus output for all REPORT outputs.
[citadel.git] / webcit-ng / server / caldav_reports.c
1 // This file contains functions which handle all of the CalDAV "REPORT" queries
2 // specified in RFC4791 section 7.
3 //
4 // Copyright (c) 2023-2024 by the citadel.org team
5 //
6 // This program is open source software.  Use, duplication, or
7 // disclosure is subject to the GNU General Public License v3.
8
9 #include "webcit.h"
10
11
12 // A CalDAV REPORT can only be one type.  This is stored in the report_type member.
13 enum cr_type {
14         cr_calendar_query,
15         cr_calendar_multiget,
16         cr_freebusy_query
17 };
18
19
20 // Data type for CalDAV Report Parameters.
21 // As we slog our way through the XML we learn what the client is asking for
22 // and build up the contents of this data type.
23 struct cr_parms {
24         int tag_nesting_level;          // not needed, just kept for pretty-printing
25         enum cr_type report_type;       // which RFC4791 section 7 REPORT are we generating
26         StrBuf *Chardata;               // XML chardata in between tags is built up here
27         StrBuf *Hrefs;                  // list of items requested by a `calendar-multiget` REPORT
28 };
29
30
31 // XML parser callback
32 void caldav_xml_start(void *data, const char *el, const char **attr) {
33         struct cr_parms *crp = (struct cr_parms *) data;
34         int i;
35
36         // syslog(LOG_DEBUG, "CALDAV ELEMENT START: <%s> %d", el, crp->tag_nesting_level);
37
38         for (i = 0; attr[i] != NULL; i += 2) {
39                 syslog(LOG_DEBUG, "                    Attribute '%s' = '%s'", attr[i], attr[i + 1]);
40         }
41
42         // RFC4791 7.8 "calendar-query" REPORT - Client will send a lot of search criteria.
43         if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-query")) {
44                 crp->report_type = cr_calendar_query;
45         }
46
47         // RFC4791 7.9 "calendar-multiget" REPORT - Client will supply a list of specific hrefs.
48         else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:calendar-multiget")) {
49                 crp->report_type = cr_calendar_multiget;
50         }
51
52         // RFC4791 7.10 "free-busy-query" REPORT
53         else if (!strcasecmp(el, "urn:ietf:params:xml:ns:caldav:free-busy-query")) {
54                 crp->report_type = cr_freebusy_query;
55         }
56
57         ++crp->tag_nesting_level;
58 }
59
60
61 // XML parser callback
62 void caldav_xml_end(void *data, const char *el) {
63         struct cr_parms *crp = (struct cr_parms *) data;
64         --crp->tag_nesting_level;
65
66         if (crp->Chardata != NULL) {
67                 // syslog(LOG_DEBUG, "CALDAV CHARDATA     : %s", ChrPtr(crp->Chardata));
68         }
69         // syslog(LOG_DEBUG, "CALDAV ELEMENT END  : <%s> %d", el, crp->tag_nesting_level);
70
71         if ((!strcasecmp(el, "DAV::href")) || (!strcasecmp(el, "DAV:href"))) {
72                 if (crp->Hrefs == NULL) {       // append crp->Chardata to crp->Hrefs
73                         crp->Hrefs = NewStrBuf();
74                 }
75                 else {
76                         StrBufAppendBufPlain(crp->Hrefs, HKEY("|"), 0);
77                 }
78                 StrBufAppendBuf(crp->Hrefs, crp->Chardata, 0);
79         }
80
81         if (crp->Chardata != NULL) {            // Tag is closed; chardata is now out of scope.
82                 FreeStrBuf(&crp->Chardata);     // Free the buffer.
83                 crp->Chardata = NULL;
84         }
85 }
86
87
88 // XML parser callback
89 void caldav_xml_chardata(void *data, const XML_Char * s, int len) {
90         struct cr_parms *crp = (struct cr_parms *) data;
91
92         if (crp->Chardata == NULL) {
93                 crp->Chardata = NewStrBuf();
94         }
95
96         StrBufAppendBufPlain(crp->Chardata, s, len, 0);
97
98         return;
99 }
100
101
102 // Called by caldav_report_one_item() to fetch a message (by number) in the current room,
103 // and return only the icalendar data as a StrBuf.  Returns NULL if not found.
104 //
105 // NOTE: this function expects that "MSGP text/calendar" was issued at the beginning
106 // of a REPORT operation to set our preferred MIME type to calendar data.
107 StrBuf *fetch_ical(struct ctdlsession *c, long msgnum) {
108         char buf[1024];
109         StrBuf *Buf = NULL;
110
111         ctdl_printf(c, "MSG4 %ld", msgnum);
112         ctdl_readline(c, buf, sizeof(buf));
113         if (buf[0] != '1') {
114                 return NULL;
115         }
116
117         while (ctdl_readline(c, buf, sizeof(buf)), strcmp(buf, "000")) {
118                 if (Buf != NULL) {              // already in body
119                         StrBufAppendPrintf(Buf, "%s\n", buf);
120                 }
121                 else if (IsEmptyStr(buf)) {     // beginning of body
122                         Buf = NewStrBuf();
123                 }
124         }
125
126         return Buf;
127 }
128
129
130 // Called by multiple REPORT types to actually perform the output in "multiget" format.
131 // We need to already know the source message number and the href, but also already have the output data.
132 void cal_multiget_out(long msgnum, StrBuf *ThisHref, StrBuf *Caldata, StrBuf *ReportOut) {
133
134         StrBufAppendPrintf(ReportOut, "<D:response>");
135         StrBufAppendPrintf(ReportOut, "<D:href>");
136         StrBufXMLEscAppend(ReportOut, ThisHref, NULL, 0, 0);
137         StrBufAppendPrintf(ReportOut, "</D:href>");
138         StrBufAppendPrintf(ReportOut, "<D:propstat>");
139
140         if (Caldata != NULL) {
141                 // syslog(LOG_DEBUG, "caldav_report_one_item(%s) 200 OK", ChrPtr(ThisHref));
142                 StrBufAppendPrintf(ReportOut, "<D:status>");
143                 StrBufAppendPrintf(ReportOut, "HTTP/1.1 200 OK");
144                 StrBufAppendPrintf(ReportOut, "</D:status>");
145                 StrBufAppendPrintf(ReportOut, "<D:prop>");
146                 StrBufAppendPrintf(ReportOut, "<D:getetag>");
147                 StrBufAppendPrintf(ReportOut, "%ld", msgnum);
148                 StrBufAppendPrintf(ReportOut, "</D:getetag>");
149                 StrBufAppendPrintf(ReportOut, "<C:calendar-data>");
150                 StrBufXMLEscAppend(ReportOut, Caldata, NULL, 0, 0);
151                 StrBufAppendPrintf(ReportOut, "</C:calendar-data>");
152                 StrBufAppendPrintf(ReportOut, "</D:prop>");
153         }
154         else {
155                 // syslog(LOG_DEBUG, "caldav_report_one_item(%s) 404 not found", ChrPtr(ThisHref));
156                 StrBufAppendPrintf(ReportOut, "<D:status>");
157                 StrBufAppendPrintf(ReportOut, "HTTP/1.1 404 not found");
158                 StrBufAppendPrintf(ReportOut, "</D:status>");
159         }
160
161         StrBufAppendPrintf(ReportOut, "</D:propstat>");
162         StrBufAppendPrintf(ReportOut, "</D:response>");
163 }
164
165
166 // Called by caldav_report() to output a single item.
167 // Our policy is to throw away the list of properties the client asked for, and just send everything.
168 void caldav_report_one_item(struct http_transaction *h, struct ctdlsession *c, StrBuf *ReportOut, StrBuf *ThisHref) {
169         long msgnum;
170         StrBuf *Caldata = NULL;
171         char *euid;
172
173         euid = strrchr(ChrPtr(ThisHref), '/');
174         if (euid != NULL) {
175                 ++euid;
176         }
177         else {
178                 euid = (char *) ChrPtr(ThisHref);
179         }
180
181         char *unescaped_euid = strdup(euid);
182         if (!unescaped_euid) {
183                 return;
184         }
185         unescape_input(unescaped_euid);
186
187         msgnum = locate_message_by_uid(c, unescaped_euid);
188         free(unescaped_euid);
189         if (msgnum > 0) {
190                 Caldata = fetch_ical(c, msgnum);
191         }
192         else {
193                 Caldata = NULL;
194         }
195
196         cal_multiget_out(msgnum, ThisHref, Caldata, ReportOut);
197
198         if (Caldata != NULL) {
199                 FreeStrBuf(&Caldata);
200         }
201 }
202
203
204 // Called by report_the_room_itself() in room_functions.c when a CalDAV REPORT method
205 // is requested on a calendar room.  We fire up an XML Parser to decode the request and
206 // hopefully produce the correct output.
207 void caldav_report(struct http_transaction *h, struct ctdlsession *c) {
208         struct cr_parms crp;
209         char buf[1024];
210
211         memset(&crp, 0, sizeof(struct cr_parms));
212
213         XML_Parser xp = XML_ParserCreateNS("UTF-8", ':');
214         if (xp == NULL) {
215                 syslog(LOG_INFO, "Cannot create XML parser!");
216                 do_404(h);
217                 return;
218         }
219
220         XML_SetElementHandler(xp, caldav_xml_start, caldav_xml_end);
221         XML_SetCharacterDataHandler(xp, caldav_xml_chardata);
222         XML_SetUserData(xp, &crp);
223         XML_SetDefaultHandler(xp, NULL);        // Disable internal entity expansion to prevent "billion laughs attack"
224         XML_Parse(xp, h->request_body, h->request_body_length, 1);
225         XML_ParserFree(xp);
226
227         if (crp.Chardata != NULL) {     // Discard any trailing chardata ... normally nothing here
228                 FreeStrBuf(&crp.Chardata);
229                 crp.Chardata = NULL;
230         }
231
232         // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar".
233         // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics.
234         ctdl_printf(c, "MSGP text/calendar");
235         ctdl_readline(c, buf, sizeof buf);
236
237         // Now begin the REPORT.
238         syslog(LOG_DEBUG, "CalDAV REPORT type is: %d", crp.report_type);
239         StrBuf *ReportOut = NewStrBuf();
240         StrBufAppendPrintf(ReportOut,
241                 "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
242                 "<D:multistatus "
243                 "xmlns:D=\"DAV:\" "
244                 "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
245                 ">"
246         );
247
248         // RFC4791 7.8 "calendar-query" REPORT - Client will send a lot of search criteria.
249         if (crp.report_type == cr_calendar_query) {
250                 int i = 0;
251                 Array *msglist = get_msglist(c, "ALL");
252                 if (msglist != NULL) {
253                         for (i = 0; i < array_len(msglist); ++i) {
254                                 long m;
255                                 memcpy(&m, array_get_element_at(msglist, i), sizeof(long));
256
257                                 // load and parse one calendar item
258                                 StrBuf *one_item = fetch_ical(c, m);
259                                 icalcomponent *cal = icalcomponent_new_from_string(ChrPtr(one_item));
260
261                                 // this is a horrible temporary hack to output every item
262                                 int qualify = 1;
263
264                                 if (qualify) {
265                                         cal_multiget_out(m, NULL, one_item, ReportOut);
266                                 }
267
268                                 icalcomponent_free(cal);
269                                 FreeStrBuf(&one_item);
270
271                         }
272                         array_free(msglist);
273                 }
274         }
275
276         // RFC4791 7.9 "calendar-multiget" REPORT - go get the specific Hrefs the client asked for.
277         // Can we move this back into citserver too?
278         else if ( (crp.report_type == cr_calendar_multiget) && (crp.Hrefs != NULL) ) {
279
280                 StrBuf *ThisHref = NewStrBuf();
281                 const char *pvset = NULL;
282                 while (StrBufExtract_NextToken(ThisHref, crp.Hrefs, &pvset, '|') >= 0) {
283                         StrBufTrim(ThisHref);                           // remove leading/trailing whitespace from the href
284                         caldav_report_one_item(h, c, ReportOut, ThisHref);
285                 }
286                 FreeStrBuf(&ThisHref);
287         }
288
289         // RFC4791 7.10 "free-busy-query" REPORT
290         else if (crp.report_type == cr_freebusy_query) {
291                 // FIXME build this REPORT.  At the moment we send an empty multistatus.
292         }
293
294         // Free any query parameters that might have been allocated during the xml parse
295         if (crp.Hrefs != NULL) {
296                 FreeStrBuf(&crp.Hrefs);
297                 crp.Hrefs = NULL;
298         }
299
300         StrBufAppendPrintf(ReportOut, "</D:multistatus>\n");            // End the REPORT.
301
302         add_response_header(h, strdup("Content-type"), strdup("text/xml"));
303         h->response_code = 207;
304         h->response_string = strdup("Multi-Status");
305         h->response_body_length = StrLength(ReportOut);
306         h->response_body = SmashStrBuf(&ReportOut);
307 }