2 * This file is part of DisOrder.
3 * Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
26 #include <sys/types.h>
27 #include <sys/socket.h>
43 #include "configuration.h"
54 #include "trackname.h"
63 static void expand(cgi_sink
*output
,
66 static void expandstring(cgi_sink
*output
,
76 static const char *nonce(void) {
77 static unsigned long count
;
80 byte_xasprintf(&s
, "%lx%lx%lx",
81 (unsigned long)time(0),
82 (unsigned long)getpid(),
87 static int compare_entry(const void *a
, const void *b
) {
88 const struct entry
*ea
= a
, *eb
= b
;
90 return compare_tracks(ea
->sort
, eb
->sort
,
91 ea
->display
, eb
->display
,
95 static const char *front_url(void) {
99 /* preserve management interface visibility */
100 if((mgmt
= cgi_get("mgmt")) && !strcmp(mgmt
, "true")) {
101 byte_xasprintf(&url
, "%s?mgmt=true", config
->url
);
107 static void header_cookie(struct sink
*output
) {
111 memset(&u
, 0, sizeof u
);
113 parse_url(config
->url
, &u
);
115 dynstr_append_string(d
, "disorder=");
116 dynstr_append_string(d
, quote822(login_cookie
, 0));
118 /* Force browser to discard cookie */
119 dynstr_append_string(d
, "disorder=none;Max-Age=0");
122 /* The default domain matches the request host, so we need not override
123 * that. But the default path only goes up to the rightmost /, which would
124 * cause the browser to expose the cookie to other CGI programs on the same
126 dynstr_append_string(d
, ";Path=");
127 dynstr_append_string(d
, quote822(u
.path
, 0));
130 cgi_header(output
, "Set-Cookie", d
->vec
);
133 static void redirect(struct sink
*output
) {
136 back
= cgi_get("back");
137 cgi_header(output
, "Location", back
&& *back ? back
: front_url());
138 header_cookie(output
);
142 static void expand_template(dcgi_state
*ds
, cgi_sink
*output
,
143 const char *action
) {
144 cgi_header(output
->sink
, "Content-Type", "text/html");
145 header_cookie(output
->sink
);
146 cgi_body(output
->sink
);
147 expand(output
, action
, ds
);
150 static void lookups(dcgi_state
*ds
, unsigned want
) {
152 struct queue_entry
*r
, *rnext
;
153 const char *dir
, *re
;
156 if(ds
->g
->client
&& (need
= want
^ (ds
->g
->flags
& want
)) != 0) {
158 disorder_queue(ds
->g
->client
, &ds
->g
->queue
);
159 if(need
& DC_PLAYING
)
160 disorder_playing(ds
->g
->client
, &ds
->g
->playing
);
162 disorder_new_tracks(ds
->g
->client
, &ds
->g
->new, &ds
->g
->nnew
, 0);
163 if(need
& DC_RECENT
) {
164 /* we need to reverse the order of the list */
165 disorder_recent(ds
->g
->client
, &r
);
168 r
->next
= ds
->g
->recent
;
174 disorder_get_volume(ds
->g
->client
,
175 &ds
->g
->volume_left
, &ds
->g
->volume_right
);
176 if(need
& (DC_FILES
|DC_DIRS
)) {
177 if(!(dir
= cgi_get("directory")))
179 re
= cgi_get("regexp");
181 if(disorder_directories(ds
->g
->client
, dir
, re
,
182 &ds
->g
->dirs
, &ds
->g
->ndirs
))
185 if(disorder_files(ds
->g
->client
, dir
, re
,
186 &ds
->g
->files
, &ds
->g
->nfiles
))
189 if(need
& DC_RIGHTS
) {
190 ds
->g
->rights
= RIGHT_READ
; /* fail-safe */
191 if(!disorder_userinfo(ds
->g
->client
, disorder_user(ds
->g
->client
),
193 parse_rights(rights
, &ds
->g
->rights
, 1);
195 ds
->g
->flags
|= need
;
199 /* actions ********************************************************************/
201 static void act_disable(cgi_sink
*output
,
204 disorder_disable(ds
->g
->client
);
205 redirect(output
->sink
);
208 static void act_enable(cgi_sink
*output
,
211 disorder_enable(ds
->g
->client
);
212 redirect(output
->sink
);
215 static void act_random_disable(cgi_sink
*output
,
218 disorder_random_disable(ds
->g
->client
);
219 redirect(output
->sink
);
222 static void act_random_enable(cgi_sink
*output
,
225 disorder_random_enable(ds
->g
->client
);
226 redirect(output
->sink
);
229 static void act_remove(cgi_sink
*output
,
233 if(!(id
= cgi_get("id"))) fatal(0, "missing id argument");
235 disorder_remove(ds
->g
->client
, id
);
236 redirect(output
->sink
);
239 static void act_move(cgi_sink
*output
,
241 const char *id
, *delta
;
243 if(!(id
= cgi_get("id"))) fatal(0, "missing id argument");
244 if(!(delta
= cgi_get("delta"))) fatal(0, "missing delta argument");
246 disorder_move(ds
->g
->client
, id
, atoi(delta
));
247 redirect(output
->sink
);
250 static void act_scratch(cgi_sink
*output
,
253 disorder_scratch(ds
->g
->client
, cgi_get("id"));
254 redirect(output
->sink
);
257 static void act_playing(cgi_sink
*output
, dcgi_state
*ds
) {
259 long refresh
= config
->refresh
, length
;
261 int random_enabled
= 0;
264 lookups(ds
, DC_PLAYING
|DC_QUEUE
);
265 cgi_header(output
->sink
, "Content-Type", "text/html");
266 disorder_random_enabled(ds
->g
->client
, &random_enabled
);
267 disorder_enabled(ds
->g
->client
, &enabled
);
269 && ds
->g
->playing
->state
== playing_started
/* i.e. not paused */
270 && !disorder_length(ds
->g
->client
, ds
->g
->playing
->track
, &length
)
272 && ds
->g
->playing
->sofar
>= 0) {
273 /* Try to put the next refresh at the start of the next track. */
275 fin
= now
+ length
- ds
->g
->playing
->sofar
+ config
->gap
;
276 if(now
+ refresh
> fin
)
279 if(ds
->g
->queue
&& ds
->g
->queue
->state
== playing_isscratch
) {
280 /* next track is a scratch, don't leave more than the inter-track gap */
281 if(refresh
> config
->gap
)
282 refresh
= config
->gap
;
284 if(!ds
->g
->playing
&& ((ds
->g
->queue
285 && ds
->g
->queue
->state
!= playing_random
)
286 || random_enabled
) && enabled
) {
287 /* no track playing but playing is enabled and there is something coming
288 * up, must be in a gap */
289 if(refresh
> config
->gap
)
290 refresh
= config
->gap
;
292 byte_snprintf(r
, sizeof r
, "%ld;url=%s", refresh
> 0 ? refresh
: 1,
294 cgi_header(output
->sink
, "Refresh", r
);
295 header_cookie(output
->sink
);
296 cgi_body(output
->sink
);
297 expand(output
, "playing", ds
);
300 static void act_play(cgi_sink
*output
,
302 const char *track
, *dir
;
307 if((track
= cgi_get("file"))) {
308 disorder_play(ds
->g
->client
, track
);
309 } else if((dir
= cgi_get("directory"))) {
310 if(disorder_files(ds
->g
->client
, dir
, 0, &tracks
, &ntracks
)) ntracks
= 0;
312 e
= xmalloc(ntracks
* sizeof (struct entry
));
313 for(n
= 0; n
< ntracks
; ++n
) {
314 e
[n
].path
= tracks
[n
];
315 e
[n
].sort
= trackname_transform("track", tracks
[n
], "sort");
316 e
[n
].display
= trackname_transform("track", tracks
[n
], "display");
318 qsort(e
, ntracks
, sizeof (struct entry
), compare_entry
);
319 for(n
= 0; n
< ntracks
; ++n
)
320 disorder_play(ds
->g
->client
, e
[n
].path
);
323 /* XXX error handling */
324 redirect(output
->sink
);
327 static int clamp(int n
, int min
, int max
) {
335 static const char *volume_url(void) {
338 byte_xasprintf(&url
, "%s?action=volume", config
->url
);
342 static void act_volume(cgi_sink
*output
, dcgi_state
*ds
) {
343 const char *l
, *r
, *d
, *back
;
344 int nd
, changed
= 0;;
346 if((d
= cgi_get("delta"))) {
347 lookups(ds
, DC_VOLUME
);
348 nd
= clamp(atoi(d
), -255, 255);
349 disorder_set_volume(ds
->g
->client
,
350 clamp(ds
->g
->volume_left
+ nd
, 0, 255),
351 clamp(ds
->g
->volume_right
+ nd
, 0, 255));
353 } else if((l
= cgi_get("left")) && (r
= cgi_get("right"))) {
354 disorder_set_volume(ds
->g
->client
, atoi(l
), atoi(r
));
358 /* redirect back to ourselves (but without the volume-changing bits in the
360 cgi_header(output
->sink
, "Location",
361 (back
= cgi_get("back")) ? back
: volume_url());
362 header_cookie(output
->sink
);
363 cgi_body(output
->sink
);
365 cgi_header(output
->sink
, "Content-Type", "text/html");
366 header_cookie(output
->sink
);
367 cgi_body(output
->sink
);
368 expand(output
, "volume", ds
);
372 static void act_prefs_errors(const char *msg
,
373 void attribute((unused
)) *u
) {
374 fatal(0, "error splitting parts list: %s", msg
);
377 static const char *numbered_arg(const char *argname
, int numfile
) {
380 byte_xasprintf(&fullname
, "%d_%s", numfile
, argname
);
381 return cgi_get(fullname
);
384 static void process_prefs(dcgi_state
*ds
, int numfile
) {
385 const char *file
, *name
, *value
, *part
, *parts
, *current
, *context
;
388 if(!(file
= numbered_arg("file", numfile
)))
389 /* The first file doesn't need numbering. */
390 if(numfile
> 0 || !(file
= cgi_get("file")))
392 if((parts
= numbered_arg("parts", numfile
))
393 || (parts
= cgi_get("parts"))) {
394 /* Default context is display. Other contexts not actually tested. */
395 if(!(context
= numbered_arg("context", numfile
))) context
= "display";
396 partslist
= split(parts
, 0, 0, act_prefs_errors
, 0);
397 while((part
= *partslist
++)) {
398 if(!(value
= numbered_arg(part
, numfile
)))
400 /* If it's already right (whether regexps or db) don't change anything,
401 * so we don't fill the database up with rubbish. */
402 if(disorder_part(ds
->g
->client
, (char **)¤t
,
403 file
, context
, part
))
404 fatal(0, "disorder_part() failed");
405 if(!strcmp(current
, value
))
407 byte_xasprintf((char **)&name
, "trackname_%s_%s", context
, part
);
408 disorder_set(ds
->g
->client
, file
, name
, value
);
410 if((value
= numbered_arg("random", numfile
)))
411 disorder_unset(ds
->g
->client
, file
, "pick_at_random");
413 disorder_set(ds
->g
->client
, file
, "pick_at_random", "0");
414 if((value
= numbered_arg("tags", numfile
)))
415 disorder_set(ds
->g
->client
, file
, "tags", value
);
416 } else if((name
= cgi_get("name"))) {
417 /* Raw preferences. Not well supported in the templates at the moment. */
418 value
= cgi_get("value");
420 disorder_set(ds
->g
->client
, file
, name
, value
);
422 disorder_unset(ds
->g
->client
, file
, name
);
426 static void act_prefs(cgi_sink
*output
, dcgi_state
*ds
) {
430 if((files
= cgi_get("files"))) nfiles
= atoi(files
);
432 for(numfile
= 0; numfile
< nfiles
; ++numfile
)
433 process_prefs(ds
, numfile
);
434 cgi_header(output
->sink
, "Content-Type", "text/html");
435 header_cookie(output
->sink
);
436 cgi_body(output
->sink
);
437 expand(output
, "prefs", ds
);
440 static void act_pause(cgi_sink
*output
,
443 disorder_pause(ds
->g
->client
);
444 redirect(output
->sink
);
447 static void act_resume(cgi_sink
*output
,
450 disorder_resume(ds
->g
->client
);
451 redirect(output
->sink
);
454 static void act_login(cgi_sink
*output
,
456 const char *username
, *password
, *back
;
459 username
= cgi_get("username");
460 password
= cgi_get("password");
461 if(!username
|| !password
462 || !strcmp(username
, "guest")/*bodge to avoid guest cookies*/) {
463 /* We're just visiting the login page */
464 expand_template(ds
, output
, "login");
467 /* We'll need a new connection as we are going to stop being guest */
469 if(disorder_connect_user(c
, username
, password
)) {
470 cgi_set_option("error", "loginfailed");
471 expand_template(ds
, output
, "login");
474 if(disorder_make_cookie(c
, &login_cookie
)) {
475 cgi_set_option("error", "cookiefailed");
476 expand_template(ds
, output
, "login");
479 /* Use the new connection henceforth */
482 /* We have a new cookie */
483 header_cookie(output
->sink
);
484 cgi_set_option("status", "loginok");
485 if((back
= cgi_get("back")) && *back
)
486 /* Redirect back to somewhere or other */
487 redirect(output
->sink
);
489 /* Stick to the login page */
490 expand_template(ds
, output
, "login");
493 static void act_logout(cgi_sink
*output
,
495 disorder_revoke(ds
->g
->client
);
497 /* Reconnect as guest */
498 disorder_cgi_login(ds
, output
);
499 /* Back to the login page */
500 cgi_set_option("status", "logoutok");
501 expand_template(ds
, output
, "login");
504 static void act_register(cgi_sink
*output
,
506 const char *username
, *password
, *email
;
507 char *confirm
, *content_type
;
508 const char *text
, *encoding
, *charset
;
510 username
= cgi_get("username");
511 password
= cgi_get("password");
512 email
= cgi_get("email");
514 if(!username
|| !*username
) {
515 cgi_set_option("error", "nousername");
516 expand_template(ds
, output
, "login");
519 if(!password
|| !*password
) {
520 cgi_set_option("error", "nopassword");
521 expand_template(ds
, output
, "login");
524 if(!email
|| !*email
) {
525 cgi_set_option("error", "noemail");
526 expand_template(ds
, output
, "login");
529 /* We could well do better address validation but for now we'll just do the
531 if(!strchr(email
, '@')) {
532 cgi_set_option("error", "bademail");
533 expand_template(ds
, output
, "login");
536 if(disorder_register(ds
->g
->client
, username
, password
, email
, &confirm
)) {
537 cgi_set_option("error", "cannotregister");
538 expand_template(ds
, output
, "login");
541 /* Send the user a mail */
542 /* TODO templatize this */
543 byte_xasprintf((char **)&text
,
544 "Welcome to DisOrder. To active your login, please visit this URL:\n"
546 "%s?c=%s\n", config
->url
, urlencodestring(confirm
));
547 if(!(text
= mime_encode_text(text
, &charset
, &encoding
)))
548 fatal(0, "cannot encode email");
549 byte_xasprintf(&content_type
, "text/plain;charset=%s",
550 quote822(charset
, 0));
551 sendmail("", config
->mail_sender
, email
, "Welcome to DisOrder",
552 encoding
, content_type
, text
); /* TODO error checking */
553 /* We'll go back to the login page with a suitable message */
554 cgi_set_option("status", "registered");
555 expand_template(ds
, output
, "login");
558 static void act_confirm(cgi_sink
*output
,
560 const char *confirmation
;
562 if(!(confirmation
= cgi_get("c"))) {
563 cgi_set_option("error", "noconfirm");
564 expand_template(ds
, output
, "login");
566 /* Confirm our registration */
567 if(disorder_confirm(ds
->g
->client
, confirmation
)) {
568 cgi_set_option("error", "badconfirm");
569 expand_template(ds
, output
, "login");
572 if(disorder_make_cookie(ds
->g
->client
, &login_cookie
)) {
573 cgi_set_option("error", "cookiefailed");
574 expand_template(ds
, output
, "login");
577 /* Discard any cached data JIC */
579 /* We have a new cookie */
580 header_cookie(output
->sink
);
581 cgi_set_option("status", "confirmed");
582 expand_template(ds
, output
, "login");
585 static const struct action
{
587 void (*handler
)(cgi_sink
*output
, dcgi_state
*ds
);
589 { "confirm", act_confirm
},
590 { "disable", act_disable
},
591 { "enable", act_enable
},
592 { "login", act_login
},
593 { "logout", act_logout
},
594 { "move", act_move
},
595 { "pause", act_pause
},
596 { "play", act_play
},
597 { "playing", act_playing
},
598 { "prefs", act_prefs
},
599 { "random-disable", act_random_disable
},
600 { "random-enable", act_random_enable
},
601 { "register", act_register
},
602 { "remove", act_remove
},
603 { "resume", act_resume
},
604 { "scratch", act_scratch
},
605 { "volume", act_volume
},
608 /* expansions *****************************************************************/
610 static void exp_include(int attribute((unused
)) nargs
,
614 expand(output
, args
[0], u
);
617 static void exp_server_version(int attribute((unused
)) nargs
,
618 char attribute((unused
)) **args
,
625 if(disorder_version(ds
->g
->client
, (char **)&v
)) v
= "(cannot get version)";
627 v
= "(server not running)";
628 cgi_output(output
, "%s", v
);
631 static void exp_version(int attribute((unused
)) nargs
,
632 char attribute((unused
)) **args
,
634 void attribute((unused
)) *u
) {
635 cgi_output(output
, "%s", disorder_short_version_string
);
638 static void exp_nonce(int attribute((unused
)) nargs
,
639 char attribute((unused
)) **args
,
641 void attribute((unused
)) *u
) {
642 cgi_output(output
, "%s", nonce());
645 static void exp_label(int attribute((unused
)) nargs
,
648 void attribute((unused
)) *u
) {
649 cgi_output(output
, "%s", cgi_label(args
[0]));
652 struct trackinfo_state
{
654 const struct queue_entry
*q
;
659 static void exp_who(int attribute((unused
)) nargs
,
660 char attribute((unused
)) **args
,
665 if(ds
->track
&& ds
->track
->submitter
)
666 cgi_output(output
, "%s", ds
->track
->submitter
);
669 static void exp_length(int attribute((unused
)) nargs
,
670 char attribute((unused
)) **args
,
677 && (ds
->track
->state
== playing_started
678 || ds
->track
->state
== playing_paused
)
679 && ds
->track
->sofar
>= 0)
680 cgi_output(output
, "%ld:%02ld/",
681 ds
->track
->sofar
/ 60, ds
->track
->sofar
% 60);
684 disorder_length(ds
->g
->client
, ds
->track
->track
, &length
);
686 disorder_length(ds
->g
->client
, ds
->tracks
[0], &length
);
688 cgi_output(output
, "%ld:%02ld", length
/ 60, length
% 60);
690 sink_printf(output
->sink
, "%s", " ");
693 static void exp_when(int attribute((unused
)) nargs
,
694 char attribute((unused
)) **args
,
698 const struct tm
*w
= 0;
701 switch(ds
->track
->state
) {
702 case playing_isscratch
:
703 case playing_unplayed
:
705 if(ds
->track
->expected
)
706 w
= localtime(&ds
->track
->expected
);
709 case playing_no_player
:
711 case playing_scratched
:
712 case playing_started
:
714 case playing_quitting
:
715 if(ds
->track
->played
)
716 w
= localtime(&ds
->track
->played
);
720 cgi_output(output
, "%d:%02d", w
->tm_hour
, w
->tm_min
);
722 sink_printf(output
->sink
, " ");
725 static void exp_part(int nargs
,
730 const char *s
, *track
, *part
, *context
;
736 track
= ds
->track
->track
;
738 track
= ds
->tracks
[0];
756 if(disorder_part(ds
->g
->client
, (char **)&s
, track
,
757 !strcmp(context
, "short") ?
"display" : context
, part
))
758 fatal(0, "disorder_part() failed");
759 if(!strcmp(context
, "short"))
760 s
= truncate_for_display(s
, config
->short_display
);
761 cgi_output(output
, "%s", s
);
763 sink_printf(output
->sink
, " ");
766 static void exp_playing(int attribute((unused
)) nargs
,
773 lookups(ds
, DC_PLAYING
);
774 memset(&s
, 0, sizeof s
);
777 s
.track
= ds
->g
->playing
;
778 expandstring(output
, args
[0], &s
);
782 static void exp_queue(int attribute((unused
)) nargs
,
788 struct queue_entry
*q
;
790 lookups(ds
, DC_QUEUE
);
791 memset(&s
, 0, sizeof s
);
794 for(q
= ds
->g
->queue
; q
; q
= q
->next
) {
797 expandstring(output
, args
[0], &s
);
803 static void exp_recent(int attribute((unused
)) nargs
,
809 struct queue_entry
*q
;
811 lookups(ds
, DC_RECENT
);
812 memset(&s
, 0, sizeof s
);
815 for(q
= ds
->g
->recent
; q
; q
= q
->next
) {
818 expandstring(output
, args
[0], &s
);
824 static void exp_new(int attribute((unused
)) nargs
,
832 memset(&s
, 0, sizeof s
);
835 for(s
.index
= 0; s
.index
< ds
->g
->nnew
; ++s
.index
) {
836 s
.last
= s
.index
+ 1 < ds
->g
->nnew
;
837 s
.tracks
= &ds
->g
->new[s
.index
];
838 expandstring(output
, args
[0], &s
);
843 static void exp_url(int attribute((unused
)) nargs
,
844 char attribute((unused
)) **args
,
846 void attribute((unused
)) *u
) {
847 cgi_output(output
, "%s", config
->url
);
855 static int compare_result(const void *a
, const void *b
) {
856 const struct result
*ra
= a
, *rb
= b
;
859 if(!(c
= strcmp(ra
->sort
, rb
->sort
)))
860 c
= strcmp(ra
->track
, rb
->track
);
864 static void exp_search(int nargs
,
868 dcgi_state
*ds
= u
, substate
;
870 const char *q
, *context
, *part
, *template;
886 assert(!"should never happen");
887 part
= context
= template = 0; /* quieten compiler */
889 if(ds
->tracks
== 0) {
890 /* we are the top level, let's get some search results */
891 if(!(q
= cgi_get("query"))) return; /* no results yet */
892 if(disorder_search(ds
->g
->client
, q
, &tracks
, &ntracks
)) return;
896 ntracks
= ds
->ntracks
;
898 assert(ntracks
!= 0);
899 /* sort tracks by the appropriate part */
900 r
= xmalloc(ntracks
* sizeof *r
);
901 for(n
= 0; n
< ntracks
; ++n
) {
902 r
[n
].track
= tracks
[n
];
903 if(disorder_part(ds
->g
->client
, (char **)&r
[n
].sort
,
904 tracks
[n
], context
, part
))
905 fatal(0, "disorder_part() failed");
907 qsort(r
, ntracks
, sizeof (struct result
), compare_result
);
908 /* expand the 2nd arg once for each group. We re-use the passed-in tracks
909 * array as we know it's guaranteed to be big enough and isn't going to be
910 * used for anything else any more. */
911 memset(&substate
, 0, sizeof substate
);
916 substate
.tracks
= tracks
;
917 substate
.ntracks
= 0;
920 && !strcmp(r
[m
].sort
, r
[n
].sort
))
921 tracks
[substate
.ntracks
++] = r
[m
++].track
;
922 substate
.last
= (m
== ntracks
);
923 expandstring(output
, template, &substate
);
928 assert(substate
.last
!= 0);
931 static void exp_arg(int attribute((unused
)) nargs
,
934 void attribute((unused
)) *u
) {
937 if((v
= cgi_get(args
[0])))
938 cgi_output(output
, "%s", v
);
941 static void exp_stats(int attribute((unused
)) nargs
,
942 char attribute((unused
)) **args
,
948 cgi_opentag(output
->sink
, "pre", "class", "stats", (char *)0);
949 if(!disorder_stats(ds
->g
->client
, &v
, 0)) {
951 cgi_output(output
, "%s\n", *v
++);
953 cgi_closetag(output
->sink
, "pre");
956 static void exp_volume(int attribute((unused
)) nargs
,
962 lookups(ds
, DC_VOLUME
);
963 if(!strcmp(args
[0], "left"))
964 cgi_output(output
, "%d", ds
->g
->volume_left
);
966 cgi_output(output
, "%d", ds
->g
->volume_right
);
969 static void exp_shell(int attribute((unused
)) nargs
,
972 void attribute((unused
)) *u
) {
978 if(!(pid
= xfork())) {
983 execlp("sh", "sh", "-c", args
[0], (char *)0);
984 fatal(errno
, "error executing sh");
987 while((n
= read(p
[0], buffer
, sizeof buffer
))) {
989 if(errno
== EINTR
) continue;
990 else fatal(errno
, "error reading from pipe");
992 output
->sink
->write(output
->sink
, buffer
, n
);
995 while((n
= waitpid(pid
, &w
, 0)) < 0 && errno
== EINTR
)
997 if(n
< 0) fatal(errno
, "error calling waitpid");
999 error(0, "shell command '%s' %s", args
[0], wstat(w
));
1002 static inline int str2bool(const char *s
) {
1003 return !strcmp(s
, "true");
1006 static inline const char *bool2str(int n
) {
1007 return n ?
"true" : "false";
1010 static char *expandarg(const char *arg
, dcgi_state
*ds
) {
1016 output
.sink
= sink_dynstr(&d
);
1017 expandstring(&output
, arg
, ds
);
1018 dynstr_terminate(&d
);
1022 static void exp_prefs(int attribute((unused
)) nargs
,
1027 dcgi_state substate
;
1029 const char *file
= expandarg(args
[0], ds
);
1031 memset(&substate
, 0, sizeof substate
);
1034 if(disorder_prefs(ds
->g
->client
, file
, &k
)) return;
1036 substate
.last
= !k
->next
;
1038 expandstring(output
, args
[1], &substate
);
1045 static void exp_pref(int attribute((unused
)) nargs
,
1052 if(!disorder_get(ds
->g
->client
, args
[0], args
[1], &value
))
1053 cgi_output(output
, "%s", value
);
1056 static void exp_if(int nargs
,
1061 int n
= str2bool(expandarg(args
[0], ds
)) ?
1 : 2;
1064 expandstring(output
, args
[n
], ds
);
1067 static void exp_and(int nargs
,
1074 for(n
= 0; n
< nargs
; ++n
)
1075 if(!str2bool(expandarg(args
[n
], ds
))) {
1079 sink_printf(output
->sink
, "%s", bool2str(result
));
1082 static void exp_or(int nargs
,
1089 for(n
= 0; n
< nargs
; ++n
)
1090 if(str2bool(expandarg(args
[n
], ds
))) {
1094 sink_printf(output
->sink
, "%s", bool2str(result
));
1097 static void exp_not(int attribute((unused
)) nargs
,
1100 void attribute((unused
)) *u
) {
1101 sink_printf(output
->sink
, "%s", bool2str(!str2bool(args
[0])));
1104 static void exp_isplaying(int attribute((unused
)) nargs
,
1105 char attribute((unused
)) **args
,
1110 lookups(ds
, DC_PLAYING
);
1111 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->playing
));
1114 static void exp_isqueue(int attribute((unused
)) nargs
,
1115 char attribute((unused
)) **args
,
1120 lookups(ds
, DC_QUEUE
);
1121 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->queue
));
1124 static void exp_isrecent(int attribute((unused
)) nargs
,
1125 char attribute((unused
)) **args
,
1130 lookups(ds
, DC_RECENT
);
1131 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->recent
));
1134 static void exp_isnew(int attribute((unused
)) nargs
,
1135 char attribute((unused
)) **args
,
1140 lookups(ds
, DC_NEW
);
1141 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->nnew
));
1144 static void exp_id(int attribute((unused
)) nargs
,
1145 char attribute((unused
)) **args
,
1151 cgi_output(output
, "%s", ds
->track
->id
);
1154 static void exp_track(int attribute((unused
)) nargs
,
1155 char attribute((unused
)) **args
,
1161 cgi_output(output
, "%s", ds
->track
->track
);
1164 static void exp_parity(int attribute((unused
)) nargs
,
1165 char attribute((unused
)) **args
,
1170 cgi_output(output
, "%s", ds
->index
% 2 ?
"odd" : "even");
1173 static void exp_comment(int attribute((unused
)) nargs
,
1174 char attribute((unused
)) **args
,
1175 cgi_sink
attribute((unused
)) *output
,
1176 void attribute((unused
)) *u
) {
1180 static void exp_prefname(int attribute((unused
)) nargs
,
1181 char attribute((unused
)) **args
,
1186 if(ds
->pref
&& ds
->pref
->name
)
1187 cgi_output(output
, "%s", ds
->pref
->name
);
1190 static void exp_prefvalue(int attribute((unused
)) nargs
,
1191 char attribute((unused
)) **args
,
1196 if(ds
->pref
&& ds
->pref
->value
)
1197 cgi_output(output
, "%s", ds
->pref
->value
);
1200 static void exp_isfiles(int attribute((unused
)) nargs
,
1201 char attribute((unused
)) **args
,
1206 lookups(ds
, DC_FILES
);
1207 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->nfiles
));
1210 static void exp_isdirectories(int attribute((unused
)) nargs
,
1211 char attribute((unused
)) **args
,
1216 lookups(ds
, DC_DIRS
);
1217 sink_printf(output
->sink
, "%s", bool2str(!!ds
->g
->ndirs
));
1220 static void exp_choose(int attribute((unused
)) nargs
,
1225 dcgi_state substate
;
1229 const char *type
, *what
= expandarg(args
[0], ds
);
1231 if(!strcmp(what
, "files")) {
1232 lookups(ds
, DC_FILES
);
1233 files
= ds
->g
->files
;
1234 nfiles
= ds
->g
->nfiles
;
1236 } else if(!strcmp(what
, "directories")) {
1237 lookups(ds
, DC_DIRS
);
1238 files
= ds
->g
->dirs
;
1239 nfiles
= ds
->g
->ndirs
;
1242 error(0, "unknown @choose@ argument '%s'", what
);
1245 e
= xmalloc(nfiles
* sizeof (struct entry
));
1246 for(n
= 0; n
< nfiles
; ++n
) {
1247 e
[n
].path
= files
[n
];
1248 e
[n
].sort
= trackname_transform(type
, files
[n
], "sort");
1249 e
[n
].display
= trackname_transform(type
, files
[n
], "display");
1251 qsort(e
, nfiles
, sizeof (struct entry
), compare_entry
);
1252 memset(&substate
, 0, sizeof substate
);
1255 for(n
= 0; n
< nfiles
; ++n
) {
1256 substate
.last
= (n
== nfiles
- 1);
1258 substate
.entry
= &e
[n
];
1259 expandstring(output
, args
[1], &substate
);
1264 static void exp_file(int attribute((unused
)) nargs
,
1265 char attribute((unused
)) **args
,
1271 cgi_output(output
, "%s", ds
->entry
->path
);
1273 cgi_output(output
, "%s", ds
->track
->track
);
1275 cgi_output(output
, "%s", ds
->tracks
[0]);
1278 static void exp_transform(int nargs
,
1281 void attribute((unused
)) *u
) {
1282 const char *context
= nargs
> 2 ? args
[2] : "display";
1284 cgi_output(output
, "%s", trackname_transform(args
[1], args
[0], context
));
1287 static void exp_urlquote(int attribute((unused
)) nargs
,
1290 void attribute((unused
)) *u
) {
1291 cgi_output(output
, "%s", urlencodestring(args
[0]));
1294 static void exp_scratchable(int attribute((unused
)) nargs
,
1295 char attribute((unused
)) **args
,
1297 void attribute((unused
)) *u
) {
1300 lookups(ds
, DC_PLAYING
|DC_RIGHTS
);
1301 sink_printf(output
->sink
, "%s",
1302 bool2str(right_scratchable(ds
->g
->rights
,
1303 disorder_user(ds
->g
->client
),
1307 static void exp_removable(int attribute((unused
)) nargs
,
1308 char attribute((unused
)) **args
,
1310 void attribute((unused
)) *u
) {
1313 lookups(ds
, DC_RIGHTS
);
1314 sink_printf(output
->sink
, "%s",
1315 bool2str(right_removable(ds
->g
->rights
,
1316 disorder_user(ds
->g
->client
),
1320 static void exp_movable(int attribute((unused
)) nargs
,
1321 char attribute((unused
)) **args
,
1323 void attribute((unused
)) *u
) {
1326 lookups(ds
, DC_RIGHTS
);
1327 sink_printf(output
->sink
, "%s",
1328 bool2str(right_movable(ds
->g
->rights
,
1329 disorder_user(ds
->g
->client
),
1333 static void exp_navigate(int attribute((unused
)) nargs
,
1338 dcgi_state substate
;
1339 const char *path
= expandarg(args
[0], ds
);
1344 memset(&substate
, 0, sizeof substate
);
1346 ptr
= path
+ 1; /* skip root */
1348 substate
.nav_path
= path
;
1351 while(*ptr
&& *ptr
!= '/')
1353 substate
.last
= !*ptr
;
1354 substate
.nav_len
= ptr
- path
;
1355 substate
.nav_dirlen
= dirlen
;
1356 expandstring(output
, args
[1], &substate
);
1357 dirlen
= substate
.nav_len
;
1364 static void exp_fullname(int attribute((unused
)) nargs
,
1365 char attribute((unused
)) **args
,
1369 cgi_output(output
, "%.*s", ds
->nav_len
, ds
->nav_path
);
1372 static void exp_basename(int nargs
,
1380 if((s
= strrchr(args
[0], '/'))) ++s
;
1382 cgi_output(output
, "%s", s
);
1384 cgi_output(output
, "%.*s", ds
->nav_len
- ds
->nav_dirlen
- 1,
1385 ds
->nav_path
+ ds
->nav_dirlen
+ 1);
1388 static void exp_dirname(int nargs
,
1396 if((s
= strrchr(args
[0], '/')))
1397 cgi_output(output
, "%.*s", (int)(s
- args
[0]), args
[0]);
1399 cgi_output(output
, "%.*s", ds
->nav_dirlen
, ds
->nav_path
);
1402 static void exp_eq(int attribute((unused
)) nargs
,
1405 void attribute((unused
)) *u
) {
1406 cgi_output(output
, "%s", bool2str(!strcmp(args
[0], args
[1])));
1409 static void exp_ne(int attribute((unused
)) nargs
,
1412 void attribute((unused
)) *u
) {
1413 cgi_output(output
, "%s", bool2str(strcmp(args
[0], args
[1])));
1416 static void exp_enabled(int attribute((unused
)) nargs
,
1417 char attribute((unused
)) **args
,
1424 disorder_enabled(ds
->g
->client
, &enabled
);
1425 cgi_output(output
, "%s", bool2str(enabled
));
1428 static void exp_random_enabled(int attribute((unused
)) nargs
,
1429 char attribute((unused
)) **args
,
1436 disorder_random_enabled(ds
->g
->client
, &enabled
);
1437 cgi_output(output
, "%s", bool2str(enabled
));
1440 static void exp_trackstate(int attribute((unused
)) nargs
,
1445 struct queue_entry
*q
;
1448 if(disorder_resolve(ds
->g
->client
, &track
, args
[0])) return;
1449 lookups(ds
, DC_QUEUE
|DC_PLAYING
);
1450 if(ds
->g
->playing
&& !strcmp(ds
->g
->playing
->track
, track
))
1451 cgi_output(output
, "playing");
1453 for(q
= ds
->g
->queue
; q
&& strcmp(q
->track
, track
); q
= q
->next
)
1456 cgi_output(output
, "queued");
1460 static void exp_thisurl(int attribute((unused
)) nargs
,
1461 char attribute((unused
)) **args
,
1463 void attribute((unused
)) *u
) {
1464 kvp_set(&cgi_args
, "nonce", nonce()); /* nonces had better differ! */
1465 cgi_output(output
, "%s?%s", config
->url
, kvp_urlencode(cgi_args
, 0));
1468 static void exp_isfirst(int attribute((unused
)) nargs
,
1469 char attribute((unused
)) **args
,
1474 sink_printf(output
->sink
, "%s", bool2str(!!ds
->first
));
1477 static void exp_islast(int attribute((unused
)) nargs
,
1478 char attribute((unused
)) **args
,
1483 sink_printf(output
->sink
, "%s", bool2str(!!ds
->last
));
1486 static void exp_action(int attribute((unused
)) nargs
,
1487 char attribute((unused
)) **args
,
1489 void attribute((unused
)) *u
) {
1490 const char *action
= cgi_get("action"), *mgmt
;
1492 if(!action
) action
= "playing";
1493 if(!strcmp(action
, "playing")
1494 && (mgmt
= cgi_get("mgmt"))
1495 && !strcmp(mgmt
, "true"))
1497 sink_printf(output
->sink
, "%s", action
);
1500 static void exp_resolve(int attribute((unused
)) nargs
,
1503 void attribute((unused
)) *u
) {
1507 if(!disorder_resolve(ds
->g
->client
, &track
, args
[0]))
1508 sink_printf(output
->sink
, "%s", track
);
1511 static void exp_paused(int attribute((unused
)) nargs
,
1512 char attribute((unused
)) **args
,
1518 lookups(ds
, DC_PLAYING
);
1519 if(ds
->g
->playing
&& ds
->g
->playing
->state
== playing_paused
)
1521 cgi_output(output
, "%s", bool2str(paused
));
1524 static void exp_state(int attribute((unused
)) nargs
,
1525 char attribute((unused
)) **args
,
1531 cgi_output(output
, "%s", playing_states
[ds
->track
->state
]);
1534 static void exp_files(int attribute((unused
)) nargs
,
1539 dcgi_state substate
;
1540 const char *nfiles_arg
, *directory
;
1541 int nfiles
, numfile
;
1544 memset(&substate
, 0, sizeof substate
);
1546 if((directory
= cgi_get("directory"))) {
1547 /* Prefs for whole directory. */
1548 lookups(ds
, DC_FILES
);
1549 /* Synthesize args for the file list. */
1550 nfiles
= ds
->g
->nfiles
;
1551 for(numfile
= 0; numfile
< nfiles
; ++numfile
) {
1552 k
= xmalloc(sizeof *k
);
1553 byte_xasprintf((char **)&k
->name
, "%d_file", numfile
);
1554 k
->value
= ds
->g
->files
[numfile
];
1559 /* Args already present. */
1560 if((nfiles_arg
= cgi_get("files"))) nfiles
= atoi(nfiles_arg
);
1563 for(numfile
= 0; numfile
< nfiles
; ++numfile
) {
1564 substate
.index
= numfile
;
1565 expandstring(output
, args
[0], &substate
);
1569 static void exp_index(int attribute((unused
)) nargs
,
1570 char attribute((unused
)) **args
,
1575 cgi_output(output
, "%d", ds
->index
);
1578 static void exp_nfiles(int attribute((unused
)) nargs
,
1579 char attribute((unused
)) **args
,
1583 const char *files_arg
;
1585 if(cgi_get("directory")) {
1586 lookups(ds
, DC_FILES
);
1587 cgi_output(output
, "%d", ds
->g
->nfiles
);
1588 } else if((files_arg
= cgi_get("files")))
1589 cgi_output(output
, "%s", files_arg
);
1591 cgi_output(output
, "1");
1594 static void exp_user(int attribute((unused
)) nargs
,
1595 char attribute((unused
)) **args
,
1598 dcgi_state
*const ds
= u
;
1600 cgi_output(output
, "%s", disorder_user(ds
->g
->client
));
1603 static void exp_right(int attribute((unused
)) nargs
,
1607 dcgi_state
*const ds
= u
;
1608 const char *right
= expandarg(args
[0], ds
);
1611 lookups(ds
, DC_RIGHTS
);
1612 if(parse_rights(right
, &r
, 1/*report*/))
1615 cgi_output(output
, "%s", bool2str(!!(r
& ds
->g
->rights
)));
1616 else if(r
& ds
->g
->rights
)
1617 expandstring(output
, args
[1], ds
);
1619 expandstring(output
, args
[2], ds
);
1622 static const struct cgi_expansion expansions
[] = {
1623 { "#", 0, INT_MAX
, EXP_MAGIC
, exp_comment
},
1624 { "action", 0, 0, 0, exp_action
},
1625 { "and", 0, INT_MAX
, EXP_MAGIC
, exp_and
},
1626 { "arg", 1, 1, 0, exp_arg
},
1627 { "basename", 0, 1, 0, exp_basename
},
1628 { "choose", 2, 2, EXP_MAGIC
, exp_choose
},
1629 { "dirname", 0, 1, 0, exp_dirname
},
1630 { "enabled", 0, 0, 0, exp_enabled
},
1631 { "eq", 2, 2, 0, exp_eq
},
1632 { "file", 0, 0, 0, exp_file
},
1633 { "files", 1, 1, EXP_MAGIC
, exp_files
},
1634 { "fullname", 0, 0, 0, exp_fullname
},
1635 { "id", 0, 0, 0, exp_id
},
1636 { "if", 2, 3, EXP_MAGIC
, exp_if
},
1637 { "include", 1, 1, 0, exp_include
},
1638 { "index", 0, 0, 0, exp_index
},
1639 { "isdirectories", 0, 0, 0, exp_isdirectories
},
1640 { "isfiles", 0, 0, 0, exp_isfiles
},
1641 { "isfirst", 0, 0, 0, exp_isfirst
},
1642 { "islast", 0, 0, 0, exp_islast
},
1643 { "isnew", 0, 0, 0, exp_isnew
},
1644 { "isplaying", 0, 0, 0, exp_isplaying
},
1645 { "isqueue", 0, 0, 0, exp_isqueue
},
1646 { "isrecent", 0, 0, 0, exp_isrecent
},
1647 { "label", 1, 1, 0, exp_label
},
1648 { "length", 0, 0, 0, exp_length
},
1649 { "movable", 0, 0, 0, exp_movable
},
1650 { "navigate", 2, 2, EXP_MAGIC
, exp_navigate
},
1651 { "ne", 2, 2, 0, exp_ne
},
1652 { "new", 1, 1, EXP_MAGIC
, exp_new
},
1653 { "nfiles", 0, 0, 0, exp_nfiles
},
1654 { "nonce", 0, 0, 0, exp_nonce
},
1655 { "not", 1, 1, 0, exp_not
},
1656 { "or", 0, INT_MAX
, EXP_MAGIC
, exp_or
},
1657 { "parity", 0, 0, 0, exp_parity
},
1658 { "part", 1, 3, 0, exp_part
},
1659 { "paused", 0, 0, 0, exp_paused
},
1660 { "playing", 1, 1, EXP_MAGIC
, exp_playing
},
1661 { "pref", 2, 2, 0, exp_pref
},
1662 { "prefname", 0, 0, 0, exp_prefname
},
1663 { "prefs", 2, 2, EXP_MAGIC
, exp_prefs
},
1664 { "prefvalue", 0, 0, 0, exp_prefvalue
},
1665 { "queue", 1, 1, EXP_MAGIC
, exp_queue
},
1666 { "random-enabled", 0, 0, 0, exp_random_enabled
},
1667 { "recent", 1, 1, EXP_MAGIC
, exp_recent
},
1668 { "removable", 0, 0, 0, exp_removable
},
1669 { "resolve", 1, 1, 0, exp_resolve
},
1670 { "right", 1, 3, EXP_MAGIC
, exp_right
},
1671 { "scratchable", 0, 0, 0, exp_scratchable
},
1672 { "search", 2, 3, EXP_MAGIC
, exp_search
},
1673 { "server-version", 0, 0, 0, exp_server_version
},
1674 { "shell", 1, 1, 0, exp_shell
},
1675 { "state", 0, 0, 0, exp_state
},
1676 { "stats", 0, 0, 0, exp_stats
},
1677 { "thisurl", 0, 0, 0, exp_thisurl
},
1678 { "track", 0, 0, 0, exp_track
},
1679 { "trackstate", 1, 1, 0, exp_trackstate
},
1680 { "transform", 2, 3, 0, exp_transform
},
1681 { "url", 0, 0, 0, exp_url
},
1682 { "urlquote", 1, 1, 0, exp_urlquote
},
1683 { "user", 0, 0, 0, exp_user
},
1684 { "version", 0, 0, 0, exp_version
},
1685 { "volume", 1, 1, 0, exp_volume
},
1686 { "when", 0, 0, 0, exp_when
},
1687 { "who", 0, 0, 0, exp_who
}
1690 static void expand(cgi_sink
*output
,
1691 const char *template,
1693 cgi_expand(template,
1694 expansions
, sizeof expansions
/ sizeof *expansions
,
1699 static void expandstring(cgi_sink
*output
,
1702 cgi_expand_string("",
1704 expansions
, sizeof expansions
/ sizeof *expansions
,
1709 static void perform_action(cgi_sink
*output
, dcgi_state
*ds
,
1710 const char *action
) {
1713 /* We don't ever want anything to be cached */
1714 cgi_header(output
->sink
, "Cache-Control", "no-cache");
1715 if((n
= TABLE_FIND(actions
, struct action
, name
, action
)) >= 0)
1716 actions
[n
].handler(output
, ds
);
1718 expand_template(ds
, output
, action
);
1721 void disorder_cgi(cgi_sink
*output
, dcgi_state
*ds
) {
1722 const char *action
= cgi_get("action");
1725 /* We allow URLs which are just confirm=... in order to keep confirmation
1726 * URLs, which are user-facing, as short as possible. */
1732 perform_action(output
, ds
, action
);
1735 void disorder_cgi_error(cgi_sink
*output
, dcgi_state
*ds
,
1737 cgi_set_option("error", msg
);
1738 perform_action(output
, ds
, "error");
1741 /** @brief Log in as the current user or guest if none */
1742 void disorder_cgi_login(dcgi_state
*ds
, cgi_sink
*output
) {
1743 /* Create a new connection */
1744 ds
->g
->client
= disorder_new(0);
1745 /* Forget everything we knew */
1748 if(disorder_connect_cookie(ds
->g
->client
, login_cookie
)) {
1749 disorder_cgi_error(output
, ds
, "connect");
1752 /* If there was a cookie but it went bad, we forget it */
1753 if(login_cookie
&& !strcmp(disorder_user(ds
->g
->client
), "guest"))