initial work on inline rendering setup
[citadel.git] / webcit-ng / room_functions.c
1 /*
2  * Room functions
3  *
4  * Copyright (c) 1996-2018 by the citadel.org team
5  *
6  * This program is open source software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 #include "webcit.h"
16
17
18 /*
19  * Return a "zero-terminated" array of message numbers in the current room.
20  * Caller owns the memory and must free it.  Returns NULL if any problems.
21  */
22 long *get_msglist(struct ctdlsession *c, char *which_msgs)
23 {
24         char buf[1024];
25         long *msglist = NULL;
26         int num_msgs = 0;
27         int num_alloc = 0;
28
29         ctdl_printf(c, "MSGS %s", which_msgs);
30         ctdl_readline(c, buf, sizeof(buf));
31         if (buf[0] == '1') do
32         {
33                 if (num_msgs >= num_alloc)
34                 {
35                         if (num_alloc == 0)
36                         {
37                                 num_alloc = 1024;
38                                 msglist = malloc(num_alloc * sizeof(long));
39                         }
40                         else
41                         {
42                                 num_alloc *= 2;
43                                 msglist = realloc(msglist, num_alloc * sizeof(long));
44                         }
45                 }
46                 ctdl_readline(c, buf, sizeof(buf));
47                 msglist[num_msgs++] = atol(buf);
48         } while (strcmp(buf, "000"));                           // this makes the last element a "0" terminator
49         return msglist;
50 }
51
52
53 /*
54  * Supplied with a list of potential matches from an If-Match: or If-None-Match: header, and
55  * a message number (which we always use as the entity tag in Citadel), return nonzero if the
56  * message number matches any of the supplied tags in the string.
57  */
58 int match_etags(char *taglist, long msgnum)
59 {
60         int num_tags = num_tokens(taglist, ',');
61         int i=0;
62         char tag[1024];
63
64         if (msgnum <= 0)                        // no msgnum?  no match.
65         {
66                 return(0);
67         }
68
69         for (i=0; i<num_tags; ++i)
70         {
71                 extract_token(tag, taglist, i, ',', sizeof tag);
72                 striplt(tag);
73                 char *lq = (strchr(tag, '"'));
74                 char *rq = (strrchr(tag, '"'));
75                 if (lq < rq)                                    // has two double quotes
76                 {
77                         strcpy(rq, "");
78                         strcpy(tag, ++lq);
79                 }
80                 striplt(tag);
81                 if (!strcmp(tag, "*"))                          // wildcard match
82                 {
83                         return(1);
84                 }
85                 long tagmsgnum = atol(tag);
86                 if ( (tagmsgnum > 0) && (tagmsgnum == msgnum) ) // match
87                 {
88                         return(1);
89                 }
90         }
91
92         return(0);                                              // no match
93 }
94
95
96 /*
97  * Client is requesting a message list
98  */
99 void json_msglist(struct http_transaction *h, struct ctdlsession *c, char *which)
100 {
101         int i = 0;
102         long *msglist = get_msglist(c, which);
103         JsonValue *j = NewJsonArray(HKEY("msgs"));
104
105         if (msglist != NULL)
106         {
107                 for (i=0; msglist[i]>0 ; ++i)
108                 {
109                         JsonArrayAppend(j, NewJsonNumber( HKEY("m"), msglist[i]));
110                 }
111                 free(msglist);
112         }
113
114         StrBuf *sj = NewStrBuf();
115         SerializeJson(sj, j, 1);                        // '1' == free the source array
116
117         add_response_header(h, strdup("Content-type"), strdup("application/json"));
118         h->response_code = 200;
119         h->response_string = strdup("OK");
120         h->response_body_length = StrLength(sj);
121         h->response_body = SmashStrBuf(&sj);
122         return;
123
124
125 }
126
127
128 /*
129  * Client requested an object in a room.
130  */
131 void object_in_room(struct http_transaction *h, struct ctdlsession *c)
132 {
133         char buf[1024];
134         long msgnum = (-1);
135         char unescaped_euid[1024];
136
137         extract_token(buf, h->uri, 4, '/', sizeof buf);
138
139         if (!strncasecmp(buf, "msgs.", 5))                      // Client is requesting a list of message numbers
140         {
141                 json_msglist(h, c, &buf[5]);
142                 return;
143         }
144
145 #if 0
146         if (!strncasecmp(buf, "threads", 5))                    // Client is requesting a threaded view (still kind of fuzzy here)
147         {
148                 threaded_view(h, c, &buf[5]);
149                 return;
150         }
151
152         if (!strncasecmp(buf, "flat", 5))                       // Client is requesting a flat view (still kind of fuzzy here)
153         {
154                 flat_view(h, c, &buf[5]);
155                 return;
156         }
157 #endif
158
159         if (    (c->room_default_view == VIEW_CALENDAR)         // room types where objects are referenced by EUID
160                 || (c->room_default_view == VIEW_TASKS)
161                 || (c->room_default_view == VIEW_ADDRESSBOOK)
162         ) {
163                 safestrncpy(unescaped_euid, buf, sizeof unescaped_euid);
164                 unescape_input(unescaped_euid);
165                 msgnum = locate_message_by_uid(c, unescaped_euid);
166         }
167         else
168         {
169                 msgnum = atol(buf);
170         }
171
172         /*
173          * All methods except PUT require the message to already exist
174          */
175         if ( (msgnum <= 0) && (strcasecmp(h->method, "PUT")) )
176         {
177                 do_404(h);
178         }
179
180         /*
181          * If we get to this point we have a valid message number in an accessible room.
182          */
183         syslog(LOG_DEBUG, "msgnum is %ld, method is %s", msgnum, h->method);
184
185         /*
186          * A sixth component in the URL can be one of two things:
187          * (1) a MIME part specifier, in which case the client wants to download that component within the message
188          * (2) a content-type, in which ase the client wants us to try to render it a certain way
189          */
190         if (num_tokens(h->uri, '/') == 6)
191         {
192                 extract_token(buf, h->uri, 5, '/', sizeof buf);
193                 if (!IsEmptyStr(buf)) {
194                         if (!strcasecmp(buf, "html"))
195                         {
196                                 // FIXME render as html
197                                 html_render_one_message(h, c, msgnum);
198                         }
199                         else
200                         {
201                                 download_mime_component(h, c, msgnum, buf);
202                         }
203                         return;
204                 }
205         }
206
207         /*
208          * Ok, we want a full message, but first let's check for the if[-none]-match headers.
209          */
210         char *if_match = header_val(h, "If-Match");
211         if ( (if_match != NULL) && (!match_etags(if_match, msgnum)) )
212         {
213                 do_412(h);
214                 return;
215         }
216
217         char *if_none_match = header_val(h, "If-None-Match");
218         if ( (if_none_match != NULL) && (match_etags(if_none_match, msgnum)) )
219         {
220                 do_412(h);
221                 return;
222         }
223
224         /*
225          * DOOOOOO ITTTTT!!!
226          */
227
228         if (!strcasecmp(h->method, "DELETE"))
229         {
230                 dav_delete_message(h, c, msgnum);
231         }
232         else if (!strcasecmp(h->method, "GET"))
233         {
234                 dav_get_message(h, c, msgnum);
235         }
236         else if (!strcasecmp(h->method, "PUT"))
237         {
238                 dav_put_message(h, c, unescaped_euid, msgnum);
239         }
240         else
241         {
242                 do_404(h);                                      // Got this far but the method made no sense?  Bummer.
243         }
244
245 }
246
247
248 /*
249  * Called by the_room_itself() when the HTTP method is REPORT
250  */
251 void report_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
252 {
253         if (c->room_default_view == VIEW_CALENDAR)
254         {
255                 caldav_report(h, c);                            // CalDAV REPORTs ... fmgwac
256                 return;
257         }
258
259         do_404(h);      // future implementations like CardDAV will require code paths here
260 }
261
262
263 /*
264  * Called by the_room_itself() when the HTTP method is OPTIONS
265  */
266 void options_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
267 {
268         h->response_code = 200;
269         h->response_string = strdup("OK");
270         if (c->room_default_view == VIEW_CALENDAR)
271         {
272                 add_response_header(h, strdup("DAV"), strdup("1, calendar-access"));    // offer CalDAV
273         }
274         else if (c->room_default_view == VIEW_ADDRESSBOOK)
275         {
276                 add_response_header(h, strdup("DAV"), strdup("1, addressbook"));        // offer CardDAV
277         }
278         else
279         {
280                 add_response_header(h, strdup("DAV"), strdup("1"));                     // ordinary WebDAV for all other room types
281         }
282         add_response_header(h, strdup("Allow"), strdup("OPTIONS, PROPFIND, GET, PUT, REPORT, DELETE"));
283 }
284
285
286 /*
287  * Called by the_room_itself() when the HTTP method is PROPFIND
288  */
289 void propfind_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
290 {
291         char *e;
292         long timestamp;
293         int dav_depth = (header_val(h, "Depth") ? atoi(header_val(h, "Depth")) : INT_MAX);
294         syslog(LOG_DEBUG, "Client PROPFIND requested depth: %d", dav_depth);
295         StrBuf *Buf = NewStrBuf();
296
297         StrBufAppendPrintf(Buf, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
298                 "<D:multistatus "
299                         "xmlns:D=\"DAV:\" "
300                         "xmlns:C=\"urn:ietf:params:xml:ns:caldav\""
301                 ">"
302         );
303
304         /* Transmit the collection resource */
305         StrBufAppendPrintf(Buf, "<D:response>");
306         StrBufAppendPrintf(Buf, "<D:href>");
307         StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0);
308         StrBufAppendPrintf(Buf, "/ctdl/r/");
309         StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
310         StrBufAppendPrintf(Buf, "</D:href>");
311
312         StrBufAppendPrintf(Buf, "<D:propstat>");
313         StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
314         StrBufAppendPrintf(Buf, "<D:prop>");
315         StrBufAppendPrintf(Buf, "<D:displayname>");
316         StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
317         StrBufAppendPrintf(Buf, "</D:displayname>");
318
319         StrBufAppendPrintf(Buf, "<D:owner />");         // empty owner ought to be legal; see rfc3744 section 5.1
320
321         StrBufAppendPrintf(Buf, "<D:resourcetype><D:collection />");
322         switch(c->room_default_view)
323         {
324                 case VIEW_CALENDAR:
325                         StrBufAppendPrintf(Buf, "<C:calendar />");      // RFC4791 section 4.2
326                         break;
327         }
328         StrBufAppendPrintf(Buf, "</D:resourcetype>");
329
330         int enumerate_by_euid = 0;                      // nonzero if messages will be retrieved by euid instead of msgnum
331         switch(c->room_default_view)
332         {
333                 case VIEW_CALENDAR:                     // RFC4791 section 5.2
334                         StrBufAppendPrintf(Buf, "<C:supported-calendar-component-set><C:comp name=\"VEVENT\"/></C:supported-calendar-component-set>");
335                         StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
336                         StrBufAppendPrintf(Buf,         "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
337                         StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
338                         enumerate_by_euid = 1;
339                         break;
340                 case VIEW_TASKS:                        // RFC4791 section 5.2
341                         StrBufAppendPrintf(Buf, "<C:supported-calendar-component-set><C:comp name=\"VTODO\"/></C:supported-calendar-component-set>");
342                         StrBufAppendPrintf(Buf, "<C:supported-calendar-data>");
343                         StrBufAppendPrintf(Buf,         "<C:calendar-data content-type=\"text/calendar\" version=\"2.0\"/>");
344                         StrBufAppendPrintf(Buf, "</C:supported-calendar-data>");
345                         enumerate_by_euid = 1;
346                         break;
347                 case VIEW_ADDRESSBOOK:                  // FIXME put some sort of CardDAV crapola here when we implement it
348                         enumerate_by_euid = 1;
349                         break;
350                 case VIEW_WIKI:                         // FIXME invent "WikiDAV" ?
351                         enumerate_by_euid = 1;
352                         break;
353         }
354
355
356         /* FIXME get the mtime
357         StrBufAppendPrintf(Buf, "<D:getlastmodified>");
358         escputs(datestring);
359         StrBufAppendPrintf(Buf, "</D:getlastmodified>");
360         */
361
362         StrBufAppendPrintf(Buf, "</D:prop>");
363         StrBufAppendPrintf(Buf, "</D:propstat>");
364         StrBufAppendPrintf(Buf, "</D:response>\n");
365
366         // If a depth greater than zero was specified, transmit the collection listing
367         // BEGIN COLLECTION
368         if (dav_depth > 0)
369         {
370                 long *msglist = get_msglist(c, "ALL");
371                 if (msglist)
372                 {
373                         int i;
374                         for (i=0; (msglist[i] > 0); ++i)
375                         {
376                                 if ((i%10) == 0) syslog(LOG_DEBUG, "PROPFIND enumerated %d messages", i);
377                                 e = NULL;       // EUID gets stored here
378                                 timestamp = 0;
379
380                                 char cbuf[1024];
381                                 ctdl_printf(c, "MSG0 %ld|3", msglist[i]);
382                                 ctdl_readline(c, cbuf, sizeof(cbuf));
383                                 if (cbuf[0] == '1') while (ctdl_readline(c, cbuf, sizeof(cbuf)), strcmp(cbuf, "000"))
384                                 {
385                                         if ( (enumerate_by_euid) && (!strncasecmp(cbuf, "exti=", 5)) )
386                                         {
387                                                 // e = strdup(&cbuf[5]);
388                                                 int elen = (2 * strlen(&cbuf[5]));
389                                                 e = malloc(elen);
390                                                 urlesc(e, elen, &cbuf[5]);
391                                         }
392                                         if (!strncasecmp(cbuf, "time=", 5))
393                                         {
394                                                 timestamp = atol(&cbuf[5]);
395                                         }
396                                 }
397                                 if (e == NULL)
398                                 {
399                                         e = malloc(20);
400                                         sprintf(e, "%ld", msglist[i]);
401                                 }
402                                 StrBufAppendPrintf(Buf, "<D:response>");
403
404                                 // Generate the 'href' tag for this message
405                                 StrBufAppendPrintf(Buf, "<D:href>");
406                                 StrBufXMLEscAppend(Buf, NULL, h->site_prefix, strlen(h->site_prefix), 0);
407                                 StrBufAppendPrintf(Buf, "/ctdl/r/");
408                                 StrBufXMLEscAppend(Buf, NULL, c->room, strlen(c->room), 0);
409                                 StrBufAppendPrintf(Buf, "/");
410                                 StrBufXMLEscAppend(Buf, NULL, e, strlen(e), 0);
411                                 StrBufAppendPrintf(Buf, "</D:href>");
412                                 StrBufAppendPrintf(Buf, "<D:propstat>");
413                                 StrBufAppendPrintf(Buf, "<D:status>HTTP/1.1 200 OK</D:status>");
414                                 StrBufAppendPrintf(Buf, "<D:prop>");
415
416                                 switch(c->room_default_view)
417                                 {
418                                         case VIEW_CALENDAR:
419                                                 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vevent</D:getcontenttype>");
420                                                 break;
421                                         case VIEW_TASKS:
422                                                 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/calendar; component=vtodo</D:getcontenttype>");
423                                                 break;
424                                         case VIEW_ADDRESSBOOK:
425                                                 StrBufAppendPrintf(Buf, "<D:getcontenttype>text/x-vcard</D:getcontenttype>");
426                                                 break;
427                                 }
428
429                                 if (timestamp > 0)
430                                 {
431                                         char *datestring = http_datestring(timestamp);
432                                         if (datestring)
433                                         {
434                                                 StrBufAppendPrintf(Buf, "<D:getlastmodified>");
435                                                 StrBufXMLEscAppend(Buf, NULL, datestring, strlen(datestring), 0);
436                                                 StrBufAppendPrintf(Buf, "</D:getlastmodified>");
437                                                 free(datestring);
438                                         }
439                                         if (enumerate_by_euid)          // FIXME ajc 2017oct30 should this be inside the timestamp conditional?
440                                         {
441                                                 StrBufAppendPrintf(Buf, "<D:getetag>\"%ld\"</D:getetag>", msglist[i]);
442                                         }
443                                 }
444                                 StrBufAppendPrintf(Buf, "</D:prop></D:propstat></D:response>\n");
445                                 free(e);
446                         }
447                         free(msglist);
448                 };
449         }
450         // END COLLECTION
451
452         StrBufAppendPrintf(Buf, "</D:multistatus>\n");
453
454         add_response_header(h, strdup("Content-type"), strdup("text/xml"));
455         h->response_code = 207;
456         h->response_string = strdup("Multi-Status");
457         h->response_body_length = StrLength(Buf);
458         h->response_body = SmashStrBuf(&Buf);
459 }
460
461 // some good examples here
462 // http://blogs.nologin.es/rickyepoderi/index.php?/archives/14-Introducing-CalDAV-Part-I.html
463
464
465 /*
466  * Called by the_room_itself() when the HTTP method is PROPFIND
467  */
468 void get_the_room_itself(struct http_transaction *h, struct ctdlsession *c)
469 {
470         JsonValue *j = NewJsonObject(HKEY("gotoroom"));
471
472         JsonObjectAppend(j, NewJsonPlainString( HKEY("name"),           c->room,                -1));
473         JsonObjectAppend(j, NewJsonNumber(      HKEY("current_view"),   c->room_current_view    ));
474         JsonObjectAppend(j, NewJsonNumber(      HKEY("default_view"),   c->room_default_view    ));
475         JsonObjectAppend(j, NewJsonNumber(      HKEY("new_messages"),   c->new_messages         ));
476         JsonObjectAppend(j, NewJsonNumber(      HKEY("total_messages"), c->total_messages       ));
477         JsonObjectAppend(j, NewJsonNumber(      HKEY("last_seen"),      c->last_seen            ));
478
479         StrBuf *sj = NewStrBuf();
480         SerializeJson(sj, j, 1);                        // '1' == free the source array
481
482         add_response_header(h, strdup("Content-type"), strdup("application/json"));
483         h->response_code = 200;
484         h->response_string = strdup("OK");
485         h->response_body_length = StrLength(sj);
486         h->response_body = SmashStrBuf(&sj);
487         return;
488 }
489
490
491 /*
492  * Handle REST/DAV requests for the room itself (such as /ctdl/r/roomname
493  * or /ctdl/r/roomname/ but *not* specific objects within the room)
494  */
495 void the_room_itself(struct http_transaction *h, struct ctdlsession *c)
496 {
497         // OPTIONS method on the room itself usually is a DAV client assessing what's here.
498
499         if (!strcasecmp(h->method, "OPTIONS"))
500         {
501                 options_the_room_itself(h, c);
502                 return;
503         }
504
505         // PROPFIND method on the room itself could be looking for a directory
506
507         if (!strcasecmp(h->method, "PROPFIND"))
508         {
509                 propfind_the_room_itself(h, c);
510                 return;
511         }
512
513         // REPORT method on the room itself is probably the dreaded CalDAV tower-of-crapola
514
515         if (!strcasecmp(h->method, "REPORT"))
516         {
517                 report_the_room_itself(h, c);
518                 return;
519         }
520
521         // GET method on the room itself is an API call, possibly from our JavaScript front end
522
523         if (!strcasecmp(h->method, "get"))
524         {
525                 get_the_room_itself(h, c);
526                 return;
527         }
528
529         // we probably want a "go to this room" for interactive access
530         do_404(h);
531 }
532
533
534 /*
535  * Dispatcher for "/ctdl/r" and "/ctdl/r/" for the room list
536  */
537 void room_list(struct http_transaction *h, struct ctdlsession *c)
538 {
539         char buf[1024];
540         char roomname[1024];
541
542         ctdl_printf(c, "LKRA");
543         ctdl_readline(c, buf, sizeof(buf));
544         if (buf[0] != '1')
545         {
546                 do_502(h);
547                 return;
548         }
549
550         JsonValue *j = NewJsonArray(HKEY("lkra"));
551         while (ctdl_readline(c, buf, sizeof(buf)) , strcmp(buf, "000"))
552         {
553
554                 // name|QRflags|QRfloor|QRorder|QRflags2|ra|current_view|default_view|mtime
555                 JsonValue *jr = NewJsonObject(HKEY("room"));
556
557                 extract_token(roomname, buf, 0, '|', sizeof roomname);
558                 JsonObjectAppend(jr, NewJsonPlainString( HKEY("name"),  roomname, -1));
559
560                 int ra = extract_int(buf, 5);
561                 JsonObjectAppend(jr, NewJsonBool( HKEY("known"), (ra && UA_KNOWN)));
562                 JsonObjectAppend(jr, NewJsonBool( HKEY("hasnewmsgs"), (ra && UA_HASNEWMSGS)));
563
564                 int floor = extract_int(buf, 2);
565                 JsonObjectAppend(jr, NewJsonNumber( HKEY("floor"), floor));
566
567                 int rorder = extract_int(buf, 3);
568                 JsonObjectAppend(jr, NewJsonNumber( HKEY("rorder"), rorder));
569
570                 JsonArrayAppend(j, jr);                 // add the room to the array
571         }
572
573         StrBuf *sj = NewStrBuf();
574         SerializeJson(sj, j, 1);                        // '1' == free the source array
575
576         add_response_header(h, strdup("Content-type"), strdup("application/json"));
577         h->response_code = 200;
578         h->response_string = strdup("OK");
579         h->response_body_length = StrLength(sj);
580         h->response_body = SmashStrBuf(&sj);
581 }
582
583
584 /*
585  * Dispatcher for paths starting with /ctdl/r/
586  */
587 void ctdl_r(struct http_transaction *h, struct ctdlsession *c)
588 {
589         char requested_roomname[128];
590         char buf[1024];
591
592         // All room-related functions require being "in" the room specified.  Are we in that room already?
593         extract_token(requested_roomname, h->uri, 3, '/', sizeof requested_roomname);
594         unescape_input(requested_roomname);
595
596         if (IsEmptyStr(requested_roomname))                     //      /ctdl/r/
597         {
598                 room_list(h, c);
599                 return;
600         }
601
602         // If not, try to go there.
603         if (strcasecmp(requested_roomname, c->room))
604         {
605                 ctdl_printf(c, "GOTO %s", requested_roomname);
606                 ctdl_readline(c, buf, sizeof(buf));
607                 if (buf[0] == '2')
608                 {
609                         // buf[3] will indicate whether any instant messages are waiting
610                         extract_token(c->room, &buf[4], 0, '|', sizeof c->room);
611                         c->new_messages = extract_int(&buf[4], 1);      
612                         c->total_messages = extract_int(&buf[4], 2);    
613                         //      3       (int)info                       Info flag: set to nonzero if the user needs to read this room's info file
614                         //      4       (int)CCC->room.QRflags          Various flags associated with this room.
615                         //      5       (long)CCC->room.QRhighest       The highest message number present in this room
616                         c->last_seen = extract_long(&buf[4], 6);        // The highest message number the user has read in this room
617                         //      7       (int)rmailflag                  Boolean flag: 1 if this is a Mail> room, 0 otherwise.
618                         //      8       (int)raideflag                  Nonzero if user is either Aide or a Room Aide in this room
619                         //      9       (int)newmailcount               The number of new Mail messages the user has
620                         //      10      (int)CCC->room.QRfloor          The floor number this room resides on
621                         c->room_current_view = extract_int(&buf[4], 11);
622                         c->room_default_view = extract_int(&buf[4], 12);
623                         //      13      (int)is_trash                   Boolean flag: 1 if this is the user's Trash folder, 0 otherwise.
624                         //      14      (int)CCC->room.QRflags2         More flags associated with this room
625                         //      15      (long)CCC->room.QRmtime         Timestamp of the last write activity in this room
626                 }
627                 else
628                 {
629                         do_404(h);
630                         return;
631                 }
632         }
633
634         // At this point our Citadel client session is "in" the specified room.
635
636         if (num_tokens(h->uri, '/') == 4)                       //      /ctdl/r/roomname
637         {
638                 the_room_itself(h, c);
639                 return;
640         }
641
642         extract_token(buf, h->uri, 4, '/', sizeof buf);
643         if (num_tokens(h->uri, '/') == 5)
644         {
645                 if (IsEmptyStr(buf))
646                 {
647                         the_room_itself(h, c);                  //      /ctdl/r/roomname/       ( same as /ctdl/r/roomname )
648                 }
649                 else
650                 {
651                         object_in_room(h, c);                   //      /ctdl/r/roomname/object
652                 }
653                 return;
654         }
655         if (num_tokens(h->uri, '/') == 6)
656         {
657                 object_in_room(h, c);                           //      /ctdl/r/roomname/object/ or possibly /ctdl/r/roomname/object/component
658                 return;
659         }
660
661         // If we get to this point, the client specified a valid room but requested an action we don't know how to perform.
662         do_404(h);
663 }