5 #include <sys/socket.h>
19 #include "server-cgi.h"
21 #include "configuration.h"
32 #include "trackname.h"
46 static int compare_entry(const void *a
, const void *b
) {
47 const struct entry
*ea
= a
, *eb
= b
;
49 return compare_tracks(ea
->sort
, eb
->sort
,
50 ea
->display
, eb
->display
,
54 static const char *front_url(void) {
58 /* preserve management interface visibility */
59 if((mgmt
= cgi_get("mgmt")) && !strcmp(mgmt
, "true")) {
60 byte_xasprintf(&url
, "%s?mgmt=true", config
->url
);
66 static void header_cookie(struct sink
*output
) {
70 memset(&u
, 0, sizeof u
);
72 parse_url(config
->url
, &u
);
74 dynstr_append_string(d
, "disorder=");
75 dynstr_append_string(d
, login_cookie
);
77 /* Force browser to discard cookie */
78 dynstr_append_string(d
, "disorder=none;Max-Age=0");
81 /* The default domain matches the request host, so we need not override
82 * that. But the default path only goes up to the rightmost /, which would
83 * cause the browser to expose the cookie to other CGI programs on the same
85 dynstr_append_string(d
, ";Version=1;Path=");
86 /* Formally we are supposed to quote the path, since it invariably has a
87 * slash in it. However Safari does not parse quoted paths correctly, so
88 * this won't work. Fortunately nothing else seems to care about proper
89 * quoting of paths, so in practice we get with it. (See also
90 * parse_cookie() where we are liberal about cookie paths on the way back
92 dynstr_append_string(d
, u
.path
);
95 cgi_header(output
, "Set-Cookie", d
->vec
);
98 static void redirect(struct sink
*output
) {
101 back
= cgi_get("back");
102 cgi_header(output
, "Location", back
&& *back ? back
: front_url());
103 header_cookie(output
);
107 static void expand_template(dcgi_state
*ds
, cgi_sink
*output
,
108 const char *action
) {
109 cgi_header(output
->sink
, "Content-Type", "text/html");
110 header_cookie(output
->sink
);
111 cgi_body(output
->sink
);
112 expand(output
, action
, ds
);
115 /* actions ********************************************************************/
117 static void act_disable(cgi_sink
*output
,
120 disorder_disable(ds
->g
->client
);
121 redirect(output
->sink
);
124 static void act_enable(cgi_sink
*output
,
127 disorder_enable(ds
->g
->client
);
128 redirect(output
->sink
);
131 static void act_random_disable(cgi_sink
*output
,
134 disorder_random_disable(ds
->g
->client
);
135 redirect(output
->sink
);
138 static void act_random_enable(cgi_sink
*output
,
141 disorder_random_enable(ds
->g
->client
);
142 redirect(output
->sink
);
145 static void act_remove(cgi_sink
*output
,
149 if(!(id
= cgi_get("id"))) fatal(0, "missing id argument");
151 disorder_remove(ds
->g
->client
, id
);
152 redirect(output
->sink
);
155 static void act_move(cgi_sink
*output
,
157 const char *id
, *delta
;
159 if(!(id
= cgi_get("id"))) fatal(0, "missing id argument");
160 if(!(delta
= cgi_get("delta"))) fatal(0, "missing delta argument");
162 disorder_move(ds
->g
->client
, id
, atoi(delta
));
163 redirect(output
->sink
);
166 static void act_scratch(cgi_sink
*output
,
169 disorder_scratch(ds
->g
->client
, cgi_get("id"));
170 redirect(output
->sink
);
173 static void act_playing(cgi_sink
*output
, dcgi_state
*ds
) {
175 long refresh
= config
->refresh
, length
;
177 int random_enabled
= 0;
180 lookups(ds
, DC_PLAYING
|DC_QUEUE
);
181 cgi_header(output
->sink
, "Content-Type", "text/html");
182 disorder_random_enabled(ds
->g
->client
, &random_enabled
);
183 disorder_enabled(ds
->g
->client
, &enabled
);
185 && ds
->g
->playing
->state
== playing_started
/* i.e. not paused */
186 && !disorder_length(ds
->g
->client
, ds
->g
->playing
->track
, &length
)
188 && ds
->g
->playing
->sofar
>= 0) {
189 /* Try to put the next refresh at the start of the next track. */
191 fin
= now
+ length
- ds
->g
->playing
->sofar
+ config
->gap
;
192 if(now
+ refresh
> fin
)
195 if(ds
->g
->queue
&& ds
->g
->queue
->state
== playing_isscratch
) {
196 /* next track is a scratch, don't leave more than the inter-track gap */
197 if(refresh
> config
->gap
)
198 refresh
= config
->gap
;
200 if(!ds
->g
->playing
&& ((ds
->g
->queue
201 && ds
->g
->queue
->state
!= playing_random
)
202 || random_enabled
) && enabled
) {
203 /* no track playing but playing is enabled and there is something coming
204 * up, must be in a gap */
205 if(refresh
> config
->gap
)
206 refresh
= config
->gap
;
208 byte_snprintf(r
, sizeof r
, "%ld;url=%s", refresh
> 0 ? refresh
: 1,
210 cgi_header(output
->sink
, "Refresh", r
);
211 header_cookie(output
->sink
);
212 cgi_body(output
->sink
);
213 expand(output
, "playing", ds
);
216 static void act_play(cgi_sink
*output
,
218 const char *track
, *dir
;
223 if((track
= cgi_get("file"))) {
224 disorder_play(ds
->g
->client
, track
);
225 } else if((dir
= cgi_get("directory"))) {
226 if(disorder_files(ds
->g
->client
, dir
, 0, &tracks
, &ntracks
)) ntracks
= 0;
228 e
= xmalloc(ntracks
* sizeof (struct entry
));
229 for(n
= 0; n
< ntracks
; ++n
) {
230 e
[n
].path
= tracks
[n
];
231 e
[n
].sort
= trackname_transform("track", tracks
[n
], "sort");
232 e
[n
].display
= trackname_transform("track", tracks
[n
], "display");
234 qsort(e
, ntracks
, sizeof (struct entry
), compare_entry
);
235 for(n
= 0; n
< ntracks
; ++n
)
236 disorder_play(ds
->g
->client
, e
[n
].path
);
239 /* XXX error handling */
240 redirect(output
->sink
);
243 static int clamp(int n
, int min
, int max
) {
251 static const char *volume_url(void) {
254 byte_xasprintf(&url
, "%s?action=volume", config
->url
);
258 static void act_volume(cgi_sink
*output
, dcgi_state
*ds
) {
259 const char *l
, *r
, *d
, *back
;
260 int nd
, changed
= 0;;
262 if((d
= cgi_get("delta"))) {
263 lookups(ds
, DC_VOLUME
);
264 nd
= clamp(atoi(d
), -255, 255);
265 disorder_set_volume(ds
->g
->client
,
266 clamp(ds
->g
->volume_left
+ nd
, 0, 255),
267 clamp(ds
->g
->volume_right
+ nd
, 0, 255));
269 } else if((l
= cgi_get("left")) && (r
= cgi_get("right"))) {
270 disorder_set_volume(ds
->g
->client
, atoi(l
), atoi(r
));
274 /* redirect back to ourselves (but without the volume-changing bits in the
276 cgi_header(output
->sink
, "Location",
277 (back
= cgi_get("back")) ? back
: volume_url());
278 header_cookie(output
->sink
);
279 cgi_body(output
->sink
);
281 cgi_header(output
->sink
, "Content-Type", "text/html");
282 header_cookie(output
->sink
);
283 cgi_body(output
->sink
);
284 expand(output
, "volume", ds
);
288 static void act_prefs_errors(const char *msg
,
289 void attribute((unused
)) *u
) {
290 fatal(0, "error splitting parts list: %s", msg
);
293 static const char *numbered_arg(const char *argname
, int numfile
) {
296 byte_xasprintf(&fullname
, "%d_%s", numfile
, argname
);
297 return cgi_get(fullname
);
300 static void process_prefs(dcgi_state
*ds
, int numfile
) {
301 const char *file
, *name
, *value
, *part
, *parts
, *current
, *context
;
304 if(!(file
= numbered_arg("file", numfile
)))
305 /* The first file doesn't need numbering. */
306 if(numfile
> 0 || !(file
= cgi_get("file")))
308 if((parts
= numbered_arg("parts", numfile
))
309 || (parts
= cgi_get("parts"))) {
310 /* Default context is display. Other contexts not actually tested. */
311 if(!(context
= numbered_arg("context", numfile
))) context
= "display";
312 partslist
= split(parts
, 0, 0, act_prefs_errors
, 0);
313 while((part
= *partslist
++)) {
314 if(!(value
= numbered_arg(part
, numfile
)))
316 /* If it's already right (whether regexps or db) don't change anything,
317 * so we don't fill the database up with rubbish. */
318 if(disorder_part(ds
->g
->client
, (char **)¤t
,
319 file
, context
, part
))
320 fatal(0, "disorder_part() failed");
321 if(!strcmp(current
, value
))
323 byte_xasprintf((char **)&name
, "trackname_%s_%s", context
, part
);
324 disorder_set(ds
->g
->client
, file
, name
, value
);
326 if((value
= numbered_arg("random", numfile
)))
327 disorder_unset(ds
->g
->client
, file
, "pick_at_random");
329 disorder_set(ds
->g
->client
, file
, "pick_at_random", "0");
330 if((value
= numbered_arg("tags", numfile
))) {
332 disorder_unset(ds
->g
->client
, file
, "tags");
334 disorder_set(ds
->g
->client
, file
, "tags", value
);
336 if((value
= numbered_arg("weight", numfile
))) {
337 if(!*value
|| !strcmp(value
, "90000"))
338 disorder_unset(ds
->g
->client
, file
, "weight");
340 disorder_set(ds
->g
->client
, file
, "weight", value
);
342 } else if((name
= cgi_get("name"))) {
343 /* Raw preferences. Not well supported in the templates at the moment. */
344 value
= cgi_get("value");
346 disorder_set(ds
->g
->client
, file
, name
, value
);
348 disorder_unset(ds
->g
->client
, file
, name
);
352 static void act_prefs(cgi_sink
*output
, dcgi_state
*ds
) {
356 if((files
= cgi_get("files"))) nfiles
= atoi(files
);
358 for(numfile
= 0; numfile
< nfiles
; ++numfile
)
359 process_prefs(ds
, numfile
);
360 cgi_header(output
->sink
, "Content-Type", "text/html");
361 header_cookie(output
->sink
);
362 cgi_body(output
->sink
);
363 expand(output
, "prefs", ds
);
366 static void act_pause(cgi_sink
*output
,
369 disorder_pause(ds
->g
->client
);
370 redirect(output
->sink
);
373 static void act_resume(cgi_sink
*output
,
376 disorder_resume(ds
->g
->client
);
377 redirect(output
->sink
);
380 static void act_login(cgi_sink
*output
,
382 const char *username
, *password
, *back
;
385 username
= cgi_get("username");
386 password
= cgi_get("password");
387 if(!username
|| !password
388 || !strcmp(username
, "guest")/*bodge to avoid guest cookies*/) {
389 /* We're just visiting the login page */
390 expand_template(ds
, output
, "login");
393 /* We'll need a new connection as we are going to stop being guest */
395 if(disorder_connect_user(c
, username
, password
)) {
396 cgi_set_option("error", "loginfailed");
397 expand_template(ds
, output
, "login");
400 if(disorder_make_cookie(c
, &login_cookie
)) {
401 cgi_set_option("error", "cookiefailed");
402 expand_template(ds
, output
, "login");
405 /* Use the new connection henceforth */
408 /* We have a new cookie */
409 header_cookie(output
->sink
);
410 cgi_set_option("status", "loginok");
411 if((back
= cgi_get("back")) && *back
)
412 /* Redirect back to somewhere or other */
413 redirect(output
->sink
);
415 /* Stick to the login page */
416 expand_template(ds
, output
, "login");
419 static void act_logout(cgi_sink
*output
,
421 disorder_revoke(ds
->g
->client
);
423 /* Reconnect as guest */
424 disorder_cgi_login(ds
, output
);
425 /* Back to the login page */
426 cgi_set_option("status", "logoutok");
427 expand_template(ds
, output
, "login");
430 static void act_register(cgi_sink
*output
,
432 const char *username
, *password
, *password2
, *email
;
433 char *confirm
, *content_type
;
434 const char *text
, *encoding
, *charset
;
436 username
= cgi_get("username");
437 password
= cgi_get("password1");
438 password2
= cgi_get("password2");
439 email
= cgi_get("email");
441 if(!username
|| !*username
) {
442 cgi_set_option("error", "nousername");
443 expand_template(ds
, output
, "login");
446 if(!password
|| !*password
) {
447 cgi_set_option("error", "nopassword");
448 expand_template(ds
, output
, "login");
451 if(!password2
|| !*password2
|| strcmp(password
, password2
)) {
452 cgi_set_option("error", "passwordmismatch");
453 expand_template(ds
, output
, "login");
456 if(!email
|| !*email
) {
457 cgi_set_option("error", "noemail");
458 expand_template(ds
, output
, "login");
461 /* We could well do better address validation but for now we'll just do the
463 if(!strchr(email
, '@')) {
464 cgi_set_option("error", "bademail");
465 expand_template(ds
, output
, "login");
468 if(disorder_register(ds
->g
->client
, username
, password
, email
, &confirm
)) {
469 cgi_set_option("error", "cannotregister");
470 expand_template(ds
, output
, "login");
473 /* Send the user a mail */
474 /* TODO templatize this */
475 byte_xasprintf((char **)&text
,
476 "Welcome to DisOrder. To active your login, please visit this URL:\n"
478 "%s?c=%s\n", config
->url
, urlencodestring(confirm
));
479 if(!(text
= mime_encode_text(text
, &charset
, &encoding
)))
480 fatal(0, "cannot encode email");
481 byte_xasprintf(&content_type
, "text/plain;charset=%s",
482 quote822(charset
, 0));
483 sendmail("", config
->mail_sender
, email
, "Welcome to DisOrder",
484 encoding
, content_type
, text
); /* TODO error checking */
485 /* We'll go back to the login page with a suitable message */
486 cgi_set_option("status", "registered");
487 expand_template(ds
, output
, "login");
490 static void act_confirm(cgi_sink
*output
,
492 const char *confirmation
;
494 if(!(confirmation
= cgi_get("c"))) {
495 cgi_set_option("error", "noconfirm");
496 expand_template(ds
, output
, "login");
498 /* Confirm our registration */
499 if(disorder_confirm(ds
->g
->client
, confirmation
)) {
500 cgi_set_option("error", "badconfirm");
501 expand_template(ds
, output
, "login");
504 if(disorder_make_cookie(ds
->g
->client
, &login_cookie
)) {
505 cgi_set_option("error", "cookiefailed");
506 expand_template(ds
, output
, "login");
509 /* Discard any cached data JIC */
511 /* We have a new cookie */
512 header_cookie(output
->sink
);
513 cgi_set_option("status", "confirmed");
514 expand_template(ds
, output
, "login");
517 static void act_edituser(cgi_sink
*output
,
519 const char *email
= cgi_get("email"), *password
= cgi_get("changepassword1");
520 const char *password2
= cgi_get("changepassword2");
524 if((password
&& *password
) || (password
&& *password2
)) {
525 if(!password
|| !password2
|| strcmp(password
, password2
)) {
526 cgi_set_option("error", "passwordmismatch");
527 expand_template(ds
, output
, "login");
531 password
= password2
= 0;
534 if(disorder_edituser(ds
->g
->client
, disorder_user(ds
->g
->client
),
536 cgi_set_option("error", "badedit");
537 expand_template(ds
, output
, "login");
542 if(disorder_edituser(ds
->g
->client
, disorder_user(ds
->g
->client
),
543 "password", password
)) {
544 cgi_set_option("error", "badedit");
545 expand_template(ds
, output
, "login");
551 login_cookie
= 0; /* it'll be invalid now */
552 /* This is a bit duplicative of act_login() */
554 if(disorder_connect_user(c
, disorder_user(ds
->g
->client
), password
)) {
555 cgi_set_option("error", "loginfailed");
556 expand_template(ds
, output
, "login");
559 if(disorder_make_cookie(c
, &login_cookie
)) {
560 cgi_set_option("error", "cookiefailed");
561 expand_template(ds
, output
, "login");
564 /* Use the new connection henceforth */
567 /* We have a new cookie */
568 header_cookie(output
->sink
);
570 cgi_set_option("status", "edited");
571 expand_template(ds
, output
, "login");
574 static void act_reminder(cgi_sink
*output
,
576 const char *const username
= cgi_get("username");
578 if(!username
|| !*username
) {
579 cgi_set_option("error", "nousername");
580 expand_template(ds
, output
, "login");
583 if(disorder_reminder(ds
->g
->client
, username
)) {
584 cgi_set_option("error", "reminderfailed");
585 expand_template(ds
, output
, "login");
588 cgi_set_option("status", "reminded");
589 expand_template(ds
, output
, "login");
592 /* expansions *****************************************************************/
594 static void exp_label(int attribute((unused
)) nargs
,
597 void attribute((unused
)) *u
) {
598 cgi_output(output
, "%s", cgi_label(args
[0]));
601 struct trackinfo_state
{
603 const struct queue_entry
*q
;
613 static int compare_result(const void *a
, const void *b
) {
614 const struct result
*ra
= a
, *rb
= b
;
617 if(!(c
= strcmp(ra
->sort
, rb
->sort
)))
618 c
= strcmp(ra
->track
, rb
->track
);
622 static void exp_search(int nargs
,
626 dcgi_state
*ds
= u
, substate
;
628 const char *q
, *context
, *part
, *template;
644 assert(!"should never happen");
645 part
= context
= template = 0; /* quieten compiler */
647 if(ds
->tracks
== 0) {
648 /* we are the top level, let's get some search results */
649 if(!(q
= cgi_get("query"))) return; /* no results yet */
650 if(disorder_search(ds
->g
->client
, q
, &tracks
, &ntracks
)) return;
654 ntracks
= ds
->ntracks
;
656 assert(ntracks
!= 0);
657 /* sort tracks by the appropriate part */
658 r
= xmalloc(ntracks
* sizeof *r
);
659 for(n
= 0; n
< ntracks
; ++n
) {
660 r
[n
].track
= tracks
[n
];
661 if(disorder_part(ds
->g
->client
, (char **)&r
[n
].sort
,
662 tracks
[n
], context
, part
))
663 fatal(0, "disorder_part() failed");
665 qsort(r
, ntracks
, sizeof (struct result
), compare_result
);
666 /* expand the 2nd arg once for each group. We re-use the passed-in tracks
667 * array as we know it's guaranteed to be big enough and isn't going to be
668 * used for anything else any more. */
669 memset(&substate
, 0, sizeof substate
);
674 substate
.tracks
= tracks
;
675 substate
.ntracks
= 0;
678 && !strcmp(r
[m
].sort
, r
[n
].sort
))
679 tracks
[substate
.ntracks
++] = r
[m
++].track
;
680 substate
.last
= (m
== ntracks
);
681 expandstring(output
, template, &substate
);
686 assert(substate
.last
!= 0);
689 static void exp_stats(int attribute((unused
)) nargs
,
690 char attribute((unused
)) **args
,
696 cgi_opentag(output
->sink
, "pre", "class", "stats", (char *)0);
697 if(!disorder_stats(ds
->g
->client
, &v
, 0)) {
699 cgi_output(output
, "%s\n", *v
++);
701 cgi_closetag(output
->sink
, "pre");
704 static char *expandarg(const char *arg
, dcgi_state
*ds
) {
710 output
.sink
= sink_dynstr(&d
);
711 expandstring(&output
, arg
, ds
);
712 dynstr_terminate(&d
);
716 static void exp_isfiles(int attribute((unused
)) nargs
,
717 char attribute((unused
)) **args
,
722 lookups(ds
, DC_FILES
);
723 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->nfiles
));
726 static void exp_isdirectories(int attribute((unused
)) nargs
,
727 char attribute((unused
)) **args
,
732 lookups(ds
, DC_DIRS
);
733 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->ndirs
));
736 static void exp_choose(int attribute((unused
)) nargs
,
745 const char *type
, *what
= expandarg(args
[0], ds
);
747 if(!strcmp(what
, "files")) {
748 lookups(ds
, DC_FILES
);
749 files
= ds
->g
->files
;
750 nfiles
= ds
->g
->nfiles
;
752 } else if(!strcmp(what
, "directories")) {
753 lookups(ds
, DC_DIRS
);
755 nfiles
= ds
->g
->ndirs
;
758 error(0, "unknown @choose@ argument '%s'", what
);
761 e
= xmalloc(nfiles
* sizeof (struct entry
));
762 for(n
= 0; n
< nfiles
; ++n
) {
763 e
[n
].path
= files
[n
];
764 e
[n
].sort
= trackname_transform(type
, files
[n
], "sort");
765 e
[n
].display
= trackname_transform(type
, files
[n
], "display");
767 qsort(e
, nfiles
, sizeof (struct entry
), compare_entry
);
768 memset(&substate
, 0, sizeof substate
);
771 for(n
= 0; n
< nfiles
; ++n
) {
772 substate
.last
= (n
== nfiles
- 1);
774 substate
.entry
= &e
[n
];
775 expandstring(output
, args
[1], &substate
);
780 static void exp_file(int attribute((unused
)) nargs
,
781 char attribute((unused
)) **args
,
787 cgi_output(output
, "%s", ds
->entry
->path
);
789 cgi_output(output
, "%s", ds
->track
->track
);
791 cgi_output(output
, "%s", ds
->tracks
[0]);
794 static void exp_navigate(int attribute((unused
)) nargs
,
800 const char *path
= expandarg(args
[0], ds
);
805 memset(&substate
, 0, sizeof substate
);
807 ptr
= path
+ 1; /* skip root */
809 substate
.nav_path
= path
;
812 while(*ptr
&& *ptr
!= '/')
814 substate
.last
= !*ptr
;
815 substate
.nav_len
= ptr
- path
;
816 substate
.nav_dirlen
= dirlen
;
817 expandstring(output
, args
[1], &substate
);
818 dirlen
= substate
.nav_len
;
825 static void exp_fullname(int attribute((unused
)) nargs
,
826 char attribute((unused
)) **args
,
830 cgi_output(output
, "%.*s", ds
->nav_len
, ds
->nav_path
);
833 static void exp_basename(int nargs
,
841 if((s
= strrchr(args
[0], '/'))) ++s
;
843 cgi_output(output
, "%s", s
);
845 cgi_output(output
, "%.*s", ds
->nav_len
- ds
->nav_dirlen
- 1,
846 ds
->nav_path
+ ds
->nav_dirlen
+ 1);
849 static void exp_dirname(int nargs
,
857 if((s
= strrchr(args
[0], '/')))
858 cgi_output(output
, "%.*s", (int)(s
- args
[0]), args
[0]);
860 cgi_output(output
, "%.*s", ds
->nav_dirlen
, ds
->nav_path
);
863 static void exp_files(int attribute((unused
)) nargs
,
869 const char *nfiles_arg
, *directory
;
873 memset(&substate
, 0, sizeof substate
);
875 if((directory
= cgi_get("directory"))) {
876 /* Prefs for whole directory. */
877 lookups(ds
, DC_FILES
);
878 /* Synthesize args for the file list. */
879 nfiles
= ds
->g
->nfiles
;
880 for(numfile
= 0; numfile
< nfiles
; ++numfile
) {
881 k
= xmalloc(sizeof *k
);
882 byte_xasprintf((char **)&k
->name
, "%d_file", numfile
);
883 k
->value
= ds
->g
->files
[numfile
];
888 /* Args already present. */
889 if((nfiles_arg
= cgi_get("files"))) nfiles
= atoi(nfiles_arg
);
892 for(numfile
= 0; numfile
< nfiles
; ++numfile
) {
893 substate
.index
= numfile
;
894 expandstring(output
, args
[0], &substate
);
898 static void exp_nfiles(int attribute((unused
)) nargs
,
899 char attribute((unused
)) **args
,
903 const char *files_arg
;
905 if(cgi_get("directory")) {
906 lookups(ds
, DC_FILES
);
907 cgi_output(output
, "%d", ds
->g
->nfiles
);
908 } else if((files_arg
= cgi_get("files")))
909 cgi_output(output
, "%s", files_arg
);
911 cgi_output(output
, "1");
914 static void exp_image(int attribute((unused
)) nargs
,
917 void attribute((unused
)) *u
) {
919 const char *imagestem
;
921 byte_xasprintf(&labelname
, "images.%s", args
[0]);
922 if(cgi_label_exists(labelname
))
923 imagestem
= cgi_label(labelname
);
924 else if(strchr(args
[0], '.'))
927 byte_xasprintf((char **)&imagestem
, "%s.png", args
[0]);
928 if(cgi_label_exists("url.static"))
929 cgi_output(output
, "%s/%s", cgi_label("url.static"), imagestem
);
931 cgi_output(output
, "/disorder/%s", imagestem
);