4 * Handles GroupDAV PROPFIND requests.
6 * A few notes about our XML output:
8 * --> Yes, we are spewing tags directly instead of using an XML library.
9 * Whining about it will be summarily ignored.
11 * --> XML is deliberately output with no whitespace/newlines between tags.
12 * This makes it difficult to read, but we have discovered clients which
13 * crash when you try to pretty it up.
18 #include "webserver.h"
22 * Given an encoded UID, translate that to an unencoded Citadel EUID and
23 * then search for it in the current room. Return a message number or -1
27 long locate_message_by_uid(const char *uid) {
29 char decoded_uid[1024];
33 euid_unescapize(decoded_uid, uid);
35 /* ask Citadel if we have this one */
36 serv_printf("EUID %s", decoded_uid);
37 serv_getln(buf, sizeof buf);
39 retval = atol(&buf[4]);
47 * IgnoreFloor: set to 0 or 1 _nothing else_
48 * Subfolders: direct child floors will be put here.
50 const folder *GetRESTFolder(int IgnoreFloor, HashList *Subfolders)
54 const folder *ThisFolder = NULL;
55 const folder *FoundFolder = NULL;
56 const folder *BestGuess = NULL;
63 int iRoom, jURL, urlp;
67 * Guess room: if the full URL matches a room, list thats it. We also need to remember direct sub rooms.
68 * if the URL is longer, we need to find the "best guess" so we can find the room we're in, and the rest of the URL will be uids and so on.
70 itfl = GetNewHashPos(WCC->Floors, 0);
71 urlp = GetCount(WCC->Directory);
73 while (GetNextHashPos(WCC->Floors, itfl, &len, &Key, &vFolder) &&
77 if (!IgnoreFloor && /* so we can handle legacy URLS... */
78 (ThisFolder->Floor != WCC->CurrentFloor))
82 if (ThisFolder->nRoomNameParts > 1)
84 /*TODO: is that number all right? */
85 // if (urlp - ThisFolder->nRoomNameParts != 2) {
86 // if (BestGuess != NULL)
89 // itd = GetNewHashPos(WCC->Directory, 0);
90 // GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
92 itd = GetNewHashPos(WCC->Directory, 0);
93 GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
95 for (iRoom = 0, /* Fast forward the floorname as we checked it above: */ jURL = IgnoreFloor;
97 (iRoom <= ThisFolder->nRoomNameParts) && (jURL <= urlp);
99 iRoom++, jURL++, GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir))
102 if (strcmp(ChrPtr(ThisFolder->RoomNameParts[iRoom]),
111 if ((iRoom == ThisFolder->nRoomNameParts) && (jURL == urlp))
113 FoundFolder = ThisFolder;
115 /* URL got more parts then this room, so we remember it for the best guess*/
116 else if ((jURL <= urlp) &&
117 (ThisFolder->nRoomNameParts <= nBestGuess))
119 BestGuess = ThisFolder;
120 nBestGuess = jURL - 1;
122 /* Room has more parts than the URL, it might be a sub-room? */
123 else if (iRoom <ThisFolder->nRoomNameParts)
124 {//// TODO: ThisFolder->nRoomNameParts == urlp - IgnoreFloor???
125 Put(Subfolders, SKEY(ThisFolder->name), ThisFolder, reference_free_handler);
129 delta = GetCount(WCC->Directory) - ThisFolder->nRoomNameParts;
130 if ((delta != 2) && (nBestGuess > 1))
133 itd = GetNewHashPos(WCC->Directory, 0);
135 if (!GetNextHashPos(WCC->Directory,
136 itd, &len, &Key, &vDir) ||
145 Dir = (StrBuf*) vDir;
146 if (strcmp(ChrPtr(ThisFolder->name),
155 DeleteHashPos(&itfl);
159 BestGuess = ThisFolder;
162 FoundFolder = ThisFolder;
166 /* TODO: Subfolders: remove patterns not matching the best guess or thisfolder */
167 DeleteHashPos(&itfl);
168 if (FoundFolder != NULL)
179 int IgnoreFloor = 0; /* deprecated... */
183 const folder *ThisFolder;
184 HashList *SubRooms = NULL;
186 State = REST_TOPLEVEL;
188 if (WCC->Hdr->HR.Handler != NULL)
189 State |= REST_IN_NAMESPACE;
191 Count = GetCount(WCC->Directory);
193 if (Count == 0) return State;
195 if (Count >= 1) State |=REST_IN_FLOOR;
196 if (Count == 1) return State;
199 * More than 3 params and no floor found?
200 * -> fall back to old non-floored notation
202 if ((Count >= 3) && (WCC->CurrentFloor == NULL))
207 State |= REST_IN_FLOOR;
208 SubRooms = NewHash(1, Flathash);
209 ThisFolder = GetRESTFolder(IgnoreFloor, SubRooms);
210 if (ThisFolder != NULL)
212 if (WCC->ThisRoom != NULL)
213 if (CompareRooms(WCC->ThisRoom, ThisFolder))
214 gotoroom(ThisFolder->name);
215 State |= REST_IN_ROOM;
218 if ((WCC->ThisRoom != NULL) &&
219 (Count + IgnoreFloor > 3))
221 if (WCC->Hdr->HR.Handler.RID(ExistsID, IgnoreFloor))
223 State |= REST_GOT_EUID;
226 /// WHOOPS, not there???
231 /// TODO: ID detection
232 /// TODO: File detection
241 * List rooms (or "collections" in DAV terminology) which contain
242 * interesting groupware objects.
244 void groupdav_collection_list(void)
250 char datestring[256];
253 int is_groupware_collection = 0;
254 int starting_point = 1; /**< 0 for /, 1 for /groupdav/ */
256 if (WCC->Hdr->HR.Handler == NULL) {
259 else if (StrLength(WCC->Hdr->HR.ReqLine) == 0) {
267 http_datestring(datestring, sizeof datestring, now);
270 * Be rude. Completely ignore the XML request and simply send them
271 * everything we know about. Let the client sort it out.
273 hprintf("HTTP/1.0 207 Multi-Status\r\n");
274 groupdav_common_headers();
275 hprintf("Date: %s\r\n", datestring);
276 hprintf("Content-type: text/xml\r\n");
277 hprintf("Content-encoding: identity\r\n");
281 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
282 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
286 * If the client is requesting the root, show a root node.
288 if (starting_point == 0) {
289 wc_printf("<response>");
291 groupdav_identify_host();
293 wc_printf("</href>");
294 wc_printf("<propstat>");
295 wc_printf("<status>HTTP/1.1 200 OK</status>");
297 wc_printf("<displayname>/</displayname>");
298 wc_printf("<resourcetype><collection/></resourcetype>");
299 wc_printf("<getlastmodified>");
301 wc_printf("</getlastmodified>");
302 wc_printf("</prop>");
303 wc_printf("</propstat>");
304 wc_printf("</response>");
308 * If the client is requesting "/groupdav", show a /groupdav subdirectory.
310 if ((starting_point + WCC->Hdr->HR.dav_depth) >= 1) {
311 wc_printf("<response>");
313 groupdav_identify_host();
314 wc_printf("/groupdav");
315 wc_printf("</href>");
316 wc_printf("<propstat>");
317 wc_printf("<status>HTTP/1.1 200 OK</status>");
319 wc_printf("<displayname>GroupDAV</displayname>");
320 wc_printf("<resourcetype><collection/></resourcetype>");
321 wc_printf("<getlastmodified>");
323 wc_printf("</getlastmodified>");
324 wc_printf("</prop>");
325 wc_printf("</propstat>");
326 wc_printf("</response>");
330 * Now go through the list and make it look like a DAV collection
333 serv_getln(buf, sizeof buf);
334 if (buf[0] == '1') while (serv_getln(buf, sizeof buf), strcmp(buf, "000")) {
336 extract_token(roomname, buf, 0, '|', sizeof roomname);
337 view = extract_int(buf, 7);
338 mtime = extract_long(buf, 8);
339 http_datestring(datestring, sizeof datestring, mtime);
342 * For now, only list rooms that we know a GroupDAV client
343 * might be interested in. In the future we may add
346 * We determine the type of objects which are stored in each
347 * room by looking at the *default* view for the room. This
348 * allows, for example, a Calendar room to appear as a
349 * GroupDAV calendar even if the user has switched it to a
350 * Calendar List view.
352 if ( (view == VIEW_CALENDAR) ||
353 (view == VIEW_TASKS) ||
354 (view == VIEW_ADDRESSBOOK) ||
355 (view == VIEW_NOTES) ||
356 (view == VIEW_JOURNAL) ||
359 is_groupware_collection = 1;
362 is_groupware_collection = 0;
365 if ( (is_groupware_collection) && ((starting_point + WCC->Hdr->HR.dav_depth) >= 2) ) {
366 wc_printf("<response>");
369 groupdav_identify_host();
370 wc_printf("/groupdav/");
371 urlescputs(roomname);
372 wc_printf("/</href>");
374 wc_printf("<propstat>");
375 wc_printf("<status>HTTP/1.1 200 OK</status>");
377 wc_printf("<displayname>");
379 wc_printf("</displayname>");
380 wc_printf("<resourcetype><collection/>");
384 wc_printf("<G:vevent-collection />");
387 wc_printf("<G:vtodo-collection />");
389 case VIEW_ADDRESSBOOK:
390 wc_printf("<G:vcard-collection />");
393 wc_printf("<G:vnotes-collection />");
396 wc_printf("<G:vjournal-collection />");
399 wc_printf("<G:wiki-collection />");
403 wc_printf("</resourcetype>");
404 wc_printf("<getlastmodified>");
406 wc_printf("</getlastmodified>");
407 wc_printf("</prop>");
408 wc_printf("</propstat>");
409 wc_printf("</response>");
412 wc_printf("</multistatus>\n");
420 * The pathname is always going to be /groupdav/room_name/msg_num
422 void groupdav_propfind(void)
425 StrBuf *dav_roomname;
429 long dav_msgnum = (-1);
431 char encoded_uid[256];
435 char datestring[256];
440 http_datestring(datestring, sizeof datestring, now);
442 dav_roomname = NewStrBuf();
443 dav_uid = NewStrBuf();
444 StrBufExtract_token(dav_roomname, WCC->Hdr->HR.ReqLine, 0, '/');
445 StrBufExtract_token(dav_uid, WCC->Hdr->HR.ReqLine, 1, '/');
448 * If the room name is blank, the client is requesting a
451 State = GotoRestRoom();
452 if (((State & REST_IN_ROOM) == 0) ||
453 (((State & (REST_GOT_EUID|REST_GOT_ID|REST_GOT_FILENAME)) == 0) &&
454 (WCC->Hdr->HR.dav_depth == 0)))
457 http_datestring(datestring, sizeof datestring, now);
460 * Be rude. Completely ignore the XML request and simply send them
461 * everything we know about. Let the client sort it out.
463 hprintf("HTTP/1.0 207 Multi-Status\r\n");
464 groupdav_common_headers();
465 hprintf("Date: %s\r\n", datestring);
466 hprintf("Content-type: text/xml\r\n");
467 hprintf("Content-encoding: identity\r\n");
473 * If the client is requesting the root, show a root node.
475 do_template("dav_propfind_top", NULL);
477 FreeStrBuf(&dav_roomname);
478 FreeStrBuf(&dav_uid);
482 if ((State & (REST_GOT_EUID|REST_GOT_ID|REST_GOT_FILENAME)) == 0) {
483 readloop(headers, eReadEUIDS);
490 * If the room name is blank, the client is requesting a
493 if (StrLength(dav_roomname) == 0) {
494 groupdav_collection_list();
495 FreeStrBuf(&dav_roomname);
496 FreeStrBuf(&dav_uid);
500 /* Go to the correct room. */
501 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
502 gotoroom(dav_roomname);
504 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
505 hprintf("HTTP/1.1 404 not found\r\n");
506 groupdav_common_headers();
507 hprintf("Date: %s\r\n", datestring);
508 hprintf("Content-Type: text/plain\r\n");
509 wc_printf("There is no folder called \"%s\" on this server.\r\n",
513 FreeStrBuf(&dav_roomname);
514 FreeStrBuf(&dav_uid);
518 /* If dav_uid is non-empty, client is requesting a PROPFIND on
519 * a specific item in the room. This is not valid GroupDAV, but
520 * it is valid WebDAV.
522 if (StrLength(dav_uid) != 0) {
524 dav_msgnum = locate_message_by_uid(ChrPtr(dav_uid));
525 if (dav_msgnum < 0) {
526 hprintf("HTTP/1.1 404 not found\r\n");
527 groupdav_common_headers();
528 hprintf("Content-Type: text/plain\r\n");
529 wc_printf("Object \"%s\" was not found in the \"%s\" folder.\r\n",
534 FreeStrBuf(&dav_roomname);
535 FreeStrBuf(&dav_uid);
539 /* Be rude. Completely ignore the XML request and simply send them
540 * everything we know about (which is going to simply be the ETag and
541 * nothing else). Let the client-side parser sort it out.
543 hprintf("HTTP/1.0 207 Multi-Status\r\n");
544 groupdav_common_headers();
545 hprintf("Date: %s\r\n", datestring);
546 hprintf("Content-type: text/xml\r\n");
547 hprintf("Content-encoding: identity\r\n");
551 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
552 "<multistatus xmlns=\"DAV:\">"
555 wc_printf("<response>");
558 groupdav_identify_host();
559 wc_printf("/groupdav/");
560 urlescputs(ChrPtr(WCC->CurRoom.name));
561 euid_escapize(encoded_uid, ChrPtr(dav_uid));
562 wc_printf("/%s", encoded_uid);
563 wc_printf("</href>");
564 wc_printf("<propstat>");
565 wc_printf("<status>HTTP/1.1 200 OK</status>");
567 wc_printf("<getetag>\"%ld\"</getetag>", dav_msgnum);
568 wc_printf("<getlastmodified>");
570 wc_printf("</getlastmodified>");
571 wc_printf("</prop>");
572 wc_printf("</propstat>");
574 wc_printf("</response>\n");
575 wc_printf("</multistatus>\n");
577 FreeStrBuf(&dav_roomname);
578 FreeStrBuf(&dav_uid);
581 FreeStrBuf(&dav_roomname);
582 FreeStrBuf(&dav_uid);
586 * We got to this point, which means that the client is requesting
587 * a 'collection' (i.e. a list of all items in the room).
589 * Be rude. Completely ignore the XML request and simply send them
590 * everything we know about (which is going to simply be the ETag and
591 * nothing else). Let the client-side parser sort it out.
593 hprintf("HTTP/1.0 207 Multi-Status\r\n");
594 groupdav_common_headers();
595 hprintf("Date: %s\r\n", datestring);
596 hprintf("Content-type: text/xml\r\n");
597 hprintf("Content-encoding: identity\r\n");
601 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
602 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
606 /* Transmit the collection resource (FIXME check depth and starting point) */
607 wc_printf("<response>");
610 groupdav_identify_host();
611 wc_printf("/groupdav/");
612 urlescputs(ChrPtr(WCC->CurRoom.name));
613 wc_printf("</href>");
615 wc_printf("<propstat>");
616 wc_printf("<status>HTTP/1.1 200 OK</status>");
618 wc_printf("<displayname>");
619 escputs(ChrPtr(WCC->CurRoom.name));
620 wc_printf("</displayname>");
621 wc_printf("<resourcetype><collection/>");
623 switch(WCC->CurRoom.defview) {
625 wc_printf("<G:vevent-collection />");
628 wc_printf("<G:vtodo-collection />");
630 case VIEW_ADDRESSBOOK:
631 wc_printf("<G:vcard-collection />");
635 wc_printf("</resourcetype>");
636 /* FIXME get the mtime
637 wc_printf("<getlastmodified>");
639 wc_printf("</getlastmodified>");
641 wc_printf("</prop>");
642 wc_printf("</propstat>");
643 wc_printf("</response>");
645 /* Transmit the collection listing (FIXME check depth and starting point) */
647 MsgNum = NewStrBuf();
648 serv_puts("MSGS ALL");
650 StrBuf_ServGetln(MsgNum);
651 if (GetServerStatus(MsgNum, NULL) == 1)
652 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000")) {
653 msgs = realloc(msgs, ++num_msgs * sizeof(long));
654 msgs[num_msgs-1] = StrTol(MsgNum);
657 if (num_msgs > 0) for (i=0; i<num_msgs; ++i) {
661 serv_printf("MSG0 %ld|3", msgs[i]);
662 StrBuf_ServGetln(MsgNum);
663 if (GetServerStatus(MsgNum, NULL) == 1)
664 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000"))
666 if (!strncasecmp(ChrPtr(MsgNum), "exti=", 5)) {
667 strcpy(uid, &ChrPtr(MsgNum)[5]);
669 else if (!strncasecmp(ChrPtr(MsgNum), "time=", 5)) {
670 now = atol(&ChrPtr(MsgNum)[5]);
674 if (!IsEmptyStr(uid)) {
675 wc_printf("<response>");
677 groupdav_identify_host();
678 wc_printf("/groupdav/");
679 urlescputs(ChrPtr(WCC->CurRoom.name));
680 euid_escapize(encoded_uid, uid);
681 wc_printf("/%s", encoded_uid);
682 wc_printf("</href>");
683 switch(WCC->CurRoom.defview) {
685 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
688 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
690 case VIEW_ADDRESSBOOK:
691 wc_printf("<getcontenttype>text/x-vcard</getcontenttype>");
694 wc_printf("<propstat>");
695 wc_printf("<status>HTTP/1.1 200 OK</status>");
697 wc_printf("<getetag>\"%ld\"</getetag>", msgs[i]);
699 http_datestring(datestring, sizeof datestring, now);
700 wc_printf("<getlastmodified>");
702 wc_printf("</getlastmodified>");
704 wc_printf("</prop>");
705 wc_printf("</propstat>");
706 wc_printf("</response>");
711 wc_printf("</multistatus>\n");
721 int ParseMessageListHeaders_EUID(StrBuf *Line,
723 message_summary *Msg,
724 StrBuf *ConversionBuffer)
726 Msg->euid = NewStrBuf();
727 StrBufExtract_NextToken(Msg->euid, Line, pos, '|');
728 return StrLength(Msg->euid) > 0;
731 int DavUIDL_GetParamsGetServerCall(SharedMessageStatus *Stat,
737 Stat->defaultsortorder = 0;
740 Stat->maxmsgs = 9999999;
742 snprintf(cmd, len, "MSGS ALL|||2");
746 int DavUIDL_RenderView_or_Tail(SharedMessageStatus *Stat,
751 DoTemplate(HKEY("msg_listview"),NULL,&NoCtx);
756 int DavUIDL_Cleanup(void **ViewSpecific)
758 /* Note: wDumpContent() will output one additional </div> tag. */
759 /* We ought to move this out into template */
772 RegisterReadLoopHandlerset(
774 DavUIDL_GetParamsGetServerCall,
775 NULL, /// TODO: is this right?
776 ParseMessageListHeaders_EUID,
778 DavUIDL_RenderView_or_Tail,