Moved multiget output into its own function.
[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                 FreeStrBuf(&Caldata);
154                 Caldata = NULL;
155         }
156         else {
157                 // syslog(LOG_DEBUG, "caldav_report_one_item(%s) 404 not found", ChrPtr(ThisHref));
158                 StrBufAppendPrintf(ReportOut, "<D:status>");
159                 StrBufAppendPrintf(ReportOut, "HTTP/1.1 404 not found");
160                 StrBufAppendPrintf(ReportOut, "</D:status>");
161         }
162
163         StrBufAppendPrintf(ReportOut, "</D:propstat>");
164         StrBufAppendPrintf(ReportOut, "</D:response>");
165 }
166
167
168 // Called by caldav_report() to output a single item.
169 // Our policy is to throw away the list of properties the client asked for, and just send everything.
170 void caldav_report_one_item(struct http_transaction *h, struct ctdlsession *c, StrBuf *ReportOut, StrBuf *ThisHref) {
171         long msgnum;
172         StrBuf *Caldata = NULL;
173         char *euid;
174
175         euid = strrchr(ChrPtr(ThisHref), '/');
176         if (euid != NULL) {
177                 ++euid;
178         }
179         else {
180                 euid = (char *) ChrPtr(ThisHref);
181         }
182
183         char *unescaped_euid = strdup(euid);
184         if (!unescaped_euid) {
185                 return;
186         }
187         unescape_input(unescaped_euid);
188
189         msgnum = locate_message_by_uid(c, unescaped_euid);
190         free(unescaped_euid);
191         if (msgnum > 0) {
192                 Caldata = fetch_ical(c, msgnum);
193         }
194         else {
195                 Caldata = NULL;
196         }
197
198         cal_multiget_out(msgnum, ThisHref, Caldata, ReportOut);
199 }
200
201
202 // Called by report_the_room_itself() in room_functions.c when a CalDAV REPORT method
203 // is requested on a calendar room.  We fire up an XML Parser to decode the request and
204 // hopefully produce the correct output.
205 void caldav_report(struct http_transaction *h, struct ctdlsession *c) {
206         struct cr_parms crp;
207         char buf[1024];
208
209         memset(&crp, 0, sizeof(struct cr_parms));
210
211         XML_Parser xp = XML_ParserCreateNS("UTF-8", ':');
212         if (xp == NULL) {
213                 syslog(LOG_INFO, "Cannot create XML parser!");
214                 do_404(h);
215                 return;
216         }
217
218         XML_SetElementHandler(xp, caldav_xml_start, caldav_xml_end);
219         XML_SetCharacterDataHandler(xp, caldav_xml_chardata);
220         XML_SetUserData(xp, &crp);
221         XML_SetDefaultHandler(xp, NULL);        // Disable internal entity expansion to prevent "billion laughs attack"
222         XML_Parse(xp, h->request_body, h->request_body_length, 1);
223         XML_ParserFree(xp);
224
225         if (crp.Chardata != NULL) {     // Discard any trailing chardata ... normally nothing here
226                 FreeStrBuf(&crp.Chardata);
227                 crp.Chardata = NULL;
228         }
229
230         // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar".
231         // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics.
232         ctdl_printf(c, "MSGP text/calendar");
233         ctdl_readline(c, buf, sizeof buf);
234
235         // Now begin the REPORT.
236         syslog(LOG_DEBUG, "CalDAV REPORT type is: %d", crp.report_type);
237         StrBuf *ReportOut = NewStrBuf();
238         StrBufAppendPrintf(ReportOut,
239                 "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
240                 "<D:multistatus "
241                 "xmlns:D=\"DAV:\" "
242                 "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
243                 ">"
244         );
245
246         // RFC4791 7.8 "calendar-query" REPORT - Client will send a lot of search criteria.
247         if (crp.report_type == cr_calendar_query) {
248                 int i = 0;
249                 Array *msglist = get_msglist(c, "ALL");
250                 if (msglist != NULL) {
251                         for (i = 0; i < array_len(msglist); ++i) {
252                                 long m;
253                                 memcpy(&m, array_get_element_at(msglist, i), sizeof(long));
254
255                                 // Begin -- evaluate the calendar item
256
257                                 syslog(LOG_DEBUG, "evaluating message %ld", m);
258                                 StrBuf *one_item;
259                                 one_item = fetch_ical(c, m);
260                                 syslog(LOG_DEBUG, "calendar item:\n---\n\033[33m%s\n---\033[0m", ChrPtr(one_item));
261                                 FreeStrBuf(&one_item);
262
263                                 // End - evaluate the calendar item
264
265                         }
266                         array_free(msglist);
267                 }
268         }
269
270         // RFC4791 7.9 "calendar-multiget" REPORT - go get the specific Hrefs the client asked for.
271         // Can we move this back into citserver too?
272         else if ( (crp.report_type == cr_calendar_multiget) && (crp.Hrefs != NULL) ) {
273
274                 StrBuf *ThisHref = NewStrBuf();
275                 const char *pvset = NULL;
276                 while (StrBufExtract_NextToken(ThisHref, crp.Hrefs, &pvset, '|') >= 0) {
277                         StrBufTrim(ThisHref);                           // remove leading/trailing whitespace from the href
278                         caldav_report_one_item(h, c, ReportOut, ThisHref);
279                 }
280                 FreeStrBuf(&ThisHref);
281         }
282
283         // RFC4791 7.10 "free-busy-query" REPORT
284         else if (crp.report_type == cr_freebusy_query) {
285                 // FIXME build this REPORT.  At the moment we send an empty multistatus.
286         }
287
288         // Free any query parameters that might have been allocated during the xml parse
289         if (crp.Hrefs != NULL) {
290                 FreeStrBuf(&crp.Hrefs);
291                 crp.Hrefs = NULL;
292         }
293
294         StrBufAppendPrintf(ReportOut, "</D:multistatus>\n");            // End the REPORT.
295
296         add_response_header(h, strdup("Content-type"), strdup("text/xml"));
297         h->response_code = 207;
298         h->response_string = strdup("Multi-Status");
299         h->response_body_length = StrLength(ReportOut);
300         h->response_body = SmashStrBuf(&ReportOut);
301 }