2 * Handles GroupDAV PROPFIND requests.
4 * A few notes about our XML output:
6 * --> Yes, we are spewing tags directly instead of using an XML library.
7 * Whining about it will be summarily ignored.
9 * --> XML is deliberately output with no whitespace/newlines between tags.
10 * This makes it difficult to read, but we have discovered clients which
11 * crash when you try to pretty it up.
13 * Copyright (c) 2005-2011 by the citadel.org team
15 * This program is open source software; you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation; either version 3 of the License, or
18 * (at your option) any later version.
20 * This program is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
25 * You should have received a copy of the GNU General Public License
26 * along with this program; if not, write to the Free Software
27 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
31 #include "webserver.h"
34 extern int DisableGzip;
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 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
294 hprintf("Content-encoding: identity\r\n");
298 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
299 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
303 * If the client is requesting the root, show a root node.
305 if (starting_point == 0) {
306 wc_printf("<response>");
308 groupdav_identify_host();
310 wc_printf("</href>");
311 wc_printf("<propstat>");
312 wc_printf("<status>HTTP/1.1 200 OK</status>");
314 wc_printf("<displayname>/</displayname>");
315 wc_printf("<resourcetype><collection/></resourcetype>");
316 wc_printf("<getlastmodified>");
318 wc_printf("</getlastmodified>");
319 wc_printf("</prop>");
320 wc_printf("</propstat>");
321 wc_printf("</response>");
325 * If the client is requesting "/groupdav", show a /groupdav subdirectory.
327 if ((starting_point + WCC->Hdr->HR.dav_depth) >= 1) {
328 wc_printf("<response>");
330 groupdav_identify_host();
331 wc_printf("/groupdav");
332 wc_printf("</href>");
333 wc_printf("<propstat>");
334 wc_printf("<status>HTTP/1.1 200 OK</status>");
336 wc_printf("<displayname>GroupDAV</displayname>");
337 wc_printf("<resourcetype><collection/></resourcetype>");
338 wc_printf("<getlastmodified>");
340 wc_printf("</getlastmodified>");
341 wc_printf("</prop>");
342 wc_printf("</propstat>");
343 wc_printf("</response>");
347 * Now go through the list and make it look like a DAV collection
350 serv_getln(buf, sizeof buf);
351 if (buf[0] == '1') while (serv_getln(buf, sizeof buf), strcmp(buf, "000")) {
353 extract_token(roomname, buf, 0, '|', sizeof roomname);
354 view = extract_int(buf, 7);
355 mtime = extract_long(buf, 8);
356 http_datestring(datestring, sizeof datestring, mtime);
359 * For now, only list rooms that we know a GroupDAV client
360 * might be interested in. In the future we may add
363 * We determine the type of objects which are stored in each
364 * room by looking at the *default* view for the room. This
365 * allows, for example, a Calendar room to appear as a
366 * GroupDAV calendar even if the user has switched it to a
367 * Calendar List view.
369 if ( (view == VIEW_CALENDAR) ||
370 (view == VIEW_TASKS) ||
371 (view == VIEW_ADDRESSBOOK) ||
372 (view == VIEW_NOTES) ||
373 (view == VIEW_JOURNAL) ||
376 is_groupware_collection = 1;
379 is_groupware_collection = 0;
382 if ( (is_groupware_collection) && ((starting_point + WCC->Hdr->HR.dav_depth) >= 2) ) {
383 wc_printf("<response>");
386 groupdav_identify_host();
387 wc_printf("/groupdav/");
388 urlescputs(roomname);
389 wc_printf("/</href>");
391 wc_printf("<propstat>");
392 wc_printf("<status>HTTP/1.1 200 OK</status>");
394 wc_printf("<displayname>");
396 wc_printf("</displayname>");
397 wc_printf("<resourcetype><collection/>");
401 wc_printf("<G:vevent-collection />");
404 wc_printf("<G:vtodo-collection />");
406 case VIEW_ADDRESSBOOK:
407 wc_printf("<G:vcard-collection />");
410 wc_printf("<G:vnotes-collection />");
413 wc_printf("<G:vjournal-collection />");
416 wc_printf("<G:wiki-collection />");
420 wc_printf("</resourcetype>");
421 wc_printf("<getlastmodified>");
423 wc_printf("</getlastmodified>");
424 wc_printf("</prop>");
425 wc_printf("</propstat>");
426 wc_printf("</response>");
429 wc_printf("</multistatus>\n");
437 * The pathname is always going to be /groupdav/room_name/msg_num
439 void groupdav_propfind(void)
442 HashList *SubRooms = NULL;
446 StrBuf *dav_roomname;
450 long dav_msgnum = (-1);
452 char encoded_uid[256];
456 char datestring[256];
460 http_datestring(datestring, sizeof datestring, now);
462 dav_roomname = NewStrBuf();
463 dav_uid = NewStrBuf();
464 StrBufExtract_token(dav_roomname, WCC->Hdr->HR.ReqLine, 0, '/');
465 StrBufExtract_token(dav_uid, WCC->Hdr->HR.ReqLine, 1, '/');
468 * If the room name is blank, the client is requesting a
471 SubRooms = NewHash(1, Flathash);
472 State = GotoRestRoom(SubRooms);
473 if (((State & REST_IN_ROOM) == 0) ||
474 (((State & (REST_GOT_LOCAL_PART)) == 0) &&
475 (WCC->Hdr->HR.dav_depth == 0)))
478 http_datestring(datestring, sizeof datestring, now);
481 * Be rude. Completely ignore the XML request and simply send them
482 * everything we know about. Let the client sort it out.
484 hprintf("HTTP/1.0 207 Multi-Status\r\n");
485 groupdav_common_headers();
486 hprintf("Date: %s\r\n", datestring);
487 hprintf("Content-type: text/xml\r\n");
488 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
489 hprintf("Content-encoding: identity\r\n");
495 * If the client is requesting the root, show a root node.
497 do_template("dav_propfind_top");
499 FreeStrBuf(&dav_roomname);
500 FreeStrBuf(&dav_uid);
501 FreeHashList(&SubRooms);
505 if ((State & (REST_GOT_LOCAL_PART)) == 0) {
506 readloop(headers, eReadEUIDS);
507 FreeHashList(&SubRooms);
514 FreeHashList(&SubRooms);
519 * If the room name is blank, the client is requesting a
522 if (StrLength(dav_roomname) == 0) {
523 groupdav_collection_list();
524 FreeStrBuf(&dav_roomname);
525 FreeStrBuf(&dav_uid);
529 /* Go to the correct room. */
530 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
531 gotoroom(dav_roomname);
533 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
534 hprintf("HTTP/1.1 404 not found\r\n");
535 groupdav_common_headers();
536 hprintf("Date: %s\r\n", datestring);
537 hprintf("Content-Type: text/plain\r\n");
538 wc_printf("There is no folder called \"%s\" on this server.\r\n",
542 FreeStrBuf(&dav_roomname);
543 FreeStrBuf(&dav_uid);
547 /* If dav_uid is non-empty, client is requesting a PROPFIND on
548 * a specific item in the room. This is not valid GroupDAV, but
549 * it is valid WebDAV.
551 if (StrLength(dav_uid) != 0) {
553 dav_msgnum = locate_message_by_uid(ChrPtr(dav_uid));
554 if (dav_msgnum < 0) {
555 hprintf("HTTP/1.1 404 not found\r\n");
556 groupdav_common_headers();
557 hprintf("Content-Type: text/plain\r\n");
558 wc_printf("Object \"%s\" was not found in the \"%s\" folder.\r\n",
563 FreeStrBuf(&dav_roomname);
564 FreeStrBuf(&dav_uid);
568 /* Be rude. Completely ignore the XML request and simply send them
569 * everything we know about (which is going to simply be the ETag and
570 * nothing else). Let the client-side parser sort it out.
572 hprintf("HTTP/1.0 207 Multi-Status\r\n");
573 groupdav_common_headers();
574 hprintf("Date: %s\r\n", datestring);
575 hprintf("Content-type: text/xml\r\n");
576 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
577 hprintf("Content-encoding: identity\r\n");
581 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
582 "<multistatus xmlns=\"DAV:\">"
585 wc_printf("<response>");
588 groupdav_identify_host();
589 wc_printf("/groupdav/");
590 urlescputs(ChrPtr(WCC->CurRoom.name));
591 euid_escapize(encoded_uid, ChrPtr(dav_uid));
592 wc_printf("/%s", encoded_uid);
593 wc_printf("</href>");
594 wc_printf("<propstat>");
595 wc_printf("<status>HTTP/1.1 200 OK</status>");
597 wc_printf("<getetag>\"%ld\"</getetag>", dav_msgnum);
598 wc_printf("<getlastmodified>");
600 wc_printf("</getlastmodified>");
601 wc_printf("</prop>");
602 wc_printf("</propstat>");
604 wc_printf("</response>\n");
605 wc_printf("</multistatus>\n");
607 FreeStrBuf(&dav_roomname);
608 FreeStrBuf(&dav_uid);
611 FreeStrBuf(&dav_roomname);
612 FreeStrBuf(&dav_uid);
616 * We got to this point, which means that the client is requesting
617 * a 'collection' (i.e. a list of all items in the room).
619 * Be rude. Completely ignore the XML request and simply send them
620 * everything we know about (which is going to simply be the ETag and
621 * nothing else). Let the client-side parser sort it out.
623 hprintf("HTTP/1.0 207 Multi-Status\r\n");
624 groupdav_common_headers();
625 hprintf("Date: %s\r\n", datestring);
626 hprintf("Content-type: text/xml\r\n");
627 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
628 hprintf("Content-encoding: identity\r\n");
632 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
633 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
637 /* Transmit the collection resource (FIXME check depth and starting point) */
638 wc_printf("<response>");
641 groupdav_identify_host();
642 wc_printf("/groupdav/");
643 urlescputs(ChrPtr(WCC->CurRoom.name));
644 wc_printf("</href>");
646 wc_printf("<propstat>");
647 wc_printf("<status>HTTP/1.1 200 OK</status>");
649 wc_printf("<displayname>");
650 escputs(ChrPtr(WCC->CurRoom.name));
651 wc_printf("</displayname>");
652 wc_printf("<resourcetype><collection/>");
654 switch(WCC->CurRoom.defview) {
656 wc_printf("<G:vevent-collection />");
659 wc_printf("<G:vtodo-collection />");
661 case VIEW_ADDRESSBOOK:
662 wc_printf("<G:vcard-collection />");
666 wc_printf("</resourcetype>");
667 /* FIXME get the mtime
668 wc_printf("<getlastmodified>");
670 wc_printf("</getlastmodified>");
672 wc_printf("</prop>");
673 wc_printf("</propstat>");
674 wc_printf("</response>");
676 /* Transmit the collection listing (FIXME check depth and starting point) */
678 MsgNum = NewStrBuf();
679 serv_puts("MSGS ALL");
681 StrBuf_ServGetln(MsgNum);
682 if (GetServerStatus(MsgNum, NULL) == 1)
683 while (BufLen = StrBuf_ServGetln(MsgNum),
685 ((BufLen != 3) || strcmp(ChrPtr(MsgNum), "000")) ))
687 msgs = realloc(msgs, ++num_msgs * sizeof(long));
688 msgs[num_msgs-1] = StrTol(MsgNum);
691 if (num_msgs > 0) for (i=0; i<num_msgs; ++i) {
695 serv_printf("MSG0 %ld|3", msgs[i]);
696 StrBuf_ServGetln(MsgNum);
697 if (GetServerStatus(MsgNum, NULL) == 1)
698 while (BufLen = StrBuf_ServGetln(MsgNum),
700 ((BufLen != 3) || strcmp(ChrPtr(MsgNum), "000")) ))
702 if (!strncasecmp(ChrPtr(MsgNum), "exti=", 5)) {
703 strcpy(uid, &ChrPtr(MsgNum)[5]);
705 else if (!strncasecmp(ChrPtr(MsgNum), "time=", 5)) {
706 now = atol(&ChrPtr(MsgNum)[5]);
710 if (!IsEmptyStr(uid)) {
711 wc_printf("<response>");
713 groupdav_identify_host();
714 wc_printf("/groupdav/");
715 urlescputs(ChrPtr(WCC->CurRoom.name));
716 euid_escapize(encoded_uid, uid);
717 wc_printf("/%s", encoded_uid);
718 wc_printf("</href>");
719 switch(WCC->CurRoom.defview) {
721 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
724 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
726 case VIEW_ADDRESSBOOK:
727 wc_printf("<getcontenttype>text/x-vcard</getcontenttype>");
730 wc_printf("<propstat>");
731 wc_printf("<status>HTTP/1.1 200 OK</status>");
733 wc_printf("<getetag>\"%ld\"</getetag>", msgs[i]);
735 http_datestring(datestring, sizeof datestring, now);
736 wc_printf("<getlastmodified>");
738 wc_printf("</getlastmodified>");
740 wc_printf("</prop>");
741 wc_printf("</propstat>");
742 wc_printf("</response>");
747 wc_printf("</multistatus>\n");
757 int ParseMessageListHeaders_EUID(StrBuf *Line,
759 message_summary *Msg,
760 StrBuf *ConversionBuffer)
762 Msg->euid = NewStrBuf();
763 StrBufExtract_NextToken(Msg->euid, Line, pos, '|');
764 Msg->date = StrBufExtractNext_long(Line, pos, '|');
766 return StrLength(Msg->euid) > 0;
769 int DavUIDL_GetParamsGetServerCall(SharedMessageStatus *Stat,
775 Stat->defaultsortorder = 0;
778 Stat->maxmsgs = 9999999;
780 snprintf(cmd, len, "MSGS ALL|||2");
784 int DavUIDL_RenderView_or_Tail(SharedMessageStatus *Stat,
789 DoTemplate(HKEY("msg_listview"),NULL,&NoCtx);
794 int DavUIDL_Cleanup(void **ViewSpecific)
796 /* Note: wDumpContent() will output one additional </div> tag. */
797 /* We ought to move this out into template */
810 RegisterReadLoopHandlerset(
812 DavUIDL_GetParamsGetServerCall,
813 NULL, /// TODO: is this right?
814 ParseMessageListHeaders_EUID,
816 DavUIDL_RenderView_or_Tail,