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