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