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