chiark / gitweb /
c38ee6c1e383d77fd76752434670d13cde8cc09f
[disorder] / server / dcgi.c
1
2 #include <stdio.h>
3 #include <errno.h>
4 #include <sys/types.h>
5 #include <sys/socket.h>
6 #include <stddef.h>
7 #include <stdlib.h>
8 #include <time.h>
9 #include <unistd.h>
10 #include <string.h>
11 #include <sys/wait.h>
12 #include <pcre.h>
13 #include <assert.h>
14
15 #include "client.h"
16 #include "mem.h"
17 #include "vector.h"
18 #include "sink.h"
19 #include "server-cgi.h"
20 #include "log.h"
21 #include "configuration.h"
22 #include "table.h"
23 #include "queue.h"
24 #include "plugin.h"
25 #include "split.h"
26 #include "wstat.h"
27 #include "kvp.h"
28 #include "syscalls.h"
29 #include "printf.h"
30 #include "regsub.h"
31 #include "defs.h"
32 #include "trackname.h"
33 #include "charset.h"
34 #include "dcgi.h"
35 #include "url.h"
36 #include "mime.h"
37 #include "sendmail.h"
38 #include "base64.h"
39
40 struct entry {
41   const char *path;
42   const char *sort;
43   const char *display;
44 };
45
46 static int compare_entry(const void *a, const void *b) {
47   const struct entry *ea = a, *eb = b;
48
49   return compare_tracks(ea->sort, eb->sort,
50                         ea->display, eb->display,
51                         ea->path, eb->path);
52 }
53
54 static const char *front_url(void) {
55   char *url;
56   const char *mgmt;
57
58   /* preserve management interface visibility */
59   if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
60     byte_xasprintf(&url, "%s?mgmt=true", config->url);
61     return url;
62   }
63   return config->url;
64 }
65
66 static void redirect(struct sink *output) {
67   const char *back;
68
69   back = cgi_get("back");
70   cgi_header(output, "Location", back && *back ? back : front_url());
71   header_cookie(output);
72   cgi_body(output);
73 }
74
75 static void expand_template(dcgi_state *ds, cgi_sink *output,
76                             const char *action) {
77   cgi_header(output->sink, "Content-Type", "text/html");
78   header_cookie(output->sink);
79   cgi_body(output->sink);
80   expand(output, action, ds);
81 }
82
83 /* actions ********************************************************************/
84
85 static void act_prefs_errors(const char *msg,
86                              void attribute((unused)) *u) {
87   fatal(0, "error splitting parts list: %s", msg);
88 }
89
90 static const char *numbered_arg(const char *argname, int numfile) {
91   char *fullname;
92
93   byte_xasprintf(&fullname, "%d_%s", numfile, argname);
94   return cgi_get(fullname);
95 }
96
97 static void process_prefs(dcgi_state *ds, int numfile) {
98   const char *file, *name, *value, *part, *parts, *current, *context;
99   char **partslist;
100
101   if(!(file = numbered_arg("file", numfile)))
102     /* The first file doesn't need numbering. */
103     if(numfile > 0 || !(file = cgi_get("file")))
104       return;
105   if((parts = numbered_arg("parts", numfile))
106      || (parts = cgi_get("parts"))) {
107     /* Default context is display.  Other contexts not actually tested. */
108     if(!(context = numbered_arg("context", numfile))) context = "display";
109     partslist = split(parts, 0, 0, act_prefs_errors, 0);
110     while((part = *partslist++)) {
111       if(!(value = numbered_arg(part, numfile)))
112         continue;
113       /* If it's already right (whether regexps or db) don't change anything,
114        * so we don't fill the database up with rubbish. */
115       if(disorder_part(ds->g->client, (char **)&current,
116                        file, context, part))
117         fatal(0, "disorder_part() failed");
118       if(!strcmp(current, value))
119         continue;
120       byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
121       disorder_set(ds->g->client, file, name, value);
122     }
123     if((value = numbered_arg("random", numfile)))
124       disorder_unset(ds->g->client, file, "pick_at_random");
125     else
126       disorder_set(ds->g->client, file, "pick_at_random", "0");
127     if((value = numbered_arg("tags", numfile))) {
128       if(!*value)
129         disorder_unset(ds->g->client, file, "tags");
130       else
131         disorder_set(ds->g->client, file, "tags", value);
132     }
133     if((value = numbered_arg("weight", numfile))) {
134       if(!*value || !strcmp(value, "90000"))
135         disorder_unset(ds->g->client, file, "weight");
136       else
137         disorder_set(ds->g->client, file, "weight", value);
138     }
139   } else if((name = cgi_get("name"))) {
140     /* Raw preferences.  Not well supported in the templates at the moment. */
141     value = cgi_get("value");
142     if(value)
143       disorder_set(ds->g->client, file, name, value);
144     else
145       disorder_unset(ds->g->client, file, name);
146   }
147 }
148
149 static void act_prefs(cgi_sink *output, dcgi_state *ds) {
150   const char *files;
151   int nfiles, numfile;
152
153   if((files = cgi_get("files"))) nfiles = atoi(files);
154   else nfiles = 1;
155   for(numfile = 0; numfile < nfiles; ++numfile)
156     process_prefs(ds, numfile);
157   cgi_header(output->sink, "Content-Type", "text/html");
158   header_cookie(output->sink);
159   cgi_body(output->sink);
160   expand(output, "prefs", ds);
161 }
162
163 static void act_login(cgi_sink *output,
164                       dcgi_state *ds) {
165   const char *username, *password, *back;
166   disorder_client *c;
167
168   username = cgi_get("username");
169   password = cgi_get("password");
170   if(!username || !password
171      || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
172     /* We're just visiting the login page */
173     expand_template(ds, output, "login");
174     return;
175   }
176   /* We'll need a new connection as we are going to stop being guest */
177   c = disorder_new(0);
178   if(disorder_connect_user(c, username, password)) {
179     cgi_set_option("error", "loginfailed");
180     expand_template(ds, output, "login");
181     return;
182   }
183   if(disorder_make_cookie(c, &login_cookie)) {
184     cgi_set_option("error", "cookiefailed");
185     expand_template(ds, output, "login");
186     return;
187   }
188   /* Use the new connection henceforth */
189   ds->g->client = c;
190   ds->g->flags = 0;
191   /* We have a new cookie */
192   header_cookie(output->sink);
193   cgi_set_option("status", "loginok");
194   if((back = cgi_get("back")) && *back)
195     /* Redirect back to somewhere or other */
196     redirect(output->sink);
197   else
198     /* Stick to the login page */
199     expand_template(ds, output, "login");
200 }
201
202 static void act_logout(cgi_sink *output,
203                        dcgi_state *ds) {
204   disorder_revoke(ds->g->client);
205   login_cookie = 0;
206   /* Reconnect as guest */
207   disorder_cgi_login(ds, output);
208   /* Back to the login page */
209   cgi_set_option("status", "logoutok");
210   expand_template(ds, output, "login");
211 }
212
213 static void act_register(cgi_sink *output,
214                          dcgi_state *ds) {
215   const char *username, *password, *password2, *email;
216   char *confirm, *content_type;
217   const char *text, *encoding, *charset;
218
219   username = cgi_get("username");
220   password = cgi_get("password1");
221   password2 = cgi_get("password2");
222   email = cgi_get("email");
223
224   if(!username || !*username) {
225     cgi_set_option("error", "nousername");
226     expand_template(ds, output, "login");
227     return;
228   }
229   if(!password || !*password) {
230     cgi_set_option("error", "nopassword");
231     expand_template(ds, output, "login");
232     return;
233   }
234   if(!password2 || !*password2 || strcmp(password, password2)) {
235     cgi_set_option("error", "passwordmismatch");
236     expand_template(ds, output, "login");
237     return;
238   }
239   if(!email || !*email) {
240     cgi_set_option("error", "noemail");
241     expand_template(ds, output, "login");
242     return;
243   }
244   /* We could well do better address validation but for now we'll just do the
245    * minimum */
246   if(!strchr(email, '@')) {
247     cgi_set_option("error", "bademail");
248     expand_template(ds, output, "login");
249     return;
250   }
251   if(disorder_register(ds->g->client, username, password, email, &confirm)) {
252     cgi_set_option("error", "cannotregister");
253     expand_template(ds, output, "login");
254     return;
255   }
256   /* Send the user a mail */
257   /* TODO templatize this */
258   byte_xasprintf((char **)&text,
259                  "Welcome to DisOrder.  To active your login, please visit this URL:\n"
260                  "\n"
261                  "%s?c=%s\n", config->url, urlencodestring(confirm));
262   if(!(text = mime_encode_text(text, &charset, &encoding)))
263     fatal(0, "cannot encode email");
264   byte_xasprintf(&content_type, "text/plain;charset=%s",
265                  quote822(charset, 0));
266   sendmail("", config->mail_sender, email, "Welcome to DisOrder",
267            encoding, content_type, text); /* TODO error checking  */
268   /* We'll go back to the login page with a suitable message */
269   cgi_set_option("status", "registered");
270   expand_template(ds, output, "login");
271 }
272
273 static void act_confirm(cgi_sink *output,
274                         dcgi_state *ds) {
275   const char *confirmation;
276
277   if(!(confirmation = cgi_get("c"))) {
278     cgi_set_option("error", "noconfirm");
279     expand_template(ds, output, "login");
280   }
281   /* Confirm our registration */
282   if(disorder_confirm(ds->g->client, confirmation)) {
283     cgi_set_option("error", "badconfirm");
284     expand_template(ds, output, "login");
285   }
286   /* Get a cookie */
287   if(disorder_make_cookie(ds->g->client, &login_cookie)) {
288     cgi_set_option("error", "cookiefailed");
289     expand_template(ds, output, "login");
290     return;
291   }
292   /* Discard any cached data JIC */
293   ds->g->flags = 0;
294   /* We have a new cookie */
295   header_cookie(output->sink);
296   cgi_set_option("status", "confirmed");
297   expand_template(ds, output, "login");
298 }
299
300 static void act_edituser(cgi_sink *output,
301                          dcgi_state *ds) {
302   const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
303   const char *password2 = cgi_get("changepassword2");
304   int newpassword = 0;
305   disorder_client *c;
306
307   if((password && *password) || (password && *password2)) {
308     if(!password || !password2 || strcmp(password, password2)) {
309       cgi_set_option("error", "passwordmismatch");
310       expand_template(ds, output, "login");
311       return;
312     }
313   } else
314     password = password2 = 0;
315   
316   if(email) {
317     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
318                          "email", email)) {
319       cgi_set_option("error", "badedit");
320       expand_template(ds, output, "login");
321       return;
322     }
323   }
324   if(password) {
325     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
326                          "password", password)) {
327       cgi_set_option("error", "badedit");
328       expand_template(ds, output, "login");
329       return;
330     }
331     newpassword = 1;
332   }
333   if(newpassword) {
334     login_cookie = 0;                   /* it'll be invalid now */
335     /* This is a bit duplicative of act_login() */
336     c = disorder_new(0);
337     if(disorder_connect_user(c, disorder_user(ds->g->client), password)) {
338       cgi_set_option("error", "loginfailed");
339       expand_template(ds, output, "login");
340       return;
341     }
342     if(disorder_make_cookie(c, &login_cookie)) {
343       cgi_set_option("error", "cookiefailed");
344       expand_template(ds, output, "login");
345       return;
346     }
347     /* Use the new connection henceforth */
348     ds->g->client = c;
349     ds->g->flags = 0;
350     /* We have a new cookie */
351     header_cookie(output->sink);
352   }
353   cgi_set_option("status", "edited");
354   expand_template(ds, output, "login");  
355 }
356
357 static void act_reminder(cgi_sink *output,
358                          dcgi_state *ds) {
359   const char *const username = cgi_get("username");
360
361   if(!username || !*username) {
362     cgi_set_option("error", "nousername");
363     expand_template(ds, output, "login");
364     return;
365   }
366   if(disorder_reminder(ds->g->client, username)) {
367     cgi_set_option("error", "reminderfailed");
368     expand_template(ds, output, "login");
369     return;
370   }
371   cgi_set_option("status", "reminded");
372   expand_template(ds, output, "login");  
373 }
374
375 /* expansions *****************************************************************/
376
377 static void exp_label(int attribute((unused)) nargs,
378                       char **args,
379                       cgi_sink *output,
380                       void attribute((unused)) *u) {
381   cgi_output(output, "%s", cgi_label(args[0]));
382 }
383
384 struct trackinfo_state {
385   dcgi_state *ds;
386   const struct queue_entry *q;
387   long length;
388   time_t when;
389 };
390
391 struct result {
392   char *track;
393   const char *sort;
394 };
395
396 static int compare_result(const void *a, const void *b) {
397   const struct result *ra = a, *rb = b;
398   int c;
399
400   if(!(c = strcmp(ra->sort, rb->sort)))
401     c = strcmp(ra->track, rb->track);
402   return c;
403 }
404
405 static void exp_stats(int attribute((unused)) nargs,
406                       char attribute((unused)) **args,
407                       cgi_sink *output,
408                       void *u) {
409   dcgi_state *ds = u;
410   char **v;
411
412   cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
413   if(!disorder_stats(ds->g->client, &v, 0)) {
414     while(*v)
415       cgi_output(output, "%s\n", *v++);
416   }
417   cgi_closetag(output->sink, "pre");
418 }
419
420 static char *expandarg(const char *arg, dcgi_state *ds) {
421   struct dynstr d;
422   cgi_sink output;
423
424   dynstr_init(&d);
425   output.quote = 0;
426   output.sink = sink_dynstr(&d);
427   expandstring(&output, arg, ds);
428   dynstr_terminate(&d);
429   return d.vec;
430 }
431
432 static void exp_navigate(int attribute((unused)) nargs,
433                          char **args,
434                          cgi_sink *output,
435                          void *u) {
436   dcgi_state *ds = u;
437   dcgi_state substate;
438   const char *path = expandarg(args[0], ds);
439   const char *ptr;
440   int dirlen;
441
442   if(*path) {
443     memset(&substate, 0, sizeof substate);
444     substate.g = ds->g;
445     ptr = path + 1;                     /* skip root */
446     dirlen = 0;
447     substate.nav_path = path;
448     substate.first = 1;
449     while(*ptr) {
450       while(*ptr && *ptr != '/')
451         ++ptr;
452       substate.last = !*ptr;
453       substate.nav_len = ptr - path;
454       substate.nav_dirlen = dirlen;
455       expandstring(output, args[1], &substate);
456       dirlen = substate.nav_len;
457       if(*ptr) ++ptr;
458       substate.first = 0;
459     }
460   }
461 }
462
463 static void exp_fullname(int attribute((unused)) nargs,
464                          char attribute((unused)) **args,
465                          cgi_sink *output,
466                          void *u) {
467   dcgi_state *ds = u;
468   cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
469 }
470
471 /*
472 Local Variables:
473 c-basic-offset:2
474 comment-column:40
475 fill-column:79
476 End:
477 */