Rearranging code to prepare for multiple REPORT types
[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 caldav_report() to output a single item.
131 // Our policy is to throw away the list of properties the client asked for, and just send everything.
132 void caldav_report_one_item(struct http_transaction *h, struct ctdlsession *c, StrBuf *ReportOut, StrBuf *ThisHref) {
133         long msgnum;
134         StrBuf *Caldata = NULL;
135         char *euid;
136
137         euid = strrchr(ChrPtr(ThisHref), '/');
138         if (euid != NULL) {
139                 ++euid;
140         }
141         else {
142                 euid = (char *) ChrPtr(ThisHref);
143         }
144
145         char *unescaped_euid = strdup(euid);
146         if (!unescaped_euid) {
147                 return;
148         }
149         unescape_input(unescaped_euid);
150
151         StrBufAppendPrintf(ReportOut, "<D:response>");
152         StrBufAppendPrintf(ReportOut, "<D:href>");
153         StrBufXMLEscAppend(ReportOut, ThisHref, NULL, 0, 0);
154         StrBufAppendPrintf(ReportOut, "</D:href>");
155         StrBufAppendPrintf(ReportOut, "<D:propstat>");
156
157         msgnum = locate_message_by_uid(c, unescaped_euid);
158         free(unescaped_euid);
159         if (msgnum > 0) {
160                 Caldata = fetch_ical(c, msgnum);
161         }
162
163         if (Caldata != NULL) {
164                 // syslog(LOG_DEBUG, "caldav_report_one_item(%s) 200 OK", ChrPtr(ThisHref));
165                 StrBufAppendPrintf(ReportOut, "<D:status>");
166                 StrBufAppendPrintf(ReportOut, "HTTP/1.1 200 OK");
167                 StrBufAppendPrintf(ReportOut, "</D:status>");
168                 StrBufAppendPrintf(ReportOut, "<D:prop>");
169                 StrBufAppendPrintf(ReportOut, "<D:getetag>");
170                 StrBufAppendPrintf(ReportOut, "%ld", msgnum);
171                 StrBufAppendPrintf(ReportOut, "</D:getetag>");
172                 StrBufAppendPrintf(ReportOut, "<C:calendar-data>");
173                 StrBufXMLEscAppend(ReportOut, Caldata, NULL, 0, 0);
174                 StrBufAppendPrintf(ReportOut, "</C:calendar-data>");
175                 StrBufAppendPrintf(ReportOut, "</D:prop>");
176                 FreeStrBuf(&Caldata);
177                 Caldata = NULL;
178         }
179         else {
180                 // syslog(LOG_DEBUG, "caldav_report_one_item(%s) 404 not found", ChrPtr(ThisHref));
181                 StrBufAppendPrintf(ReportOut, "<D:status>");
182                 StrBufAppendPrintf(ReportOut, "HTTP/1.1 404 not found");
183                 StrBufAppendPrintf(ReportOut, "</D:status>");
184         }
185
186         StrBufAppendPrintf(ReportOut, "</D:propstat>");
187         StrBufAppendPrintf(ReportOut, "</D:response>");
188 }
189
190
191 // Called by report_the_room_itself() in room_functions.c when a CalDAV REPORT method
192 // is requested on a calendar room.  We fire up an XML Parser to decode the request and
193 // hopefully produce the correct output.
194 void caldav_report(struct http_transaction *h, struct ctdlsession *c) {
195         struct cr_parms crp;
196         char buf[1024];
197
198         memset(&crp, 0, sizeof(struct cr_parms));
199
200         XML_Parser xp = XML_ParserCreateNS("UTF-8", ':');
201         if (xp == NULL) {
202                 syslog(LOG_INFO, "Cannot create XML parser!");
203                 do_404(h);
204                 return;
205         }
206
207         XML_SetElementHandler(xp, caldav_xml_start, caldav_xml_end);
208         XML_SetCharacterDataHandler(xp, caldav_xml_chardata);
209         XML_SetUserData(xp, &crp);
210         XML_SetDefaultHandler(xp, NULL);        // Disable internal entity expansion to prevent "billion laughs attack"
211         XML_Parse(xp, h->request_body, h->request_body_length, 1);
212         XML_ParserFree(xp);
213
214         if (crp.Chardata != NULL) {     // Discard any trailing chardata ... normally nothing here
215                 FreeStrBuf(&crp.Chardata);
216                 crp.Chardata = NULL;
217         }
218
219         // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar".
220         // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics.
221         ctdl_printf(c, "MSGP text/calendar");
222         ctdl_readline(c, buf, sizeof buf);
223
224         // Now begin the REPORT.
225         syslog(LOG_DEBUG, "CalDAV REPORT type is: %d", crp.report_type);
226         StrBuf *ReportOut = NewStrBuf();
227         StrBufAppendPrintf(ReportOut,
228                 "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
229                 "<D:multistatus "
230                 "xmlns:D=\"DAV:\" "
231                 "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
232                 ">"
233         );
234
235         // RFC4791 7.8 "calendar-query" REPORT - Client will send a lot of search criteria.
236         if (crp.report_type == cr_calendar_query) {
237                 // FIXME build this REPORT.  At the moment we send an empty multistatus.
238         }
239
240         // RFC4791 7.9 "calendar-multiget" REPORT - go get the specific Hrefs the client asked for.
241         else if ( (crp.report_type == cr_calendar_multiget) && (crp.Hrefs != NULL) ) {
242                 StrBuf *ThisHref = NewStrBuf();
243                 const char *pvset = NULL;
244                 while (StrBufExtract_NextToken(ThisHref, crp.Hrefs, &pvset, '|') >= 0) {
245                         StrBufTrim(ThisHref);                           // remove leading/trailing whitespace from the href
246                         caldav_report_one_item(h, c, ReportOut, ThisHref);
247                 }
248                 FreeStrBuf(&ThisHref);
249         }
250
251         // RFC4791 7.10 "free-busy-query" REPORT
252         else if (crp.report_type == cr_freebusy_query) {
253                 // FIXME build this REPORT.  At the moment we send an empty multistatus.
254         }
255
256         // Free any query parameters that might have been allocated during the xml parse
257         if (crp.Hrefs != NULL) {
258                 FreeStrBuf(&crp.Hrefs);
259                 crp.Hrefs = NULL;
260         }
261
262         StrBufAppendPrintf(ReportOut, "</D:multistatus>\n");            // End the REPORT.
263
264         add_response_header(h, strdup("Content-type"), strdup("text/xml"));
265         h->response_code = 207;
266         h->response_string = strdup("Multi-Status");
267         h->response_body_length = StrLength(ReportOut);
268         h->response_body = SmashStrBuf(&ReportOut);
269 }
270
271
272
273
274
275 // Client is requesting a message list (make this code work for calendar-query REPORT)
276 void XXXXXXXXXXXXXX(struct http_transaction *h, struct ctdlsession *c, char *range) {
277         char buf[SIZ];
278
279         // Determine the date/time range requested by the client
280         time_t lo = atol(range);
281         char *colon = strchr(range, ':');
282         time_t hi = colon ? atol(++colon) : LONG_MAX;
283
284         // Rule out impossible ranges
285         if (hi < lo) {
286                 do_404(h);
287                 return;
288         }
289
290         // Begin by requesting all messages in the room
291         int i = 0;
292         Array *msglist = get_msglist(c, "ALL");
293         if (msglist == NULL) {
294                 do_404(h);
295                 return;
296         }
297
298         // We're going to make a lot of MSG4 calls, and the preferred MIME type we want is "text/calendar".
299         // The iCalendar standard is mature now, and we are no longer interested in text/x-vcal or application/ics.
300         ctdl_printf(c, "MSGP text/calendar");
301         ctdl_readline(c, buf, sizeof buf);
302
303         // Iterate through our message list.
304         for (i = 0; i < array_len(msglist); ++i) {
305                 long m;
306                 memcpy(&m, array_get_element_at(msglist, i), sizeof(long));
307                 syslog(LOG_DEBUG, "FIXME %ld", m);
308
309                 // now we have to:
310                 // 1. fetch the message from citadel server
311                 // 2. parse the ical
312                 // 3. figure out range
313                 // we should steal code from webcit-classic for this
314
315                 StrBuf *one_item;
316                 one_item = fetch_ical(c, m);
317                 syslog(LOG_DEBUG, "calendar item:\n---\n\033[33m%s\n---\033[0m", ChrPtr(one_item));
318                 FreeStrBuf(&one_item);
319
320         }
321         array_free(msglist);
322
323         // FIXME we still fail because we aren't finished yet
324         add_response_header(h, strdup("Content-type"), strdup("application/json"));
325         h->response_code = 200;
326         h->response_string = strdup("OK");
327         h->response_body = "{ \"one\":111 , \"two\":222 , \"three\":333 }";
328         h->response_body_length = strlen(h->response_body);
329 }