4 * Copyright (c) 1996-2018 by the citadel.org team
6 * This program is open source software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
19 * Return a "zero-terminated" array of message numbers in the current room.
20 * Caller owns the memory and must free it. Returns NULL if any problems.
22 long *get_msglist(struct ctdlsession *c, char *which_msgs)
29 ctdl_printf(c, "MSGS %s", which_msgs);
30 ctdl_readline(c, buf, sizeof(buf));
33 if (num_msgs >= num_alloc)
38 msglist = malloc(num_alloc * sizeof(long));
43 msglist = realloc(msglist, num_alloc * sizeof(long));
46 ctdl_readline(c, buf, sizeof(buf));
47 msglist[num_msgs++] = atol(buf);
48 } while (strcmp(buf, "000")); // this makes the last element a "0" terminator
54 * Supplied with a list of potential matches from an If-Match: or If-None-Match: header, and
55 * a message number (which we always use as the entity tag in Citadel), return nonzero if the
56 * message number matches any of the supplied tags in the string.
58 int match_etags(char *taglist, long msgnum)
60 int num_tags = num_tokens(taglist, ',');
64 if (msgnum <= 0) // no msgnum? no match.
69 for (i=0; i<num_tags; ++i)
71 extract_token(tag, taglist, i, ',', sizeof tag);
73 char *lq = (strchr(tag, '"'));
74 char *rq = (strrchr(tag, '"'));
75 if (lq < rq) // has two double quotes
81 if (!strcmp(tag, "*")) // wildcard match
85 long tagmsgnum = atol(tag);
86 if ( (tagmsgnum > 0) && (tagmsgnum == msgnum) ) // match
92 return(0); // no match
97 * Client is requesting a message list
99 void json_msglist(struct http_transaction *h, struct ctdlsession *c, char *which)
102 long *msglist = get_msglist(c, which);
103 JsonValue *j = NewJsonArray(HKEY("msgs"));
107 for (i=0; msglist[i]>0 ; ++i)
109 JsonArrayAppend(j, NewJsonNumber( HKEY("m"), msglist[i]));
114 StrBuf *sj = NewStrBuf();
115 SerializeJson(sj, j, 1); // '1' == free the source array
117 add_response_header(h, strdup("Content-type"), strdup("application/json"));
118 h->response_code = 200;
119 h->response_string = strdup("OK");
120 h->response_body_length = StrLength(sj);
121 h->response_body = SmashStrBuf(&sj);
127 * Client requested an object in a room.
129 void object_in_room(struct http_transaction *h, struct ctdlsession *c)
133 char unescaped_euid[1024];
135 extract_token(buf, h->uri, 4, '/', sizeof buf);
137 if (!strncasecmp(buf, "msgs.", 5)) // Client is requesting a list of message numbers
139 json_msglist(h, c, &buf[5]);
144 if (!strncasecmp(buf, "threads", 5)) // Client is requesting a threaded view (still kind of fuzzy here)
146 threaded_view(h, c, &buf[5]);
150 if (!strncasecmp(buf, "flat", 5)) // Client is requesting a flat view (still kind of fuzzy here)
152 flat_view(h, c, &buf[5]);
157 if ( (c->room_default_view == VIEW_CALENDAR) // room types where objects are referenced by EUID
158 || (c->room_default_view == VIEW_TASKS)
159 || (c->room_default_view == VIEW_ADDRESSBOOK)
161 safestrncpy(unescaped_euid, buf, sizeof unescaped_euid);
162 unescape_input(unescaped_euid);
163 msgnum = locate_message_by_uid(c, unescaped_euid);
171 * All methods except PUT require the message to already exist
173 if ( (msgnum <= 0) && (strcasecmp(h->method, "PUT")) )
179 * If we get to this point we have a valid message number in an accessible room.
181 syslog(LOG_DEBUG, "msgnum is %ld, method is %s", msgnum, h->method);
184 * A sixth component in the URL can be one of two things:
185 * (1) a MIME part specifier, in which case the client wants to download that component within the message
186 * (2) a content-type, in which ase the client wants us to try to render it a certain way
188 if (num_tokens(h->uri, '/') == 6)
190 extract_token(buf, h->uri, 5, '/', sizeof buf);
191 if (!IsEmptyStr(buf)) {
192 if (!strcasecmp(buf, "json"))
194 json_render_one_message(h, c, msgnum);
196 else if (!strcasecmp(buf, "html")) // FIXME exterminate this, we don't want any server-side rendering
198 html_render_one_message(h, c, msgnum);
202 download_mime_component(h, c, msgnum, buf);
209 * Ok, we want a full message, but first let's check for the if[-none]-match headers.
211 char *if_match = header_val(h, "If-Match");
212 if ( (if_match != NULL) && (!match_etags(if_match, msgnum)) )
218 char *if_none_match = header_val(h, "If-None-Match");
219 if ( (if_none_match != NULL) && (match_etags(if_none_match, msgnum)) )
229 if (!strcasecmp(h->method, "DELETE"))
231 dav_delete_message(h, c, msgnum);
233 else if (!strcasecmp(h->method, "GET"))
235 dav_get_message(h, c, msgnum);
237 else if (!strcasecmp(h->method, "PUT"))
239 dav_put_message(h, c, unescaped_euid, msgnum);
243 do_404(h); // Got this far but the method made no sense? Bummer.
250 * Called by the_room_itself() when the HTTP method is REPORT
252 void report_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
254 if (c->room_default_view == VIEW_CALENDAR)
256 caldav_report(h, c); // CalDAV REPORTs ... fmgwac
260 do_404(h); // future implementations like CardDAV will require code paths here
265 * Called by the_room_itself() when the HTTP method is OPTIONS
267 void options_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
269 h->response_code = 200;
270 h->response_string = strdup("OK");
271 if (c->room_default_view == VIEW_CALENDAR)
273 add_response_header(h, strdup("DAV"), strdup("1, calendar-access")); // offer CalDAV
275 else if (c->room_default_view == VIEW_ADDRESSBOOK)
277 add_response_header(h, strdup("DAV"), strdup("1, addressbook")); // offer CardDAV
281 add_response_header(h, strdup("DAV"), strdup("1")); // ordinary WebDAV for all other room types
283 add_response_header(h, strdup("Allow"), strdup("OPTIONS, PROPFIND, GET, PUT, REPORT, DELETE"));
288 * Called by the_room_itself() when the HTTP method is PROPFIND
290 void propfind_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
294 int dav_depth = (header_val(h, "Depth") ? atoi(header_val(h, "Depth")) : INT_MAX);
295 syslog(LOG_DEBUG, "Client PROPFIND requested depth: %d", dav_depth);
296 StrBuf *Buf = NewStrBuf();
298 StrBufAppendPrintf(Buf, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
301 "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
305 /* Transmit the collection resource */
306 StrBufAppendPrintf(Buf, "<D:response>");
307 StrBufAppendPrintf(Buf, "<D:href>");
308 StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0);
309 StrBufAppendPrintf(Buf, "/ctdl/r/");
310 StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
311 StrBufAppendPrintf(Buf, "</D:href>");
313 StrBufAppendPrintf(Buf, "<D:propstat>");
314 StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
315 StrBufAppendPrintf(Buf, "<D:prop>");
316 StrBufAppendPrintf(Buf, "<D:displayname>");
317 StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
318 StrBufAppendPrintf(Buf, "</D:displayname>");
320 StrBufAppendPrintf(Buf, "<D:owner />"); // empty owner ought to be legal; see rfc3744 section 5.1
322 StrBufAppendPrintf(Buf, "<D:resourcetype><D:collection />");
323 switch(c->room_default_view)
326 StrBufAppendPrintf(Buf, "<C:calendar />"); // RFC4791 section 4.2
329 StrBufAppendPrintf(Buf, "</D:resourcetype>");
331 int enumerate_by_euid = 0; // nonzero if messages will be retrieved by euid instead of msgnum
332 switch(c->room_default_view)
334 case VIEW_CALENDAR: // RFC4791 section 5.2
335 StrBufAppendPrintf(Buf, "<C:supported-calendar-component-set><C:comp name=\"VEVENT\"/></C:supported-calendar-component-set>");
336 StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
337 StrBufAppendPrintf(Buf, "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
338 StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
339 enumerate_by_euid = 1;
341 case VIEW_TASKS: // RFC4791 section 5.2
342 StrBufAppendPrintf(Buf, "<C:supported-calendar-component-set><C:comp name=\"VTODO\"/></C:supported-calendar-component-set>");
343 StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
344 StrBufAppendPrintf(Buf, "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
345 StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
346 enumerate_by_euid = 1;
348 case VIEW_ADDRESSBOOK: // FIXME put some sort of CardDAV crapola here when we implement it
349 enumerate_by_euid = 1;
351 case VIEW_WIKI: // FIXME invent "WikiDAV" ?
352 enumerate_by_euid = 1;
357 /* FIXME get the mtime
358 StrBufAppendPrintf(Buf, "<D:getlastmodified>");
360 StrBufAppendPrintf(Buf, "</D:getlastmodified>");
363 StrBufAppendPrintf(Buf, "</D:prop>");
364 StrBufAppendPrintf(Buf, "</D:propstat>");
365 StrBufAppendPrintf(Buf, "</D:response>\n");
367 // If a depth greater than zero was specified, transmit the collection listing
371 long *msglist = get_msglist(c, "ALL");
375 for (i=0; (msglist[i] > 0); ++i)
377 if ((i%10) == 0) syslog(LOG_DEBUG, "PROPFIND enumerated %d messages", i);
378 e = NULL; // EUID gets stored here
382 ctdl_printf(c, "MSG0 %ld|3", msglist[i]);
383 ctdl_readline(c, cbuf, sizeof(cbuf));
384 if (cbuf[0] == '1') while (ctdl_readline(c, cbuf, sizeof(cbuf)), strcmp(cbuf, "000"))
386 if ( (enumerate_by_euid) && (!strncasecmp(cbuf, "exti=", 5)) )
388 // e = strdup(&cbuf[5]);
389 int elen = (2 * strlen(&cbuf[5]));
391 urlesc(e, elen, &cbuf[5]);
393 if (!strncasecmp(cbuf, "time=", 5))
395 timestamp = atol(&cbuf[5]);
401 sprintf(e, "%ld", msglist[i]);
403 StrBufAppendPrintf(Buf, "<D:response>");
405 // Generate the 'href' tag for this message
406 StrBufAppendPrintf(Buf, "<D:href>");
407 StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0);
408 StrBufAppendPrintf(Buf, "/ctdl/r/");
409 StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
410 StrBufAppendPrintf(Buf, "/");
411 StrBufXMLEscAppend(Buf, NULL, e, strlen(e), 0);
412 StrBufAppendPrintf(Buf, "</D:href>");
413 StrBufAppendPrintf(Buf, "<D:propstat>");
414 StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
415 StrBufAppendPrintf(Buf, "<D:prop>");
417 switch(c->room_default_view)
420 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vevent</D:getcontenttype>");
423 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vtodo</D:getcontenttype>");
425 case VIEW_ADDRESSBOOK:
426 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/x-vcard</D:getcontenttype>");
432 char *datestring = http_datestring(timestamp);
435 StrBufAppendPrintf(Buf, "<D:getlastmodified>");
436 StrBufXMLEscAppend(Buf, NULL, datestring, strlen(datestring), 0);
437 StrBufAppendPrintf(Buf, "</D:getlastmodified>");
440 if (enumerate_by_euid) // FIXME ajc 2017oct30 should this be inside the timestamp conditional?
442 StrBufAppendPrintf(Buf, "<D:getetag>\"%ld\"</D:getetag>", msglist[i]);
445 StrBufAppendPrintf(Buf, "</D:prop></D:propstat></D:response>\n");
453 StrBufAppendPrintf(Buf, "</D:multistatus>\n");
455 add_response_header(h, strdup("Content-type"), strdup("text/xml"));
456 h->response_code = 207;
457 h->response_string = strdup("Multi-Status");
458 h->response_body_length = StrLength(Buf);
459 h->response_body = SmashStrBuf(&Buf);
462 // some good examples here
463 // http://blogs.nologin.es/rickyepoderi/index.php?/archives/14-Introducing-CalDAV-Part-I.html
467 * Called by the_room_itself() when the HTTP method is PROPFIND
469 void get_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
471 JsonValue *j = NewJsonObject(HKEY("gotoroom"));
473 JsonObjectAppend(j, NewJsonPlainString( HKEY("name"), c->room, -1));
474 JsonObjectAppend(j, NewJsonNumber( HKEY("current_view"), c->room_current_view ));
475 JsonObjectAppend(j, NewJsonNumber( HKEY("default_view"), c->room_default_view ));
476 JsonObjectAppend(j, NewJsonNumber( HKEY("new_messages"), c->new_messages ));
477 JsonObjectAppend(j, NewJsonNumber( HKEY("total_messages"), c->total_messages ));
478 JsonObjectAppend(j, NewJsonNumber( HKEY("last_seen"), c->last_seen ));
480 StrBuf *sj = NewStrBuf();
481 SerializeJson(sj, j, 1); // '1' == free the source array
483 add_response_header(h, strdup("Content-type"), strdup("application/json"));
484 h->response_code = 200;
485 h->response_string = strdup("OK");
486 h->response_body_length = StrLength(sj);
487 h->response_body = SmashStrBuf(&sj);
493 * Handle REST/DAV requests for the room itself (such as /ctdl/r/roomname
494 * or /ctdl/r/roomname/ but *not* specific objects within the room)
496 void the_room_itself(struct http_transaction *h, struct ctdlsession *c)
498 // OPTIONS method on the room itself usually is a DAV client assessing what's here.
500 if (!strcasecmp(h->method, "OPTIONS"))
502 options_the_room_itself(h, c);
506 // PROPFIND method on the room itself could be looking for a directory
508 if (!strcasecmp(h->method, "PROPFIND"))
510 propfind_the_room_itself(h, c);
514 // REPORT method on the room itself is probably the dreaded CalDAV tower-of-crapola
516 if (!strcasecmp(h->method, "REPORT"))
518 report_the_room_itself(h, c);
522 // GET method on the room itself is an API call, possibly from our JavaScript front end
524 if (!strcasecmp(h->method, "get"))
526 get_the_room_itself(h, c);
530 // we probably want a "go to this room" for interactive access
536 * Dispatcher for "/ctdl/r" and "/ctdl/r/" for the room list
538 void room_list(struct http_transaction *h, struct ctdlsession *c)
543 ctdl_printf(c, "LKRA");
544 ctdl_readline(c, buf, sizeof(buf));
551 JsonValue *j = NewJsonArray(HKEY("lkra"));
552 while (ctdl_readline(c, buf, sizeof(buf)) , strcmp(buf, "000"))
555 // name|QRflags|QRfloor|QRorder|QRflags2|ra|current_view|default_view|mtime
556 JsonValue *jr = NewJsonObject(HKEY("room"));
558 extract_token(roomname, buf, 0, '|', sizeof roomname);
559 JsonObjectAppend(jr, NewJsonPlainString( HKEY("name"), roomname, -1));
561 int ra = extract_int(buf, 5);
562 JsonObjectAppend(jr, NewJsonBool( HKEY("known"), (ra && UA_KNOWN)));
563 JsonObjectAppend(jr, NewJsonBool( HKEY("hasnewmsgs"), (ra && UA_HASNEWMSGS)));
565 int floor = extract_int(buf, 2);
566 JsonObjectAppend(jr, NewJsonNumber( HKEY("floor"), floor));
568 int rorder = extract_int(buf, 3);
569 JsonObjectAppend(jr, NewJsonNumber( HKEY("rorder"), rorder));
571 JsonArrayAppend(j, jr); // add the room to the array
574 StrBuf *sj = NewStrBuf();
575 SerializeJson(sj, j, 1); // '1' == free the source array
577 add_response_header(h, strdup("Content-type"), strdup("application/json"));
578 h->response_code = 200;
579 h->response_string = strdup("OK");
580 h->response_body_length = StrLength(sj);
581 h->response_body = SmashStrBuf(&sj);
586 * Dispatcher for paths starting with /ctdl/r/
588 void ctdl_r(struct http_transaction *h, struct ctdlsession *c)
590 char requested_roomname[128];
593 // All room-related functions require being "in" the room specified. Are we in that room already?
594 extract_token(requested_roomname, h->uri, 3, '/', sizeof requested_roomname);
595 unescape_input(requested_roomname);
597 if (IsEmptyStr(requested_roomname)) // /ctdl/r/
603 // If not, try to go there.
604 if (strcasecmp(requested_roomname, c->room))
606 ctdl_printf(c, "GOTO %s", requested_roomname);
607 ctdl_readline(c, buf, sizeof(buf));
610 // buf[3] will indicate whether any instant messages are waiting
611 extract_token(c->room, &buf[4], 0, '|', sizeof c->room);
612 c->new_messages = extract_int(&buf[4], 1);
613 c->total_messages = extract_int(&buf[4], 2);
614 // 3 (int)info Info flag: set to nonzero if the user needs to read this room's info file
615 // 4 (int)CCC->room.QRflags Various flags associated with this room.
616 // 5 (long)CCC->room.QRhighest The highest message number present in this room
617 c->last_seen = extract_long(&buf[4], 6); // The highest message number the user has read in this room
618 // 7 (int)rmailflag Boolean flag: 1 if this is a Mail> room, 0 otherwise.
619 // 8 (int)raideflag Nonzero if user is either Aide or a Room Aide in this room
620 // 9 (int)newmailcount The number of new Mail messages the user has
621 // 10 (int)CCC->room.QRfloor The floor number this room resides on
622 c->room_current_view = extract_int(&buf[4], 11);
623 c->room_default_view = extract_int(&buf[4], 12);
624 // 13 (int)is_trash Boolean flag: 1 if this is the user's Trash folder, 0 otherwise.
625 // 14 (int)CCC->room.QRflags2 More flags associated with this room
626 // 15 (long)CCC->room.QRmtime Timestamp of the last write activity in this room
635 // At this point our Citadel client session is "in" the specified room.
637 if (num_tokens(h->uri, '/') == 4) // /ctdl/r/roomname
639 the_room_itself(h, c);
643 extract_token(buf, h->uri, 4, '/', sizeof buf);
644 if (num_tokens(h->uri, '/') == 5)
648 the_room_itself(h, c); // /ctdl/r/roomname/ ( same as /ctdl/r/roomname )
652 object_in_room(h, c); // /ctdl/r/roomname/object
656 if (num_tokens(h->uri, '/') == 6)
658 object_in_room(h, c); // /ctdl/r/roomname/object/ or possibly /ctdl/r/roomname/object/component
662 // If we get to this point, the client specified a valid room but requested an action we don't know how to perform.