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"
36 extern int DisableGzip;
39 * Given an encoded UID, translate that to an unencoded Citadel EUID and
40 * then search for it in the current room. Return a message number or -1
44 long locate_message_by_uid(const char *uid) {
46 char decoded_uid[1024];
50 euid_unescapize(decoded_uid, uid);
52 /* ask Citadel if we have this one */
53 serv_printf("EUID %s", decoded_uid);
54 serv_getln(buf, sizeof buf);
56 retval = atol(&buf[4]);
64 * IgnoreFloor: set to 0 or 1 _nothing else_
65 * Subfolders: direct child floors will be put here.
67 const folder *GetRESTFolder(int IgnoreFloor, HashList *Subfolders)
71 const folder *ThisFolder = NULL;
72 const folder *FoundFolder = NULL;
73 const folder *BestGuess = NULL;
80 int iRoom, jURL, urlp;
84 * Guess room: if the full URL matches a room, list thats it. We also need to remember direct sub rooms.
85 * 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.
87 itfl = GetNewHashPos(WCC->Floors, 0);
88 urlp = GetCount(WCC->Directory);
90 while (GetNextHashPos(WCC->Floors, itfl, &len, &Key, &vFolder) &&
94 if (!IgnoreFloor && /* so we can handle legacy URLS... */
95 (ThisFolder->Floor != WCC->CurrentFloor))
99 if (ThisFolder->nRoomNameParts > 1)
101 /*TODO: is that number all right? */
102 // if (urlp - ThisFolder->nRoomNameParts != 2) {
103 // if (BestGuess != NULL)
106 // itd = GetNewHashPos(WCC->Directory, 0);
107 // GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
109 itd = GetNewHashPos(WCC->Directory, 0);
110 GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
112 for (iRoom = 0, /* Fast forward the floorname as we checked it above: */ jURL = IgnoreFloor;
114 (iRoom <= ThisFolder->nRoomNameParts) && (jURL <= urlp);
116 iRoom++, jURL++, GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir))
119 if (strcmp(ChrPtr(ThisFolder->RoomNameParts[iRoom]),
128 if ((iRoom == ThisFolder->nRoomNameParts) && (jURL == urlp))
130 FoundFolder = ThisFolder;
132 /* URL got more parts then this room, so we remember it for the best guess*/
133 else if ((jURL <= urlp) &&
134 (ThisFolder->nRoomNameParts <= nBestGuess))
136 BestGuess = ThisFolder;
137 nBestGuess = jURL - 1;
139 /* Room has more parts than the URL, it might be a sub-room? */
140 else if (iRoom <ThisFolder->nRoomNameParts)
141 {//// TODO: ThisFolder->nRoomNameParts == urlp - IgnoreFloor???
142 Put(Subfolders, SKEY(ThisFolder->name),
143 /* Cast away const, its a reference. */
144 (void*)ThisFolder, reference_free_handler);
148 delta = GetCount(WCC->Directory) - ThisFolder->nRoomNameParts;
149 if ((delta != 2) && (nBestGuess > 1))
152 itd = GetNewHashPos(WCC->Directory, 0);
154 if (!GetNextHashPos(WCC->Directory,
155 itd, &len, &Key, &vDir) ||
164 Dir = (StrBuf*) vDir;
165 if (strcmp(ChrPtr(ThisFolder->name),
174 DeleteHashPos(&itfl);
178 BestGuess = ThisFolder;
181 FoundFolder = ThisFolder;
185 /* TODO: Subfolders: remove patterns not matching the best guess or thisfolder */
186 DeleteHashPos(&itfl);
187 if (FoundFolder != NULL)
196 long GotoRestRoom(HashList *SubRooms)
198 int IgnoreFloor = 0; /* deprecated... */
202 const folder *ThisFolder;
204 State = REST_TOPLEVEL;
206 if (WCC->Hdr->HR.Handler != NULL)
207 State |= REST_IN_NAMESPACE;
209 Count = GetCount(WCC->Directory);
211 if (Count == 0) return State;
213 if (Count >= 1) State |=REST_IN_FLOOR;
214 if (Count == 1) return State;
217 * More than 3 params and no floor found?
218 * -> fall back to old non-floored notation
220 if ((Count >= 3) && (WCC->CurrentFloor == NULL))
225 State |= REST_IN_FLOOR;
227 ThisFolder = GetRESTFolder(IgnoreFloor, SubRooms);
228 if (ThisFolder != NULL)
230 if (WCC->ThisRoom != NULL)
231 if (CompareRooms(WCC->ThisRoom, ThisFolder) != 0)
232 gotoroom(ThisFolder->name);
233 State |= REST_IN_ROOM;
236 if (GetCount(SubRooms) > 0)
237 State |= REST_HAVE_SUB_ROOMS;
239 if ((WCC->ThisRoom != NULL) &&
240 (Count + IgnoreFloor > 3))
242 if (WCC->Hdr->HR.Handler->RID(ExistsID, IgnoreFloor))
244 State |= REST_GOT_LOCAL_PART;
247 /// WHOOPS, not there???
248 State |= REST_NONEXIST;
259 * List rooms (or "collections" in DAV terminology) which contain
260 * interesting groupware objects.
262 void groupdav_collection_list(void)
268 char datestring[256];
271 int is_groupware_collection = 0;
272 int starting_point = 1; /**< 0 for /, 1 for /groupdav/ */
274 if (WCC->Hdr->HR.Handler == NULL) {
277 else if (StrLength(WCC->Hdr->HR.ReqLine) == 0) {
285 http_datestring(datestring, sizeof datestring, now);
288 * Be rude. Completely ignore the XML request and simply send them
289 * everything we know about. Let the client sort it out.
291 hprintf("HTTP/1.0 207 Multi-Status\r\n");
292 groupdav_common_headers();
293 hprintf("Date: %s\r\n", datestring);
294 hprintf("Content-type: text/xml\r\n");
295 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
296 hprintf("Content-encoding: identity\r\n");
300 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
301 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
305 * If the client is requesting the root, show a root node.
307 if (starting_point == 0) {
308 wc_printf("<response>");
310 groupdav_identify_host();
312 wc_printf("</href>");
313 wc_printf("<propstat>");
314 wc_printf("<status>HTTP/1.1 200 OK</status>");
316 wc_printf("<displayname>/</displayname>");
317 wc_printf("<resourcetype><collection/></resourcetype>");
318 wc_printf("<getlastmodified>");
320 wc_printf("</getlastmodified>");
321 wc_printf("</prop>");
322 wc_printf("</propstat>");
323 wc_printf("</response>");
327 * If the client is requesting "/groupdav", show a /groupdav subdirectory.
329 if ((starting_point + WCC->Hdr->HR.dav_depth) >= 1) {
330 wc_printf("<response>");
332 groupdav_identify_host();
333 wc_printf("/groupdav");
334 wc_printf("</href>");
335 wc_printf("<propstat>");
336 wc_printf("<status>HTTP/1.1 200 OK</status>");
338 wc_printf("<displayname>GroupDAV</displayname>");
339 wc_printf("<resourcetype><collection/></resourcetype>");
340 wc_printf("<getlastmodified>");
342 wc_printf("</getlastmodified>");
343 wc_printf("</prop>");
344 wc_printf("</propstat>");
345 wc_printf("</response>");
349 * Now go through the list and make it look like a DAV collection
352 serv_getln(buf, sizeof buf);
353 if (buf[0] == '1') while (serv_getln(buf, sizeof buf), strcmp(buf, "000")) {
355 extract_token(roomname, buf, 0, '|', sizeof roomname);
356 view = extract_int(buf, 7);
357 mtime = extract_long(buf, 8);
358 http_datestring(datestring, sizeof datestring, mtime);
361 * For now, only list rooms that we know a GroupDAV client
362 * might be interested in. In the future we may add
365 * We determine the type of objects which are stored in each
366 * room by looking at the *default* view for the room. This
367 * allows, for example, a Calendar room to appear as a
368 * GroupDAV calendar even if the user has switched it to a
369 * Calendar List view.
371 if ( (view == VIEW_CALENDAR) ||
372 (view == VIEW_TASKS) ||
373 (view == VIEW_ADDRESSBOOK) ||
374 (view == VIEW_NOTES) ||
375 (view == VIEW_JOURNAL) ||
378 is_groupware_collection = 1;
381 is_groupware_collection = 0;
384 if ( (is_groupware_collection) && ((starting_point + WCC->Hdr->HR.dav_depth) >= 2) ) {
385 wc_printf("<response>");
388 groupdav_identify_host();
389 wc_printf("/groupdav/");
390 urlescputs(roomname);
391 wc_printf("/</href>");
393 wc_printf("<propstat>");
394 wc_printf("<status>HTTP/1.1 200 OK</status>");
396 wc_printf("<displayname>");
398 wc_printf("</displayname>");
399 wc_printf("<resourcetype><collection/>");
403 wc_printf("<G:vevent-collection />");
406 wc_printf("<G:vtodo-collection />");
408 case VIEW_ADDRESSBOOK:
409 wc_printf("<G:vcard-collection />");
412 wc_printf("<G:vnotes-collection />");
415 wc_printf("<G:vjournal-collection />");
418 wc_printf("<G:wiki-collection />");
422 wc_printf("</resourcetype>");
423 wc_printf("<getlastmodified>");
425 wc_printf("</getlastmodified>");
426 wc_printf("</prop>");
427 wc_printf("</propstat>");
428 wc_printf("</response>");
431 wc_printf("</multistatus>\n");
439 * The pathname is always going to be /groupdav/room_name/msg_num
441 void groupdav_propfind(void)
444 HashList *SubRooms = NULL;
448 StrBuf *dav_roomname;
452 long dav_msgnum = (-1);
454 char encoded_uid[256];
458 char datestring[256];
462 http_datestring(datestring, sizeof datestring, now);
464 dav_roomname = NewStrBuf();
465 dav_uid = NewStrBuf();
466 StrBufExtract_token(dav_roomname, WCC->Hdr->HR.ReqLine, 0, '/');
467 StrBufExtract_token(dav_uid, WCC->Hdr->HR.ReqLine, 1, '/');
470 * If the room name is blank, the client is requesting a
473 SubRooms = NewHash(1, Flathash);
474 State = GotoRestRoom(SubRooms);
475 if (((State & REST_IN_ROOM) == 0) ||
476 (((State & (REST_GOT_LOCAL_PART)) == 0) &&
477 (WCC->Hdr->HR.dav_depth == 0)))
480 http_datestring(datestring, sizeof datestring, now);
483 * Be rude. Completely ignore the XML request and simply send them
484 * everything we know about. Let the client sort it out.
486 hprintf("HTTP/1.0 207 Multi-Status\r\n");
487 groupdav_common_headers();
488 hprintf("Date: %s\r\n", datestring);
489 hprintf("Content-type: text/xml\r\n");
490 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
491 hprintf("Content-encoding: identity\r\n");
497 * If the client is requesting the root, show a root node.
499 do_template("dav_propfind_top", NULL);
501 FreeStrBuf(&dav_roomname);
502 FreeStrBuf(&dav_uid);
503 FreeHashList(&SubRooms);
507 if ((State & (REST_GOT_LOCAL_PART)) == 0) {
508 readloop(headers, eReadEUIDS);
509 FreeHashList(&SubRooms);
516 FreeHashList(&SubRooms);
521 * If the room name is blank, the client is requesting a
524 if (StrLength(dav_roomname) == 0) {
525 groupdav_collection_list();
526 FreeStrBuf(&dav_roomname);
527 FreeStrBuf(&dav_uid);
531 /* Go to the correct room. */
532 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
533 gotoroom(dav_roomname);
535 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
536 hprintf("HTTP/1.1 404 not found\r\n");
537 groupdav_common_headers();
538 hprintf("Date: %s\r\n", datestring);
539 hprintf("Content-Type: text/plain\r\n");
540 wc_printf("There is no folder called \"%s\" on this server.\r\n",
544 FreeStrBuf(&dav_roomname);
545 FreeStrBuf(&dav_uid);
549 /* If dav_uid is non-empty, client is requesting a PROPFIND on
550 * a specific item in the room. This is not valid GroupDAV, but
551 * it is valid WebDAV.
553 if (StrLength(dav_uid) != 0) {
555 dav_msgnum = locate_message_by_uid(ChrPtr(dav_uid));
556 if (dav_msgnum < 0) {
557 hprintf("HTTP/1.1 404 not found\r\n");
558 groupdav_common_headers();
559 hprintf("Content-Type: text/plain\r\n");
560 wc_printf("Object \"%s\" was not found in the \"%s\" folder.\r\n",
565 FreeStrBuf(&dav_roomname);
566 FreeStrBuf(&dav_uid);
570 /* Be rude. Completely ignore the XML request and simply send them
571 * everything we know about (which is going to simply be the ETag and
572 * nothing else). Let the client-side parser sort it out.
574 hprintf("HTTP/1.0 207 Multi-Status\r\n");
575 groupdav_common_headers();
576 hprintf("Date: %s\r\n", datestring);
577 hprintf("Content-type: text/xml\r\n");
578 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
579 hprintf("Content-encoding: identity\r\n");
583 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
584 "<multistatus xmlns=\"DAV:\">"
587 wc_printf("<response>");
590 groupdav_identify_host();
591 wc_printf("/groupdav/");
592 urlescputs(ChrPtr(WCC->CurRoom.name));
593 euid_escapize(encoded_uid, ChrPtr(dav_uid));
594 wc_printf("/%s", encoded_uid);
595 wc_printf("</href>");
596 wc_printf("<propstat>");
597 wc_printf("<status>HTTP/1.1 200 OK</status>");
599 wc_printf("<getetag>\"%ld\"</getetag>", dav_msgnum);
600 wc_printf("<getlastmodified>");
602 wc_printf("</getlastmodified>");
603 wc_printf("</prop>");
604 wc_printf("</propstat>");
606 wc_printf("</response>\n");
607 wc_printf("</multistatus>\n");
609 FreeStrBuf(&dav_roomname);
610 FreeStrBuf(&dav_uid);
613 FreeStrBuf(&dav_roomname);
614 FreeStrBuf(&dav_uid);
618 * We got to this point, which means that the client is requesting
619 * a 'collection' (i.e. a list of all items in the room).
621 * Be rude. Completely ignore the XML request and simply send them
622 * everything we know about (which is going to simply be the ETag and
623 * nothing else). Let the client-side parser sort it out.
625 hprintf("HTTP/1.0 207 Multi-Status\r\n");
626 groupdav_common_headers();
627 hprintf("Date: %s\r\n", datestring);
628 hprintf("Content-type: text/xml\r\n");
629 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
630 hprintf("Content-encoding: identity\r\n");
634 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
635 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
639 /* Transmit the collection resource (FIXME check depth and starting point) */
640 wc_printf("<response>");
643 groupdav_identify_host();
644 wc_printf("/groupdav/");
645 urlescputs(ChrPtr(WCC->CurRoom.name));
646 wc_printf("</href>");
648 wc_printf("<propstat>");
649 wc_printf("<status>HTTP/1.1 200 OK</status>");
651 wc_printf("<displayname>");
652 escputs(ChrPtr(WCC->CurRoom.name));
653 wc_printf("</displayname>");
654 wc_printf("<resourcetype><collection/>");
656 switch(WCC->CurRoom.defview) {
658 wc_printf("<G:vevent-collection />");
661 wc_printf("<G:vtodo-collection />");
663 case VIEW_ADDRESSBOOK:
664 wc_printf("<G:vcard-collection />");
668 wc_printf("</resourcetype>");
669 /* FIXME get the mtime
670 wc_printf("<getlastmodified>");
672 wc_printf("</getlastmodified>");
674 wc_printf("</prop>");
675 wc_printf("</propstat>");
676 wc_printf("</response>");
678 /* Transmit the collection listing (FIXME check depth and starting point) */
680 MsgNum = NewStrBuf();
681 serv_puts("MSGS ALL");
683 StrBuf_ServGetln(MsgNum);
684 if (GetServerStatus(MsgNum, NULL) == 1)
685 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000")) {
686 msgs = realloc(msgs, ++num_msgs * sizeof(long));
687 msgs[num_msgs-1] = StrTol(MsgNum);
690 if (num_msgs > 0) for (i=0; i<num_msgs; ++i) {
694 serv_printf("MSG0 %ld|3", msgs[i]);
695 StrBuf_ServGetln(MsgNum);
696 if (GetServerStatus(MsgNum, NULL) == 1)
697 while (BufLen = StrBuf_ServGetln(MsgNum), strcmp(ChrPtr(MsgNum), "000"))
699 if (!strncasecmp(ChrPtr(MsgNum), "exti=", 5)) {
700 strcpy(uid, &ChrPtr(MsgNum)[5]);
702 else if (!strncasecmp(ChrPtr(MsgNum), "time=", 5)) {
703 now = atol(&ChrPtr(MsgNum)[5]);
707 if (!IsEmptyStr(uid)) {
708 wc_printf("<response>");
710 groupdav_identify_host();
711 wc_printf("/groupdav/");
712 urlescputs(ChrPtr(WCC->CurRoom.name));
713 euid_escapize(encoded_uid, uid);
714 wc_printf("/%s", encoded_uid);
715 wc_printf("</href>");
716 switch(WCC->CurRoom.defview) {
718 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
721 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
723 case VIEW_ADDRESSBOOK:
724 wc_printf("<getcontenttype>text/x-vcard</getcontenttype>");
727 wc_printf("<propstat>");
728 wc_printf("<status>HTTP/1.1 200 OK</status>");
730 wc_printf("<getetag>\"%ld\"</getetag>", msgs[i]);
732 http_datestring(datestring, sizeof datestring, now);
733 wc_printf("<getlastmodified>");
735 wc_printf("</getlastmodified>");
737 wc_printf("</prop>");
738 wc_printf("</propstat>");
739 wc_printf("</response>");
744 wc_printf("</multistatus>\n");
754 int ParseMessageListHeaders_EUID(StrBuf *Line,
756 message_summary *Msg,
757 StrBuf *ConversionBuffer)
759 Msg->euid = NewStrBuf();
760 StrBufExtract_NextToken(Msg->euid, Line, pos, '|');
761 Msg->date = StrBufExtractNext_long(Line, pos, '|');
763 return StrLength(Msg->euid) > 0;
766 int DavUIDL_GetParamsGetServerCall(SharedMessageStatus *Stat,
772 Stat->defaultsortorder = 0;
775 Stat->maxmsgs = 9999999;
777 snprintf(cmd, len, "MSGS ALL|||2");
781 int DavUIDL_RenderView_or_Tail(SharedMessageStatus *Stat,
786 DoTemplate(HKEY("msg_listview"),NULL,&NoCtx);
791 int DavUIDL_Cleanup(void **ViewSpecific)
793 /* Note: wDumpContent() will output one additional </div> tag. */
794 /* We ought to move this out into template */
807 RegisterReadLoopHandlerset(
809 DavUIDL_GetParamsGetServerCall,
810 NULL, /// TODO: is this right?
811 ParseMessageListHeaders_EUID,
813 DavUIDL_RenderView_or_Tail,