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.
15 * Copyright (c) 2005-2010 by the citadel.org team
17 * This program is free software; you can redistribute it and/or modify
18 * it under the terms of the GNU General Public License as published by
19 * the Free Software Foundation; either version 3 of the License, or
20 * (at your option) any later version.
22 * This program is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
27 * You should have received a copy of the GNU General Public License
28 * along with this program; if not, write to the Free Software
29 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
33 #include "webserver.h"
37 * Given an encoded UID, translate that to an unencoded Citadel EUID and
38 * then search for it in the current room. Return a message number or -1
42 long locate_message_by_uid(const char *uid) {
44 char decoded_uid[1024];
48 euid_unescapize(decoded_uid, uid);
50 /* ask Citadel if we have this one */
51 serv_printf("EUID %s", decoded_uid);
52 serv_getln(buf, sizeof buf);
54 retval = atol(&buf[4]);
62 * IgnoreFloor: set to 0 or 1 _nothing else_
63 * Subfolders: direct child floors will be put here.
65 const folder *GetRESTFolder(int IgnoreFloor, HashList *Subfolders)
69 const folder *ThisFolder = NULL;
70 const folder *FoundFolder = NULL;
71 const folder *BestGuess = NULL;
78 int iRoom, jURL, urlp;
82 * Guess room: if the full URL matches a room, list thats it. We also need to remember direct sub rooms.
83 * 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.
85 itfl = GetNewHashPos(WCC->Floors, 0);
86 urlp = GetCount(WCC->Directory);
88 while (GetNextHashPos(WCC->Floors, itfl, &len, &Key, &vFolder) &&
92 if (!IgnoreFloor && /* so we can handle legacy URLS... */
93 (ThisFolder->Floor != WCC->CurrentFloor))
97 if (ThisFolder->nRoomNameParts > 1)
99 /*TODO: is that number all right? */
100 // if (urlp - ThisFolder->nRoomNameParts != 2) {
101 // if (BestGuess != NULL)
104 // itd = GetNewHashPos(WCC->Directory, 0);
105 // GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
107 itd = GetNewHashPos(WCC->Directory, 0);
108 GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
110 for (iRoom = 0, /* Fast forward the floorname as we checked it above: */ jURL = IgnoreFloor;
112 (iRoom <= ThisFolder->nRoomNameParts) && (jURL <= urlp);
114 iRoom++, jURL++, GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir))
117 if (strcmp(ChrPtr(ThisFolder->RoomNameParts[iRoom]),
126 if ((iRoom == ThisFolder->nRoomNameParts) && (jURL == urlp))
128 FoundFolder = ThisFolder;
130 /* URL got more parts then this room, so we remember it for the best guess*/
131 else if ((jURL <= urlp) &&
132 (ThisFolder->nRoomNameParts <= nBestGuess))
134 BestGuess = ThisFolder;
135 nBestGuess = jURL - 1;
137 /* Room has more parts than the URL, it might be a sub-room? */
138 else if (iRoom <ThisFolder->nRoomNameParts)
139 {//// TODO: ThisFolder->nRoomNameParts == urlp - IgnoreFloor???
140 Put(Subfolders, SKEY(ThisFolder->name),
141 /* Cast away const, its a reference. */
142 (void*)ThisFolder, reference_free_handler);
146 delta = GetCount(WCC->Directory) - ThisFolder->nRoomNameParts;
147 if ((delta != 2) && (nBestGuess > 1))
150 itd = GetNewHashPos(WCC->Directory, 0);
152 if (!GetNextHashPos(WCC->Directory,
153 itd, &len, &Key, &vDir) ||
162 Dir = (StrBuf*) vDir;
163 if (strcmp(ChrPtr(ThisFolder->name),
172 DeleteHashPos(&itfl);
176 BestGuess = ThisFolder;
179 FoundFolder = ThisFolder;
183 /* TODO: Subfolders: remove patterns not matching the best guess or thisfolder */
184 DeleteHashPos(&itfl);
185 if (FoundFolder != NULL)
194 long GotoRestRoom(HashList *SubRooms)
196 int IgnoreFloor = 0; /* deprecated... */
200 const folder *ThisFolder;
202 State = REST_TOPLEVEL;
204 if (WCC->Hdr->HR.Handler != NULL)
205 State |= REST_IN_NAMESPACE;
207 Count = GetCount(WCC->Directory);
209 if (Count == 0) return State;
211 if (Count >= 1) State |=REST_IN_FLOOR;
212 if (Count == 1) return State;
215 * More than 3 params and no floor found?
216 * -> fall back to old non-floored notation
218 if ((Count >= 3) && (WCC->CurrentFloor == NULL))
223 State |= REST_IN_FLOOR;
225 ThisFolder = GetRESTFolder(IgnoreFloor, SubRooms);
226 if (ThisFolder != NULL)
228 if (WCC->ThisRoom != NULL)
229 if (CompareRooms(WCC->ThisRoom, ThisFolder) != 0)
230 gotoroom(ThisFolder->name);
231 State |= REST_IN_ROOM;
234 if (GetCount(SubRooms) > 0)
235 State |= REST_HAVE_SUB_ROOMS;
237 if ((WCC->ThisRoom != NULL) &&
238 (Count + IgnoreFloor > 3))
240 if (WCC->Hdr->HR.Handler->RID(ExistsID, IgnoreFloor))
242 State |= REST_GOT_LOCAL_PART;
245 /// WHOOPS, not there???
246 State |= REST_NONEXIST;
257 * List rooms (or "collections" in DAV terminology) which contain
258 * interesting groupware objects.
260 void groupdav_collection_list(void)
266 char datestring[256];
269 int is_groupware_collection = 0;
270 int starting_point = 1; /**< 0 for /, 1 for /groupdav/ */
272 if (WCC->Hdr->HR.Handler == NULL) {
275 else if (StrLength(WCC->Hdr->HR.ReqLine) == 0) {
283 http_datestring(datestring, sizeof datestring, now);
286 * Be rude. Completely ignore the XML request and simply send them
287 * everything we know about. Let the client sort it out.
289 hprintf("HTTP/1.0 207 Multi-Status\r\n");
290 groupdav_common_headers();
291 hprintf("Date: %s\r\n", datestring);
292 hprintf("Content-type: text/xml\r\n");
293 hprintf("Content-encoding: identity\r\n");
297 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
298 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
302 * If the client is requesting the root, show a root node.
304 if (starting_point == 0) {
305 wc_printf("<response>");
307 groupdav_identify_host();
309 wc_printf("</href>");
310 wc_printf("<propstat>");
311 wc_printf("<status>HTTP/1.1 200 OK</status>");
313 wc_printf("<displayname>/</displayname>");
314 wc_printf("<resourcetype><collection/></resourcetype>");
315 wc_printf("<getlastmodified>");
317 wc_printf("</getlastmodified>");
318 wc_printf("</prop>");
319 wc_printf("</propstat>");
320 wc_printf("</response>");
324 * If the client is requesting "/groupdav", show a /groupdav subdirectory.
326 if ((starting_point + WCC->Hdr->HR.dav_depth) >= 1) {
327 wc_printf("<response>");
329 groupdav_identify_host();
330 wc_printf("/groupdav");
331 wc_printf("</href>");
332 wc_printf("<propstat>");
333 wc_printf("<status>HTTP/1.1 200 OK</status>");
335 wc_printf("<displayname>GroupDAV</displayname>");
336 wc_printf("<resourcetype><collection/></resourcetype>");
337 wc_printf("<getlastmodified>");
339 wc_printf("</getlastmodified>");
340 wc_printf("</prop>");
341 wc_printf("</propstat>");
342 wc_printf("</response>");
346 * Now go through the list and make it look like a DAV collection
349 serv_getln(buf, sizeof buf);
350 if (buf[0] == '1') while (serv_getln(buf, sizeof buf), strcmp(buf, "000")) {
352 extract_token(roomname, buf, 0, '|', sizeof roomname);
353 view = extract_int(buf, 7);
354 mtime = extract_long(buf, 8);
355 http_datestring(datestring, sizeof datestring, mtime);
358 * For now, only list rooms that we know a GroupDAV client
359 * might be interested in. In the future we may add
362 * We determine the type of objects which are stored in each
363 * room by looking at the *default* view for the room. This
364 * allows, for example, a Calendar room to appear as a
365 * GroupDAV calendar even if the user has switched it to a
366 * Calendar List view.
368 if ( (view == VIEW_CALENDAR) ||
369 (view == VIEW_TASKS) ||
370 (view == VIEW_ADDRESSBOOK) ||
371 (view == VIEW_NOTES) ||
372 (view == VIEW_JOURNAL) ||
375 is_groupware_collection = 1;
378 is_groupware_collection = 0;
381 if ( (is_groupware_collection) && ((starting_point + WCC->Hdr->HR.dav_depth) >= 2) ) {
382 wc_printf("<response>");
385 groupdav_identify_host();
386 wc_printf("/groupdav/");
387 urlescputs(roomname);
388 wc_printf("/</href>");
390 wc_printf("<propstat>");
391 wc_printf("<status>HTTP/1.1 200 OK</status>");
393 wc_printf("<displayname>");
395 wc_printf("</displayname>");
396 wc_printf("<resourcetype><collection/>");
400 wc_printf("<G:vevent-collection />");
403 wc_printf("<G:vtodo-collection />");
405 case VIEW_ADDRESSBOOK:
406 wc_printf("<G:vcard-collection />");
409 wc_printf("<G:vnotes-collection />");
412 wc_printf("<G:vjournal-collection />");
415 wc_printf("<G:wiki-collection />");
419 wc_printf("</resourcetype>");
420 wc_printf("<getlastmodified>");
422 wc_printf("</getlastmodified>");
423 wc_printf("</prop>");
424 wc_printf("</propstat>");
425 wc_printf("</response>");
428 wc_printf("</multistatus>\n");
436 * The pathname is always going to be /groupdav/room_name/msg_num
438 void groupdav_propfind(void)
440 HashList *SubRooms = NULL;
442 StrBuf *dav_roomname;
446 long dav_msgnum = (-1);
448 char encoded_uid[256];
452 char datestring[256];
457 http_datestring(datestring, sizeof datestring, now);
459 dav_roomname = NewStrBuf();
460 dav_uid = NewStrBuf();
461 StrBufExtract_token(dav_roomname, WCC->Hdr->HR.ReqLine, 0, '/');
462 StrBufExtract_token(dav_uid, WCC->Hdr->HR.ReqLine, 1, '/');
465 * If the room name is blank, the client is requesting a
468 SubRooms = NewHash(1, Flathash);
469 State = GotoRestRoom(SubRooms);
470 if (((State & REST_IN_ROOM) == 0) ||
471 (((State & (REST_GOT_LOCAL_PART)) == 0) &&
472 (WCC->Hdr->HR.dav_depth == 0)))
475 http_datestring(datestring, sizeof datestring, now);
478 * Be rude. Completely ignore the XML request and simply send them
479 * everything we know about. Let the client sort it out.
481 hprintf("HTTP/1.0 207 Multi-Status\r\n");
482 groupdav_common_headers();
483 hprintf("Date: %s\r\n", datestring);
484 hprintf("Content-type: text/xml\r\n");
485 hprintf("Content-encoding: identity\r\n");
491 * If the client is requesting the root, show a root node.
493 do_template("dav_propfind_top", NULL);
495 FreeStrBuf(&dav_roomname);
496 FreeStrBuf(&dav_uid);
497 FreeHashList(&SubRooms);
501 if ((State & (REST_GOT_LOCAL_PART)) == 0) {
502 readloop(headers, eReadEUIDS);
503 FreeHashList(&SubRooms);
510 FreeHashList(&SubRooms);
515 * If the room name is blank, the client is requesting a
518 if (StrLength(dav_roomname) == 0) {
519 groupdav_collection_list();
520 FreeStrBuf(&dav_roomname);
521 FreeStrBuf(&dav_uid);
525 /* Go to the correct room. */
526 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
527 gotoroom(dav_roomname);
529 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
530 hprintf("HTTP/1.1 404 not found\r\n");
531 groupdav_common_headers();
532 hprintf("Date: %s\r\n", datestring);
533 hprintf("Content-Type: text/plain\r\n");
534 wc_printf("There is no folder called \"%s\" on this server.\r\n",
538 FreeStrBuf(&dav_roomname);
539 FreeStrBuf(&dav_uid);
543 /* If dav_uid is non-empty, client is requesting a PROPFIND on
544 * a specific item in the room. This is not valid GroupDAV, but
545 * it is valid WebDAV.
547 if (StrLength(dav_uid) != 0) {
549 dav_msgnum = locate_message_by_uid(ChrPtr(dav_uid));
550 if (dav_msgnum < 0) {
551 hprintf("HTTP/1.1 404 not found\r\n");
552 groupdav_common_headers();
553 hprintf("Content-Type: text/plain\r\n");
554 wc_printf("Object \"%s\" was not found in the \"%s\" folder.\r\n",
559 FreeStrBuf(&dav_roomname);
560 FreeStrBuf(&dav_uid);
564 /* Be rude. Completely ignore the XML request and simply send them
565 * everything we know about (which is going to simply be the ETag and
566 * nothing else). Let the client-side parser sort it out.
568 hprintf("HTTP/1.0 207 Multi-Status\r\n");
569 groupdav_common_headers();
570 hprintf("Date: %s\r\n", datestring);
571 hprintf("Content-type: text/xml\r\n");
572 hprintf("Content-encoding: identity\r\n");
576 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
577 "<multistatus xmlns=\"DAV:\">"
580 wc_printf("<response>");
583 groupdav_identify_host();
584 wc_printf("/groupdav/");
585 urlescputs(ChrPtr(WCC->CurRoom.name));
586 euid_escapize(encoded_uid, ChrPtr(dav_uid));
587 wc_printf("/%s", encoded_uid);
588 wc_printf("</href>");
589 wc_printf("<propstat>");
590 wc_printf("<status>HTTP/1.1 200 OK</status>");
592 wc_printf("<getetag>\"%ld\"</getetag>", dav_msgnum);
593 wc_printf("<getlastmodified>");
595 wc_printf("</getlastmodified>");
596 wc_printf("</prop>");
597 wc_printf("</propstat>");
599 wc_printf("</response>\n");
600 wc_printf("</multistatus>\n");
602 FreeStrBuf(&dav_roomname);
603 FreeStrBuf(&dav_uid);
606 FreeStrBuf(&dav_roomname);
607 FreeStrBuf(&dav_uid);
611 * We got to this point, which means that the client is requesting
612 * a 'collection' (i.e. a list of all items in the room).
614 * Be rude. Completely ignore the XML request and simply send them
615 * everything we know about (which is going to simply be the ETag and
616 * nothing else). Let the client-side parser sort it out.
618 hprintf("HTTP/1.0 207 Multi-Status\r\n");
619 groupdav_common_headers();
620 hprintf("Date: %s\r\n", datestring);
621 hprintf("Content-type: text/xml\r\n");
622 hprintf("Content-encoding: identity\r\n");
626 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
627 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
631 /* Transmit the collection resource (FIXME check depth and starting point) */
632 wc_printf("<response>");
635 groupdav_identify_host();
636 wc_printf("/groupdav/");
637 urlescputs(ChrPtr(WCC->CurRoom.name));
638 wc_printf("</href>");
640 wc_printf("<propstat>");
641 wc_printf("<status>HTTP/1.1 200 OK</status>");
643 wc_printf("<displayname>");
644 escputs(ChrPtr(WCC->CurRoom.name));
645 wc_printf("</displayname>");
646 wc_printf("<resourcetype><collection/>");
648 switch(WCC->CurRoom.defview) {
650 wc_printf("<G:vevent-collection />");
653 wc_printf("<G:vtodo-collection />");
655 case VIEW_ADDRESSBOOK:
656 wc_printf("<G:vcard-collection />");
660 wc_printf("</resourcetype>");
661 /* FIXME get the mtime
662 wc_printf("<getlastmodified>");
664 wc_printf("</getlastmodified>");
666 wc_printf("</prop>");
667 wc_printf("</propstat>");
668 wc_printf("</response>");
670 /* Transmit the collection listing (FIXME check depth and starting point) */
672 MsgNum = NewStrBuf();
673 serv_puts("MSGS ALL");
675 StrBuf_ServGetln(MsgNum);
676 if (GetServerStatus(MsgNum, NULL) == 1)
677 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000")) {
678 msgs = realloc(msgs, ++num_msgs * sizeof(long));
679 msgs[num_msgs-1] = StrTol(MsgNum);
682 if (num_msgs > 0) for (i=0; i<num_msgs; ++i) {
686 serv_printf("MSG0 %ld|3", msgs[i]);
687 StrBuf_ServGetln(MsgNum);
688 if (GetServerStatus(MsgNum, NULL) == 1)
689 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000"))
691 if (!strncasecmp(ChrPtr(MsgNum), "exti=", 5)) {
692 strcpy(uid, &ChrPtr(MsgNum)[5]);
694 else if (!strncasecmp(ChrPtr(MsgNum), "time=", 5)) {
695 now = atol(&ChrPtr(MsgNum)[5]);
699 if (!IsEmptyStr(uid)) {
700 wc_printf("<response>");
702 groupdav_identify_host();
703 wc_printf("/groupdav/");
704 urlescputs(ChrPtr(WCC->CurRoom.name));
705 euid_escapize(encoded_uid, uid);
706 wc_printf("/%s", encoded_uid);
707 wc_printf("</href>");
708 switch(WCC->CurRoom.defview) {
710 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
713 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
715 case VIEW_ADDRESSBOOK:
716 wc_printf("<getcontenttype>text/x-vcard</getcontenttype>");
719 wc_printf("<propstat>");
720 wc_printf("<status>HTTP/1.1 200 OK</status>");
722 wc_printf("<getetag>\"%ld\"</getetag>", msgs[i]);
724 http_datestring(datestring, sizeof datestring, now);
725 wc_printf("<getlastmodified>");
727 wc_printf("</getlastmodified>");
729 wc_printf("</prop>");
730 wc_printf("</propstat>");
731 wc_printf("</response>");
736 wc_printf("</multistatus>\n");
746 int ParseMessageListHeaders_EUID(StrBuf *Line,
748 message_summary *Msg,
749 StrBuf *ConversionBuffer)
751 Msg->euid = NewStrBuf();
752 StrBufExtract_NextToken(Msg->euid, Line, pos, '|');
753 Msg->date = StrBufExtractNext_long(Line, pos, '|');
755 return StrLength(Msg->euid) > 0;
758 int DavUIDL_GetParamsGetServerCall(SharedMessageStatus *Stat,
764 Stat->defaultsortorder = 0;
767 Stat->maxmsgs = 9999999;
769 snprintf(cmd, len, "MSGS ALL|||2");
773 int DavUIDL_RenderView_or_Tail(SharedMessageStatus *Stat,
778 DoTemplate(HKEY("msg_listview"),NULL,&NoCtx);
783 int DavUIDL_Cleanup(void **ViewSpecific)
785 /* Note: wDumpContent() will output one additional </div> tag. */
786 /* We ought to move this out into template */
799 RegisterReadLoopHandlerset(
801 DavUIDL_GetParamsGetServerCall,
802 NULL, /// TODO: is this right?
803 ParseMessageListHeaders_EUID,
805 DavUIDL_RenderView_or_Tail,