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
16 * modify it under the terms of the GNU General Public License version 3.
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
26 #include "webserver.h"
29 extern int DisableGzip;
32 * Given an encoded UID, translate that to an unencoded Citadel EUID and
33 * then search for it in the current room. Return a message number or -1
37 long locate_message_by_uid(const char *uid) {
39 char decoded_uid[1024];
43 euid_unescapize(decoded_uid, uid);
45 /* ask Citadel if we have this one */
46 serv_printf("EUID %s", decoded_uid);
47 serv_getln(buf, sizeof buf);
49 retval = atol(&buf[4]);
57 * IgnoreFloor: set to 0 or 1 _nothing else_
58 * Subfolders: direct child floors will be put here.
60 const folder *GetRESTFolder(int IgnoreFloor, HashList *Subfolders)
64 const folder *ThisFolder = NULL;
65 const folder *FoundFolder = NULL;
66 const folder *BestGuess = NULL;
73 int iRoom, jURL, urlp;
77 * Guess room: if the full URL matches a room, list thats it. We also need to remember direct sub rooms.
78 * 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.
80 itfl = GetNewHashPos(WCC->Floors, 0);
81 urlp = GetCount(WCC->Directory);
83 while (GetNextHashPos(WCC->Floors, itfl, &len, &Key, &vFolder) &&
87 if (!IgnoreFloor && /* so we can handle legacy URLS... */
88 (ThisFolder->Floor != WCC->CurrentFloor))
92 if (ThisFolder->nRoomNameParts > 1)
94 /*TODO: is that number all right? */
95 // if (urlp - ThisFolder->nRoomNameParts != 2) {
96 // if (BestGuess != NULL)
99 // itd = GetNewHashPos(WCC->Directory, 0);
100 // GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
102 itd = GetNewHashPos(WCC->Directory, 0);
103 GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir); //TODO: how many to fast forward?
105 for (iRoom = 0, /* Fast forward the floorname as we checked it above: */ jURL = IgnoreFloor;
107 (iRoom <= ThisFolder->nRoomNameParts) && (jURL <= urlp);
109 iRoom++, jURL++, GetNextHashPos(WCC->Directory, itd, &len, &Key, &vDir))
112 if (strcmp(ChrPtr(ThisFolder->RoomNameParts[iRoom]),
121 if ((iRoom == ThisFolder->nRoomNameParts) && (jURL == urlp))
123 FoundFolder = ThisFolder;
125 /* URL got more parts then this room, so we remember it for the best guess*/
126 else if ((jURL <= urlp) &&
127 (ThisFolder->nRoomNameParts <= nBestGuess))
129 BestGuess = ThisFolder;
130 nBestGuess = jURL - 1;
132 /* Room has more parts than the URL, it might be a sub-room? */
133 else if (iRoom <ThisFolder->nRoomNameParts)
134 {//// TODO: ThisFolder->nRoomNameParts == urlp - IgnoreFloor???
135 Put(Subfolders, SKEY(ThisFolder->name),
136 /* Cast away const, its a reference. */
137 (void*)ThisFolder, reference_free_handler);
141 delta = GetCount(WCC->Directory) - ThisFolder->nRoomNameParts;
142 if ((delta != 2) && (nBestGuess > 1))
145 itd = GetNewHashPos(WCC->Directory, 0);
147 if (!GetNextHashPos(WCC->Directory,
148 itd, &len, &Key, &vDir) ||
157 Dir = (StrBuf*) vDir;
158 if (strcmp(ChrPtr(ThisFolder->name),
167 DeleteHashPos(&itfl);
171 BestGuess = ThisFolder;
174 FoundFolder = ThisFolder;
178 /* TODO: Subfolders: remove patterns not matching the best guess or thisfolder */
179 DeleteHashPos(&itfl);
180 if (FoundFolder != NULL)
189 long GotoRestRoom(HashList *SubRooms)
191 int IgnoreFloor = 0; /* deprecated... */
195 const folder *ThisFolder;
197 State = REST_TOPLEVEL;
199 if (WCC->Hdr->HR.Handler != NULL)
200 State |= REST_IN_NAMESPACE;
202 Count = GetCount(WCC->Directory);
204 if (Count == 0) return State;
206 if (Count >= 1) State |=REST_IN_FLOOR;
207 if (Count == 1) return State;
210 * More than 3 params and no floor found?
211 * -> fall back to old non-floored notation
213 if ((Count >= 3) && (WCC->CurrentFloor == NULL))
218 State |= REST_IN_FLOOR;
220 ThisFolder = GetRESTFolder(IgnoreFloor, SubRooms);
221 if (ThisFolder != NULL)
223 if (WCC->ThisRoom != NULL)
224 if (CompareRooms(WCC->ThisRoom, ThisFolder) != 0)
225 gotoroom(ThisFolder->name);
226 State |= REST_IN_ROOM;
229 if (GetCount(SubRooms) > 0)
230 State |= REST_HAVE_SUB_ROOMS;
232 if ((WCC->ThisRoom != NULL) &&
233 (Count + IgnoreFloor > 3))
235 if (WCC->Hdr->HR.Handler->RID(ExistsID, IgnoreFloor))
237 State |= REST_GOT_LOCAL_PART;
240 /// WHOOPS, not there???
241 State |= REST_NONEXIST;
252 * List rooms (or "collections" in DAV terminology) which contain
253 * interesting groupware objects.
255 void dav_collection_list(void)
261 char datestring[256];
264 int is_groupware_collection = 0;
265 int starting_point = 1; /**< 0 for /, 1 for /groupdav/ */
267 if (WCC->Hdr->HR.Handler == NULL) {
270 else if (StrLength(WCC->Hdr->HR.ReqLine) == 0) {
278 http_datestring(datestring, sizeof datestring, now);
281 * Be rude. Completely ignore the XML request and simply send them
282 * everything we know about. Let the client sort it out.
284 hprintf("HTTP/1.0 207 Multi-Status\r\n");
285 dav_common_headers();
286 hprintf("Date: %s\r\n", datestring);
287 hprintf("Content-type: text/xml\r\n");
288 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
289 hprintf("Content-encoding: identity\r\n");
293 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
294 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
298 * If the client is requesting the root, show a root node.
300 if (starting_point == 0) {
301 wc_printf("<response>");
305 wc_printf("</href>");
306 wc_printf("<propstat>");
307 wc_printf("<status>HTTP/1.1 200 OK</status>");
309 wc_printf("<displayname>/</displayname>");
310 wc_printf("<resourcetype><collection/></resourcetype>");
311 wc_printf("<getlastmodified>");
313 wc_printf("</getlastmodified>");
314 wc_printf("</prop>");
315 wc_printf("</propstat>");
316 wc_printf("</response>");
320 * If the client is requesting "/groupdav", show a /groupdav subdirectory.
322 if ((starting_point + WCC->Hdr->HR.dav_depth) >= 1) {
323 wc_printf("<response>");
326 wc_printf("/groupdav");
327 wc_printf("</href>");
328 wc_printf("<propstat>");
329 wc_printf("<status>HTTP/1.1 200 OK</status>");
331 wc_printf("<displayname>GroupDAV</displayname>");
332 wc_printf("<resourcetype><collection/></resourcetype>");
333 wc_printf("<getlastmodified>");
335 wc_printf("</getlastmodified>");
336 wc_printf("</prop>");
337 wc_printf("</propstat>");
338 wc_printf("</response>");
342 * Now go through the list and make it look like a DAV collection
345 serv_getln(buf, sizeof buf);
346 if (buf[0] == '1') while (serv_getln(buf, sizeof buf), strcmp(buf, "000")) {
348 extract_token(roomname, buf, 0, '|', sizeof roomname);
349 view = extract_int(buf, 7);
350 mtime = extract_long(buf, 8);
351 http_datestring(datestring, sizeof datestring, mtime);
354 * For now, only list rooms that we know a GroupDAV client
355 * might be interested in. In the future we may add
358 * We determine the type of objects which are stored in each
359 * room by looking at the *default* view for the room. This
360 * allows, for example, a Calendar room to appear as a
361 * GroupDAV calendar even if the user has switched it to a
362 * Calendar List view.
364 if ( (view == VIEW_CALENDAR) ||
365 (view == VIEW_TASKS) ||
366 (view == VIEW_ADDRESSBOOK) ||
367 (view == VIEW_NOTES) ||
368 (view == VIEW_JOURNAL) ||
371 is_groupware_collection = 1;
374 is_groupware_collection = 0;
377 if ( (is_groupware_collection) && ((starting_point + WCC->Hdr->HR.dav_depth) >= 2) ) {
378 wc_printf("<response>");
382 wc_printf("/groupdav/");
383 urlescputs(roomname);
384 wc_printf("/</href>");
386 wc_printf("<propstat>");
387 wc_printf("<status>HTTP/1.1 200 OK</status>");
389 wc_printf("<displayname>");
391 wc_printf("</displayname>");
392 wc_printf("<resourcetype><collection/>");
396 wc_printf("<G:vevent-collection />");
399 wc_printf("<G:vtodo-collection />");
401 case VIEW_ADDRESSBOOK:
402 wc_printf("<G:vcard-collection />");
405 wc_printf("<G:vnotes-collection />");
408 wc_printf("<G:vjournal-collection />");
411 wc_printf("<G:wiki-collection />");
415 wc_printf("</resourcetype>");
416 wc_printf("<getlastmodified>");
418 wc_printf("</getlastmodified>");
419 wc_printf("</prop>");
420 wc_printf("</propstat>");
421 wc_printf("</response>");
424 wc_printf("</multistatus>\n");
432 * The pathname is always going to be /groupdav/room_name/msg_num
434 void dav_propfind(void)
437 StrBuf *dav_roomname;
441 long dav_msgnum = (-1);
443 char encoded_uid[256];
447 char datestring[256];
450 syslog(LOG_DEBUG, "PROPFIND\n\033[31m%s\033[0m", ChrPtr(WCC->upload));
453 http_datestring(datestring, sizeof datestring, now);
455 dav_roomname = NewStrBuf();
456 dav_uid = NewStrBuf();
457 StrBufExtract_token(dav_roomname, WCC->Hdr->HR.ReqLine, 0, '/');
458 StrBufExtract_token(dav_uid, WCC->Hdr->HR.ReqLine, 1, '/');
461 * If the room name is blank, the client is requesting a folder list.
463 if (StrLength(dav_roomname) == 0) {
464 dav_collection_list();
465 FreeStrBuf(&dav_roomname);
466 FreeStrBuf(&dav_uid);
470 /* Go to the correct room. */
471 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
472 gotoroom(dav_roomname);
474 if (strcasecmp(ChrPtr(WCC->CurRoom.name), ChrPtr(dav_roomname))) {
475 hprintf("HTTP/1.1 404 not found\r\n");
476 dav_common_headers();
477 hprintf("Date: %s\r\n", datestring);
478 hprintf("Content-Type: text/plain\r\n");
479 wc_printf("There is no folder called \"%s\" on this server.\r\n", ChrPtr(dav_roomname));
481 FreeStrBuf(&dav_roomname);
482 FreeStrBuf(&dav_uid);
486 /* If dav_uid is non-empty, client is requesting a PROPFIND on
487 * a specific item in the room. This is not valid GroupDAV, but
488 * it is valid WebDAV (and probably CalDAV too).
490 if (StrLength(dav_uid) != 0) {
492 dav_msgnum = locate_message_by_uid(ChrPtr(dav_uid));
493 if (dav_msgnum < 0) {
494 hprintf("HTTP/1.1 404 not found\r\n");
495 dav_common_headers();
496 hprintf("Content-Type: text/plain\r\n");
497 wc_printf("Object \"%s\" was not found in the \"%s\" folder.\r\n",
502 FreeStrBuf(&dav_roomname);
503 FreeStrBuf(&dav_uid);
507 /* Be rude. Completely ignore the XML request and simply send them
508 * everything we know about (which is going to simply be the ETag and
509 * nothing else). Let the client-side parser sort it out.
511 hprintf("HTTP/1.0 207 Multi-Status\r\n");
512 dav_common_headers();
513 hprintf("Date: %s\r\n", datestring);
514 hprintf("Content-type: text/xml\r\n");
515 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok))
516 hprintf("Content-encoding: identity\r\n");
520 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
521 "<multistatus xmlns=\"DAV:\">"
524 wc_printf("<response>");
528 wc_printf("/groupdav/");
529 urlescputs(ChrPtr(WCC->CurRoom.name));
530 euid_escapize(encoded_uid, ChrPtr(dav_uid));
531 wc_printf("/%s", encoded_uid);
532 wc_printf("</href>");
533 wc_printf("<propstat>");
534 wc_printf("<status>HTTP/1.1 200 OK</status>");
536 wc_printf("<getetag>\"%ld\"</getetag>", dav_msgnum);
537 wc_printf("<getlastmodified>");
539 wc_printf("</getlastmodified>");
540 wc_printf("</prop>");
541 wc_printf("</propstat>");
543 wc_printf("</response>\n");
544 wc_printf("</multistatus>\n");
546 FreeStrBuf(&dav_roomname);
547 FreeStrBuf(&dav_uid);
550 FreeStrBuf(&dav_roomname);
551 FreeStrBuf(&dav_uid);
555 * We got to this point, which means that the client is requesting
556 * a 'collection' (i.e. a list of all items in the room).
558 * Be rude. Completely ignore the XML request and simply send them
559 * everything we know about (which is going to simply be the ETag and
560 * nothing else). Let the client-side parser sort it out.
562 hprintf("HTTP/1.0 207 Multi-Status\r\n");
563 dav_common_headers();
564 hprintf("Date: %s\r\n", datestring);
565 hprintf("Content-type: text/xml\r\n");
566 if (DisableGzip || (!WCC->Hdr->HR.gzip_ok)) {
567 hprintf("Content-encoding: identity\r\n");
571 wc_printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
572 "<multistatus xmlns=\"DAV:\" xmlns:G=\"http://groupdav.org/\">"
576 /* Transmit the collection resource (FIXME check depth and starting point) */
577 wc_printf("<response>");
581 wc_printf("/groupdav/");
582 urlescputs(ChrPtr(WCC->CurRoom.name));
583 wc_printf("</href>");
585 wc_printf("<propstat>");
586 wc_printf("<status>HTTP/1.1 200 OK</status>");
588 wc_printf("<displayname>");
589 escputs(ChrPtr(WCC->CurRoom.name));
590 wc_printf("</displayname>");
591 wc_printf("<resourcetype><collection/>");
593 switch(WCC->CurRoom.defview) {
595 wc_printf("<G:vevent-collection />");
598 wc_printf("<G:vtodo-collection />");
600 case VIEW_ADDRESSBOOK:
601 wc_printf("<G:vcard-collection />");
605 wc_printf("</resourcetype>");
606 /* FIXME get the mtime
607 wc_printf("<getlastmodified>");
609 wc_printf("</getlastmodified>");
611 wc_printf("</prop>");
612 wc_printf("</propstat>");
613 wc_printf("</response>");
615 /* Transmit the collection listing (FIXME check depth and starting point) */
617 MsgNum = NewStrBuf();
618 serv_puts("MSGS ALL");
620 StrBuf_ServGetln(MsgNum);
621 if (GetServerStatus(MsgNum, NULL) == 1)
622 while (BufLen = StrBuf_ServGetln(MsgNum),
624 ((BufLen != 3) || strcmp(ChrPtr(MsgNum), "000")) ))
626 msgs = realloc(msgs, ++num_msgs * sizeof(long));
627 msgs[num_msgs-1] = StrTol(MsgNum);
630 if (num_msgs > 0) for (i=0; i<num_msgs; ++i) {
634 serv_printf("MSG0 %ld|3", msgs[i]);
635 StrBuf_ServGetln(MsgNum);
636 if (GetServerStatus(MsgNum, NULL) == 1)
637 while (BufLen = StrBuf_ServGetln(MsgNum),
639 ((BufLen != 3) || strcmp(ChrPtr(MsgNum), "000")) ))
641 if (!strncasecmp(ChrPtr(MsgNum), "exti=", 5)) {
642 strcpy(uid, &ChrPtr(MsgNum)[5]);
644 else if (!strncasecmp(ChrPtr(MsgNum), "time=", 5)) {
645 now = atol(&ChrPtr(MsgNum)[5]);
649 if (!IsEmptyStr(uid)) {
650 wc_printf("<response>");
653 wc_printf("/groupdav/");
654 urlescputs(ChrPtr(WCC->CurRoom.name));
655 euid_escapize(encoded_uid, uid);
656 wc_printf("/%s", encoded_uid);
657 wc_printf("</href>");
658 switch(WCC->CurRoom.defview) {
660 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
663 wc_printf("<getcontenttype>text/x-ical</getcontenttype>");
665 case VIEW_ADDRESSBOOK:
666 wc_printf("<getcontenttype>text/x-vcard</getcontenttype>");
669 wc_printf("<propstat>");
670 wc_printf("<status>HTTP/1.1 200 OK</status>");
672 wc_printf("<getetag>\"%ld\"</getetag>", msgs[i]);
674 http_datestring(datestring, sizeof datestring, now);
675 wc_printf("<getlastmodified>");
677 wc_printf("</getlastmodified>");
679 wc_printf("</prop>");
680 wc_printf("</propstat>");
681 wc_printf("</response>");
686 wc_printf("</multistatus>\n");
696 int ParseMessageListHeaders_EUID(StrBuf *Line,
698 message_summary *Msg,
699 StrBuf *ConversionBuffer)
701 Msg->euid = NewStrBuf();
702 StrBufExtract_NextToken(Msg->euid, Line, pos, '|');
703 Msg->date = StrBufExtractNext_long(Line, pos, '|');
705 return StrLength(Msg->euid) > 0;
708 int DavUIDL_GetParamsGetServerCall(SharedMessageStatus *Stat,
714 Stat->defaultsortorder = 0;
717 Stat->maxmsgs = 9999999;
719 snprintf(cmd, len, "MSGS ALL|||2");
723 int DavUIDL_RenderView_or_Tail(SharedMessageStatus *Stat,
728 DoTemplate(HKEY("msg_listview"),NULL,&NoCtx);
733 int DavUIDL_Cleanup(void **ViewSpecific)
735 /* Note: wDumpContent() will output one additional </div> tag. */
736 /* We ought to move this out into template */
749 RegisterReadLoopHandlerset(
751 DavUIDL_GetParamsGetServerCall,
752 NULL, /// TODO: is this right?
753 ParseMessageListHeaders_EUID,
755 DavUIDL_RenderView_or_Tail,