mod_musicindex  1.4.1
html.c
Go to the documentation of this file.
1 /*
2  * html.c
3  * mod_musicindex
4  *
5  * $Id: html.c 1005 2012-07-31 12:54:59Z varenet $
6  *
7  * Created by Thibaut VARENE on Thu Mar 20 2003.
8  * Copyright (c) 2003-2006 Regis BOUDIN
9  * Copyright (c) 2003-2012 Thibaut VARENE
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License version 2.1,
13  * as published by the Free Software Foundation.
14  *
15  */
16 
37 #include "html.h"
38 #include "playlist.h"
39 
40 #include <http_core.h>
41 
42 #ifdef TM_IN_SYS_TIME
43 #include <sys/time.h>
44 #else
45 #include <time.h> /* For podcast informations */
46 #endif
47 #ifdef HAVE_UNISTD_H
48 #include <unistd.h>
49 #endif
50 #ifdef HAVE_SYS_TYPES_H
51 #include <sys/types.h>
52 #endif
53 #ifdef HAVE_DIRENT_H
54 #include <dirent.h> /* opendir */
55 #endif
56 #ifdef HAVE_SETLOCALE
57 #include <locale.h>
58 #endif
59 
60 static const char const Gfavicon[] = "sound.png";
61 static const char const Gcd_icon[] = "general.png";
62 static const char const Gorigcss[] = "musicindex.css";
64 static const char *const Gcovericns[] = {
65  "cover.jpg",
66  "cover.png",
67  "cover.gif",
68  "folder.jpg",
69  "folder.png",
70  "folder.gif",
71  NULL
72 };
73 
92 static unsigned short
93 list_songs(request_rec *r, const mu_pack *const pack, const int customlist)
94 {
95  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
96  const mu_ent *q;
97  register unsigned short fld = 0, j = 0;
98  const char * restrict current = NULL, * restrict new = NULL;
99  char * restrict codeduri = NULL;
100  unsigned long tlength = 0, tsize = pack->fsize;
101 #ifdef HAVE_SETLOCALE
102  struct lconv *clconv = localeconv();
103 #endif
104 
105  ap_rputs(" <tr class=\"title\">\n", r);
106 
107  if ( (customlist != 0) || (conf->options & (MI_ALLOWDWNLD | MI_ALLOWSTREAM | MI_ALLOWTARBALL)) ||
108  ((conf->search) && (conf->options & MI_RECURSIVE)))
109  ap_rvputs(r, " <th class=\"Select\">", _("Select"), "</th>\n", NULL);
110 
111  /* The sort option is disabled for search result, and this
112  implementation handles consecutive sort requests. TODO: keep
113  memory of sort configuration by the user (either on a per-dir basis,
114  or globally), probably using cookie(s) */
115 
116  for (fld = 0; conf->fields[fld]; fld++) { /* Display title line */
117  switch (conf->fields[fld]) {
118  case SB_TITLE:
119  current = "Title"; /* the style class */
120  new = _("Title"); /* the field name */
121  break;
122  case SB_TRACK:
123  current = "Track";
124  new = _("Track");
125  break;
126  case SB_POSN:
127  current = "Disc";
128  new = _("Disc");
129  break;
130  case SB_ARTIST:
131  current = "Artist";
132  new = _("Artist");
133  break;
134  case SB_LENGTH:
135  current = "Length";
136  new = _("Length");
137  break;
138  case SB_BITRATE:
139  current = "Bitrate";
140  new = _("Bitrate");
141  break;
142  case SB_FREQ:
143  current = "Freq";
144  new = _("Freq");
145  break;
146  case SB_ALBUM:
147  current = "Album";
148  new = _("Album");
149  break;
150  case SB_DATE:
151  current = "Date";
152  new = _("Date");
153  break;
154  case SB_FILETYPE:
155  current = "Filetype";
156  new = _("Filetype");
157  break;
158  case SB_GENRE:
159  current = "Genre";
160  new = _("Genre");
161  break;
162  case SB_SIZE:
163  current = "Size";
164  new = _("Size");
165  break;
166  case SB_FILENAME:
167  current = "Filename";
168  new = _("Filename");
169  break;
170  default:
171  continue;
172  }
173 
174  /* This test should not be necessary. But, just to be sure... */
175  if ((current == NULL) || (new == NULL))
176  continue;
177 
178  /* With the quickplay option, do not show Length, freq and bitrate */
179  if ((conf->options & MI_QUICKPL) && ((conf->fields[fld] == SB_LENGTH) ||
180  (conf->fields[fld] == SB_BITRATE) || (conf->fields[fld] == SB_FREQ)))
181  continue;
182 
183  /* XXX sorting is disabled for search results */
184  ap_rvputs(r, " <th class=\"", current, "\">", NULL);
185  if ((customlist == 0) && !(conf->search))
186  ap_rprintf(r, "<a href=\"?sort=%d\">%s</a>", conf->fields[fld], new);
187  else
188  ap_rputs(new, r);
189  ap_rputs("</th>\n", r);
190  }
191  /* at the end of the loop, 'fld' contains the number of fields */
192 
193  ap_rputs(" </tr>\n", r);
194 
195  /* init value. No chance to see the first strcmp true with that one */
196  current = "///";
197  new = current;
198 
199  for (q=pack->fhead; q!= NULL; q=q->next) {
200  /* If we have a recursive search request we have some work to do */
201  if ((conf->search != NULL) && (customlist == 0) &&
202  (conf->options & MI_RECURSIVE)) {
203  new = ap_make_dirstr_parent(r->pool, q->file);
204 
205  /* We just changed directories (during search, entries
206  * are sorted on a per-dir basis) so print a nice header */
207  if (strcmp(current, new)) {
208  current = new;
209  ap_rprintf(r, " <tr class=\"title\">\n"
210  " <th align=\"left\" colspan=\"%d\">", fld+1); /* "Select" => +1 */
211  if ((current[0] == '\0') || (current[1] == '\0')) /* current dir is either "" or "/" */
212  ap_rputs(_("In Current Directory"), r);
213  else
214  ap_rvputs(r, _("In"), " <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, current)), "\">", ap_escape_html(r->pool, current), "</a>", NULL);
215 
216  ap_rputs("</th>\n"
217  " </tr>\n", r);
218  j = 0;
219  }
220  }
221 
222  if ((j++) & 1)
223  ap_rputs(" <tr class=\"odd\">\n", r);
224  else
225  ap_rputs(" <tr class=\"even\">\n", r);
226 
227  /* prepare the "Select" panel, if any */
228  if ((customlist != 0) || (conf->options & (MI_ALLOWDWNLD | MI_ALLOWSTREAM | MI_ALLOWTARBALL)) ||
229  ((conf->search) && (conf->options & MI_RECURSIVE))) {
230  /* this should save some memory */
231  codeduri = (char *)realloc(codeduri, 1 + apr_base64_encode_len(strlen(q->uri)));
232 
233  ap_rputs(" <td class=\"Select\">\n", r);
234 
235  /* Display checkbox - no matter what if we have a customlist, only if stream/tarball allowed otherwise */
236  if (codeduri && (customlist || (conf->options & (MI_ALLOWSTREAM | MI_ALLOWTARBALL)))) {
237  apr_base64_encode(codeduri, q->uri, strlen(q->uri));
238  /* we base64 encode the uri to avoid nasty special chars problems */
239  ap_rvputs(r, " <input type=\"checkbox\" name=\"file\" value=\"",
240  codeduri, "\" />\n", NULL);
241  }
242 
243  if (!customlist) {
244  if (q->flags & EF_ALLOWDWNLD) /* Display [download] */
245  ap_rvputs(r, " <a class=\"dld\" href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "\" title=\"", _("Download"), "\">&nbsp;</a>\n", NULL);
246 
247  if (q->flags & EF_ALLOWSTREAM) /* Display [stream] */
248  ap_rvputs(r, " <a class=\"stream\" href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "?stream\" title=\"", _("Stream"), "\">&nbsp;</a>\n", NULL);
249  } else { /* in a custom list */
250  if (q->flags & EF_ALLOWTARBALL) /* Tell that tarball download will work with this file */
251  ap_rvputs(r, " <span class=\"dldok\" title=\"", _("Download Allowed"), "\">&nbsp;</span>\n", NULL);
252 
253  if (q->flags & EF_ALLOWSTREAM) /* Tell that streaming will work with this file */
254  ap_rvputs(r, " <span class=\"streamok\" title=\"", _("Stream Allowed"), "\">&nbsp;</span>\n", NULL);
255  }
256 
257  ap_rputs(" </td>\n", r);
258  }
259 
260  /* Now fill in the fields */
261  for (fld = 0; conf->fields[fld]; fld++) {
262  switch (conf->fields[fld]) {
263  case SB_TITLE:
264  ap_rvputs(r, " <td class=\"Title\">", ap_escape_html(r->pool, q->title), "</td>\n", NULL);
265  break;
266  case SB_TRACK:
267  if (q->track)
268  ap_rprintf(r, " <td class=\"Track\">%d</td>\n", q->track);
269  else
270  ap_rputs( " <td></td>\n", r);
271  break;
272  case SB_POSN:
273  if (q->posn)
274  ap_rprintf(r, " <td class=\"Disc\">%d</td>\n", q->posn);
275  else
276  ap_rputs( " <td></td>\n", r);
277  break;
278  case SB_ARTIST:
279  ap_rprintf(r, " <td class=\"Artist\">%s</td>\n", q->artist ? ap_escape_html(r->pool, q->artist) : "");
280  break;
281  case SB_LENGTH:
282  if (conf->options & MI_QUICKPL)
283  break;
284  if (q->length) {
285  ap_rprintf(r, " <td class=\"Length\">%d:%.2d</td>\n", q->length / 60, q->length % 60);
286  }
287  else
288  ap_rputs( " <td></td>\n", r);
289  break;
290  case SB_BITRATE:
291  if (conf->options & MI_QUICKPL)
292  break;
293  if (q->bitrate)
294  ap_rprintf(r, " <td class=\"Bitrate\"><acronym title=\"kbps%s\">%ld</acronym></td>\n", (q->flags & EF_VBR) ? " VBR" : "", q->bitrate >> 10);
295  else
296  ap_rputs( " <td></td>\n", r);
297  break;
298  case SB_FREQ:
299  if (conf->options & MI_QUICKPL)
300  break;
301  if (q->freq)
302 #ifdef HAVE_SETLOCALE
303  ap_rprintf(r, " <td class=\"Freq\"><acronym title=\"kHz\">%d%s%d</acronym></td>\n", q->freq / 1000, clconv->decimal_point, q->freq % 1000 / 100);
304 #else
305  ap_rprintf(r, " <td class=\"Freq\"><acronym title=\"kHz\">%d.%d</acronym></td>\n", q->freq / 1000, q->freq % 1000 / 100);
306 #endif
307  else
308  ap_rputs( " <td></td>\n", r);
309  break;
310  case SB_DATE:
311  if (q->date)
312  ap_rprintf(r, " <td class=\"Date\">%d&nbsp;</td>\n", q->date);
313  else
314  ap_rputs( " <td></td>\n", r);
315  break;
316  case SB_ALBUM:
317  ap_rprintf(r, " <td class=\"Album\">%s</td>\n", q->album ? ap_escape_html(r->pool, q->album) : "");
318  break;
319  case SB_FILETYPE:
320  ap_rvputs(r, " <td class=\"Filetype\">", filetype[q->filetype % FT_MAX].nametype, "</td>\n", NULL);
321  break;
322  case SB_GENRE:
323  ap_rprintf(r, " <td class=\"Genre\">%s</td>\n", q->genre ? ap_escape_html(r->pool, q->genre) : "");
324  break;
325  case SB_SIZE:
326 #ifdef HAVE_SETLOCALE
327  ap_rprintf(r, " <td class=\"Size\"><acronym title=\"MB\">%ld%s%ld</acronym></td>\n", q->size>>20, clconv->decimal_point, (((q->size>>10)%1024)*10)>>10); /* get kB residue *10/1024 (instead of /102.4) */
328 #else
329  ap_rprintf(r, " <td class=\"Size\"><acronym title=\"MB\">%ld.%ld</acronym></td>\n", q->size>>20, (((q->size>>10)%1024)*10)>>10); /* get kB residue *10/1024 (instead of /102.4) */
330 #endif
331  break;
332  case SB_FILENAME:
333  ap_rvputs(r, " <td class=\"Filename\">", ap_escape_html(r->pool, q->filename), "</td>\n", NULL);
334  break;
335  default:
336  break;
337  }
338  }
339  /* at the end of the loop, 'fld' contains the number of fields */
340  ap_rputs(" </tr>\n", r);
341  tlength += q->length;
342  }
343 
344  free(codeduri);
345 
346  /* close the table with some useful stuff */
347  ap_rprintf(r, " <tr class=\"stfoot\">\n <th align=\"left\" colspan=\"%d\">", fld+1); /* Select => +1 */
348 
349  /* Do NOT show 'select all' if there's nothing to select... */
350  if (!customlist && (conf->search) && (conf->options & MI_ALLOWSTREAM))
351  ap_rvputs(r, "<input type=\"checkbox\" name=\"all\" onclick=\"selall(this)\" />",
352  _("Select All"), " - ", NULL);
353 
354  ap_rprintf(r, "%s: %ld:%.2ld:%.2ld - %s: %ld%s%ld MB</th>\n </tr>\n",
355  _("Total Length"), tlength / 60 / 60, tlength / 60 % 60, tlength % 60,
356  _("Total Size"), tsize >> 20,
357 #ifdef HAVE_SETLOCALE
358  clconv->decimal_point,
359 #else
360  ".",
361 #endif
362  (((tsize >> 10) % 1024) * 10) >> 10);
363 
364  return fld;
365 }
366 
388 static void send_url(request_rec *r, const char *const restrict uri, const char *const restrict command,
389  const short html)
390 {
391  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
392  static char prefix[MAX_PREFIX];
393  static char str_port[7], strtmp[6]; /* (:)65536 + '\0' */
394 #ifdef BE_INSECURE
395  char * restrict bufcoded, * restrict decoded;
396 #endif
397  unsigned short l = 0;
398  unsigned register i=0, j=6;
399 
400  /* If we have an icecast server, it takes precedence, in a 'staticdir' fashion.
401  If we're sending an RSS feed, it's up to us to send the ?stream links. */
402  if ((conf->iceserver) && !(conf->options & MI_RSS)) {
403  /* icecast can only do http, so force that method */
404  strcpy(prefix, "http://"); /* 7 */
405 
406  /* if we only have a port number, we assume icecast is running on the same host */
407  if (conf->iceserver[0] == ':')
408  strcat(prefix, ap_get_server_name(r));
409 
410  strncat(prefix, conf->iceserver, MAX_PREFIX-8); /* 7+'\0'. if overflow -> truncated */
411  }
412  else {
413  strcpy(prefix, ap_http_method(r)); /* 4-5 */
414  strcat(prefix, "://"); /* 3 */
415 
416  if (REQUEST_USER(r)) {
417 #ifdef BE_INSECURE
418  /* grab the auth credentials base64 encoded - apparently only for auth_basic */
419  /* This is unsafe and thus disabled at compile time */
420  const char *const auth = apr_table_get(r->headers_in, "Authorization");
421  if (auth) {
422  bufcoded = strrchr(auth, ' ');
423  bufcoded++;
424  decoded = (char *)malloc(1 + apr_base64_decode_len(bufcoded));
425  l = apr_base64_decode(decoded, bufcoded);
426  strncat(prefix, decoded, l); /* we have "user:pass" */
427  free(decoded);
428  }
429 #else
430  strncat(prefix, REQUEST_USER(r), MAX_PREFIX-10); /* 5+3+1+'\0' */
431 #endif
432  strcat(prefix, "@"); /* 1 */
433  }
434 
435  /* safety check */
436  if (unlikely((strlen(prefix) + strlen(ap_get_server_name(r))) > MAX_PREFIX-10)) { /* 8+'\0'+safe */
437  /* we're going to exceed the size of the buffer including port number */
438  mi_rerror("prefix too %s", "long");
439  return;
440  }
441 
442  /* add the hostname */
443  strcat(prefix, ap_get_server_name(r));
444 
445  /* add the port number no matter what. This won't hurt, and
446  * checking whether it's really needed or not is just too
447  * much overhead.
448  * ap_get_server_port() deals with UseCanonicalName
449  * for us, and thus redirected ports will be properly handled.
450  * Custom 'snprintf' implementation for speed & fun */
451  l = ap_get_server_port(r);
452  while (--j && l) {
453  strtmp[i++] = "0123456789"[l%10];
454  l /= 10;
455  }
456  str_port[0] = ':';
457  for (j=1; i; j++)
458  str_port[j] = strtmp[--i];
459  str_port[j] = '\0';
460  strcat(prefix, str_port); /* 2-8 */
461  }
462 
463  /* add the uri and potential command */
464  if (html) /* we're going to send the url in an html document */
465  ap_rvputs(r, prefix, ap_escape_html(r->pool, ap_escape_uri(r->pool, uri)), NULL);
466  else /* sending to a playlist... */
467  ap_rvputs(r, prefix, ap_escape_uri(r->pool, uri), NULL);
468  if (command)
469  ap_rputs(command, r);
470 }
471 
482 void send_head(request_rec *r)
483 {
484  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
485  register short i;
486  const char *dir;
487  char *uri, *u;
488  request_rec *subreq = NULL;
489  const mu_config *localconf = NULL;
490 
491  /* this could arguably be reduced if treated directly at the setlocale()
492  call in mod_musicindex.c - XXX TO TEST */
493 #ifdef HAVE_SETLOCALE
494  if ((uri = setlocale(LC_ALL, NULL))) { /* query current locale */
495  /* get the ISO 639 language code */
496  if ((u = strchr(uri, '.')))
497  *u = '\0';
498  }
499 #else
500  uri = "en";
501 #endif
502 
503  ap_rvputs(r, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
504  "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" "
505  "\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
506  "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"", uri, "\" lang=\"", uri, "\">\n"
507  "<head>\n"
508  " <meta name=\"generator\" content=\"" MUSIC_HEADER_STRING "\" />\n", NULL);
509 
510  /* XXX REVISIT: restoring a feature with probably not too shabby code? */
511  /* dive into user's customisations, if any */
512  if ((subreq = ap_sub_req_lookup_uri(conf->directory, r, NULL)) != NULL) {
513  /* XXX enough to catch forbidden access? */
514  DIR *dir;
515  struct dirent *dstruct;
516  const char *ext;
517 
518  if ((dir = opendir(subreq->filename))) {
519  while ((dstruct = readdir(dir))) {
520  if ((dstruct->d_name[0] != '.') &&
521  (ext = strrchr(dstruct->d_name, '.')) &&
522  (!strncmp(ext+1, "css", 3))) {
523  /* XXX we don't make sure the files are R_OK */
524  if (!strcmp(dstruct->d_name, Gorigcss))
525  continue; /* this will be dealt with later */
526  if (!strcmp(dstruct->d_name, conf->css))
527  ap_rputs(" <link rel=\"stylesheet\" title=\"default\"", r);
528  else
529  ap_rvputs(r, " <link rel=\"alternate stylesheet\" title=\"",
530  ap_escape_html(r->pool, ap_escape_uri(r->pool, dstruct->d_name)),
531  "\"", NULL);
532 
533  ap_rvputs(r, " type=\"text/css\" href=\"", conf->directory, "/",
534  ap_escape_html(r->pool, ap_escape_uri(r->pool, dstruct->d_name)),
535  "\" />\n", NULL);
536  }
537  }
538  closedir(dir);
539  }
540  ap_destroy_sub_req(subreq);
541  }
542 
543  /* the original CSS */
544  if (!strcmp(Gorigcss, conf->css))
545  ap_rputs(" <link rel=\"stylesheet\" title=\"default\"", r);
546  else
547  ap_rputs(" <link rel=\"alternate stylesheet\" title=\"Original\"", r);
548  ap_rvputs(r, " type=\"text/css\" href=\"", conf->directory, "/", Gorigcss, "\" />\n", NULL);
549 
550  /* RSS link */
551  if (conf->rss_items > 0) {
552  ap_rvputs(r, " <link rel=\"alternate\" type=\"application/rss+xml\" "
553  "title=\"", _("Latest titles"), "\" href=\"?action=RSS\" />\n", NULL);
554  ap_rputs(" <link rel=\"alternate\" type=\"application/rss+xml\" "
555  "title=\"Podcast\" href=\"?action=podcast\" />\n", r);
556  }
557 
558  /* bit of javascript to handle select all */
559  ap_rputs(" <script type=\"text/javascript\">\n"
560  " // <![CDATA[\n"
561  " function selall(mine) {\n"
562  " for(var i=0; i<mine.form.elements.length; i++) {\n"
563  " var inpt = mine.form.elements[i];\n"
564  " var m = inpt.name.match(/-/g);\n"
565  " if ((inpt.name.substr(0,4) == 'file') && (m < 1)) {\n"
566  " inpt.checked = mine.form.all.checked;\n"
567  " }\n"
568  " }\n"
569  " }\n"
570  " // ]]>\n"
571  " </script>\n", r);
572 
573  /* the original favicon */
574  ap_rvputs(r, " <link rel=\"shortcut icon\" href=\"", conf->directory, "/", Gfavicon, "\" />\n"
575  " <link rel=\"icon\" href=\"", conf->directory, "/", Gfavicon, "\" type=\"image/ico\" />\n"
576  " <title>", _("Musical index of"), " ", ap_escape_html(r->pool, r->uri), "</title>\n"
577  "</head>\n\n"
578  "<body>\n"
579  "<!-- begin header -->\n", NULL);
580 
581  ap_rputs("<div id=\"header\">\n"
582  " <div id=\"mainicon\">\n"
583  " <img alt=\"Dir\" src=\"", r);
584 
585  for (i = 0; Gcovericns[i]; i++) {
586  if (access(apr_pstrcat(r->pool, r->filename, "/", Gcovericns[i], NULL), R_OK) == 0) {
587  ap_rputs(Gcovericns[i], r);
588  i = -1;
589  break;
590  }
591  else if (access(apr_pstrcat(r->pool, r->filename, "/.", Gcovericns[i], NULL), R_OK) == 0) {
592  ap_rvputs(r, ".", Gcovericns[i], NULL);
593  i = -1;
594  break;
595  }
596  }
597 
598  if (i > 0)
599  ap_rvputs(r, conf->directory, "/", Gcd_icon, NULL);
600 
601  ap_rputs("\" />\n"
602  " </div>\n", r);
603 
604  /* Display the box containing the path and the stream links */
605  ap_rputs(" <div id=\"maintitle\">\n"
606  " <h1>\n", r);
607 
608  uri = apr_pstrdup(r->pool, r->uri);
609 
610  /* XXX Could use some doc... :P */
611  for (u = uri; *u != '\0'; u++) {
612  dir = u;
613  while ((*u != '/') && (*u != '\0'))
614  u++;
615 
616  if (conf->title == NULL) {
617  char bk = *(++u);
618  *u = '\0';
619  subreq = ap_sub_req_lookup_uri(uri, r, NULL);
620  *(u--) = bk;
621  localconf = (mu_config*)ap_get_module_config(subreq->per_dir_config, &musicindex_module);
622  }
623  else {
624  localconf = conf;
625  subreq = NULL;
626  }
627 
628  if ((localconf->options & MI_ACTIVE) || (localconf->title == NULL)) {
629  if ((u == uri) && (localconf->title != NULL))
630  dir = localconf->title;
631 
632  *u = '\0';
633  ap_rvputs(r, " <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, uri)), "/\">", ap_escape_html(r->pool, dir), "</a>\n", NULL);
634  *u = '/';
635 
636  if (*(u+1) != '\0')
637  ap_rputs(" <span class=\"rarrow\">&nbsp;</span>\n", r);
638  }
639 
640  if (subreq != NULL)
641  ap_destroy_sub_req(subreq);
642  }
643 
644  ap_rputs(" </h1>\n", r);
645 
646  if (conf->options & MI_ALLOWSTREAM) {
647  ap_rvputs(r, " <a class=\"shuffle\" "
648  "href=\"?option=recursive&amp;option=shuffle&amp;action=playall\""
649  " title=\"", _("Shuffle All"), "\">&nbsp;</a>\n"
650  " <a class=\"stream\" "
651  "href=\"?option=recursive&amp;action=playall\""
652  " title=\"", _("Stream All"), "\">&nbsp;</a>\n", NULL);
653  }
654 
655  if (conf->options & MI_ALLOWTARBALL) {
656  ap_rvputs(r, " <a class=\"tarball\" "
657  "href=\"?option=recursive&amp;action=tarball\""
658  " title=\"", _("Download All"), "\">&nbsp;</a>\n", NULL);
659  }
660 
661  if (conf->rss_items > 0) {
662  ap_rvputs(r, " <a class=\"rss\" "
663  "href=\"?action=RSS\""
664  " title=\"", _("RSS"), "\">&nbsp;</a>\n", NULL);
665  }
666 
667  /* XXX to integrate in a better way */
668  ap_rvputs(r, " <br /><a class=\"rdir\" "
669  "href=\"?action=randomdir\">[",
670  _("Random subdirectory..."), "]</a>\n </div>\n", NULL);
671 
672  /* displays a search box if option is activated */
673  /* XXX collision avec le header, a regler */
674  if (conf->options & MI_ALLOWSEARCH) {
675  ap_rvputs(r,
676  " <form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\""
677  " enctype=\"application/x-www-form-urlencoded\""
678  " id=\"searching\">\n"
679  " <p>\n"
680  " <input type=\"text\" name=\"search\" />\n"
681  " <br />\n"
682  " <button type=\"submit\" name=\"action\" value=\"Search\">", _("Search"), "</button>\n"
683  " <button type=\"submit\" name=\"action\" value=\"RecursiveSearch\">", _("Recursive Search"), "</button>\n"
684  " <input type=\"hidden\" name=\"action\" value=\"Search\" />\n"
685  " </p>\n"
686  " </form>\n",
687  NULL);
688  }
689 
690  ap_rputs("</div>\n"
691  "<hr />\n"
692  "<!-- end header -->\n\n", r);
693 
694 }
695 
713 void send_directories(request_rec *r, const mu_pack *const pack)
714 {
715  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
716  const mu_ent *q;
717  unsigned short dircnt = 0, nb = pack->dirnb;
718 
719  if (nb == 0)
720  return;
721 
722  ap_rputs("<!-- begin subdirs -->\n<h2>",r);
723  ap_rprintf(r, _("Music Directories (%d)"), nb);
724  ap_rputs("</h2>\n\n<table id=\"directories\">\n", r);
725 
726  for (q=pack->head; q && (q->filetype < 0); q = q->next) {
727  /* start a new directories row */
728  if (dircnt++ == 0)
729  ap_rputs(" <tr>\n", r);
730 
731  ap_rvputs(r, " <td>\n"
732  " <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
733  "\" class=\"bigdir\" title=\"", _("Open"), "\">&nbsp;</a>\n"
734  " <div>\n"
735  " <a href=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)), "\">",
736  ap_escape_html(r->pool, q->title), "</a>", NULL);
737 
738  if (conf->dir_per_line > 0) {
739  /* show various useful links when needed */
740  ap_rputs("<br />\n", r);
741  if (q->flags & EF_ALLOWSTREAM) {
742  ap_rvputs(r, " <a class=\"shuffle\" href=\"",
743  ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
744  "?option=recursive&amp;option=shuffle&amp;action=playall\""
745  " title=\"", _("Shuffle"), "\">&nbsp;</a>\n"
746  " <a class=\"stream\" href=\"",
747  ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
748  "?option=recursive&amp;action=playall\""
749  " title=\"", _("Stream"), "\">&nbsp;</a>\n", NULL);
750  }
751 
752  if (q->flags & EF_ALLOWTARBALL) {
753  ap_rvputs(r, " <a class=\"tarball\" href=\"",
754  ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
755  "?option=recursive&amp;action=tarball\""
756  " title=\"", _("Download"), "\">&nbsp;</a>\n", NULL);
757  }
758 
759  if (q->flags & EF_ALLOWRSS) {
760  ap_rvputs(r, " <a class=\"rss\" href=\"",
761  ap_escape_html(r->pool, ap_escape_uri(r->pool, q->file)),
762  "?action=RSS\""
763  " title=\"", _("RSS"), "\">&nbsp;</a>\n", NULL);
764  }
765  }
766 
767  ap_rputs("\n </div>\n"
768  " </td>\n",r);
769 
770  /* end the directories row */
771  if (dircnt == abs(conf->dir_per_line)) {
772  dircnt = 0;
773  ap_rputs(" </tr>\n", r);
774  }
775  }
776 
777  /* if we haven't ended on a dir_per_line row, close it here */
778  if (dircnt != 0)
779  ap_rputs( "</tr>\n", r);
780 
781  ap_rputs("</table>\n<hr />\n<!-- end subdirs -->\n\n", r);
782 }
783 
794 void send_tracks(request_rec *r, const mu_pack *const pack)
795 {
796  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
797  register unsigned short nb = 0;
798  unsigned long fnb = pack->filenb;
799  const unsigned char *const restrict order = conf->order;
800 
801  if (fnb == 0)
802  return;
803 
804  ap_rputs("<!-- begin tracks -->\n<h2>", r);
805 
806  if (conf->search)
807  ap_rprintf(r, _("Result List (%ld)"), fnb);
808  else
809  ap_rprintf(r, _("Song List (%ld)"), fnb);
810 
811  ap_rvputs(r, "</h2>\n\n<form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\" "
812  "enctype=\"application/x-www-form-urlencoded\" id=\"tracks\">\n <table>\n", NULL);
813 
814  nb = list_songs(r, pack, 0);
815 
816  ap_rputs(" </table>\n", r);
817 
818  /* Do NOT show buttons if we do not allow anything but listing or individual downloading! */
819  if ((conf->options & (MI_ALLOWSTREAM|MI_ALLOWTARBALL))) {
820  ap_rputs(" <div>\n"
821  " <input type=\"hidden\" name=\"sort\" value=\"", r);
822  for(nb=0; order[nb] && (nb<SB_MAX); nb++)
823  ap_rprintf(r, "%c", order[nb]+'`'); /* 1+'`' = 'a'. shift everything to ascii small letters */
824  ap_rvputs(r, "\" />\n"
825  " <button type=\"submit\" name=\"action\" value=\"AddToPlaylist\" class=\"playlist\">",
826  _("Add To Playlist"), "</button>\n", NULL);
827 
828  if (conf->search == NULL) {
829  ap_rvputs(r, " <button type=\"submit\" name=\"action\" value=\"AddAllToPlaylist\" class=\"playlist\">",
830  _("Add All To Playlist"), "</button>\n", NULL);
831  if (conf->options & MI_ALLOWSTREAM)
832  ap_rvputs(r, " <button type=\"submit\" name=\"action\" value=\"ShuffleAll\">",
833  _("Shuffle All"), "</button>\n"
834  " <button type=\"submit\" name=\"action\" value=\"PlayAll\">",
835  _("Play All"), "</button>\n", NULL);
836  if (conf->options & MI_ALLOWTARBALL)
837  ap_rvputs(r, " <button type=\"submit\" name=\"action\" value=\"DownloadAll\">",
838  _("Download All"), "</button>\n", NULL);
839  }
840 
841  if (conf->options & MI_ALLOWSTREAM)
842  ap_rvputs(r, " <button type=\"submit\" name=\"action\" value=\"PlaySelected\">",
843  _("Play Selected"), "</button>\n", NULL);
844 
845  if (conf->options & MI_ALLOWTARBALL)
846  ap_rvputs(r, " <button type=\"submit\" name=\"action\" value=\"DownloadSelected\">",
847  _("Download Selected"), "</button>\n", NULL);
848 
849  ap_rputs(" </div>\n", r);
850  }
851 
852  ap_rputs("</form>\n"
853  "<hr />\n"
854  "<!-- end tracks -->\n\n", r);
855 }
856 
873 void send_customlist(request_rec *r, const mu_pack *const pack)
874 {
875  register unsigned short nb = pack->filenb;
876 
877  if (!pack->fhead)
878  return;
879 
880  ap_rputs("<!-- begin custom -->\n"
881  "<h2>", r);
882  ap_rprintf(r, _("Custom Playlist (%d)"), nb);
883  ap_rputs("</h2>\n\n", r);
884 
885  ap_rvputs(r, " <form method=\"post\" action=\"", ap_escape_html(r->pool, ap_escape_uri(r->pool, r->uri)), "\" "
886  "enctype=\"application/x-www-form-urlencoded\" id=\"custom\">\n <table>\n", NULL);
887 
888  list_songs(r, pack, 1);
889 
890  ap_rvputs(r, " </table>\n"
891  " <div>\n <button type=\"submit\" name=\"action\" value=\"RemoveFromPlaylist\" class=\"playlist\">",
892  _("Remove From Playlist"), "</button>\n"
893  " <button type=\"submit\" name=\"action\" value=\"ClearPlaylist\" class=\"playlist\">",
894  _("Clear Playlist"), "</button>\n"
895  " <button type=\"submit\" name=\"action\" value=\"StreamPlaylist\" class=\"playlist\">",
896  _("Stream Playlist"), "</button>\n"
897 #ifdef ENABLE_OUTPUT_ARCHIVE
898  " <button type=\"submit\" name=\"action\" value=\"DownloadPlaylist\" class=\"playlist\">",
899  _("Download Playlist"), "</button>\n"
900 #endif
901  , NULL);
902 
903  ap_rputs(" </div>\n"
904  " </form>\n"
905  "<hr />\n"
906  "<!-- end custom -->\n\n", r);
907 }
908 
918 void send_playlist(request_rec *r, const mu_pack *const pack)
919 {
920  const mu_ent *q = pack->fhead;
921 
922  if (!q)
923  return;
924 
925  ap_rputs("#EXTM3U\n", r);
926 
927  for (; q; q = q->next) {
928  /* do not include file in playlist if streaming is not allowed */
929  if (!(q->flags & EF_ALLOWSTREAM))
930  continue;
931 
932  ap_rprintf(r, "#EXTINF:%i,", q->length);
933  if (q->artist)
934  ap_rvputs(r, q->artist, " - ", NULL);
935  ap_rvputs(r, q->title, NULL);
936  if (q->album)
937  ap_rvputs(r, " (", q->album, ")", NULL);
938  ap_rputc('\n', r);
939  send_url(r, q->uri, NULL, 0);
940  ap_rputc('\n', r);
941  }
942 }
943 
953 void send_rss(request_rec *r, const mu_pack *const pack)
954 {
955  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
956  const mu_ent *q = pack->fhead;
957  char date_buf[32];
958  struct tm time_buf;
959  unsigned short limit = conf->rss_items;
960 
961  if (!q)
962  return;
963 
964  ap_rputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
965  "<rss ", r);
966 
967  if (conf->options & MI_PODCAST)
968  ap_rputs("xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\" ", r);
969 
970  ap_rvputs(r, "version=\"2.0\">\n"
971  " <channel>\n"
972  " <title>", _("RSS Feed for "), ap_escape_html(r->pool, r->uri), "</title>\n"
973  " <link>", NULL);
974  send_url(r, r->uri, NULL, 1);
975 
976  ap_rputs("</link>\n <description>", r);
977  ap_rprintf(r, _("%d most recent songs from %s"), conf->rss_items, ap_escape_html(r->pool, r->uri));
978  ap_rputs("</description>\n", r);
979 
980  if ((conf->options & MI_PODCAST) != 0) {
981  ap_rputs(" <itunes:summary>", r);
982  ap_rprintf(r, _("%d most recent songs from %s"), conf->rss_items, ap_escape_html(r->pool, r->uri));
983  ap_rputs("</itunes:summary>\n", r);
984  }
985 
986  ap_rputs(" <generator>" MUSIC_HEADER_STRING "</generator>\n"
987  " <docs>http://backend.userland.com/rss</docs>\n"
988  " <ttl>60</ttl>\n",
989  r);
990 
991  for (; limit && q; q = q->next) {
992  ap_rvputs(r, " <item>\n"
993  " <title>", ap_escape_html(r->pool, q->title), "</title>\n", NULL);
994 
995  if (conf->options & MI_ALLOWSTREAM) {
996  ap_rputs(" <link>", r);
997  send_url(r, q->uri, "?stream", 1);
998  ap_rputs("</link>\n", r);
999  }
1000 
1001  if (conf->options & MI_ALLOWDWNLD) {
1002  ap_rputs(" <enclosure url=\"", r);
1003  send_url(r, q->uri, NULL, 1);
1004  ap_rprintf(r, "\" length=\"%lu\" type=\"%s\" />\n",
1005  q->size, filetype[q->filetype % FT_MAX].mimetype);
1006 
1007  if (conf->options & MI_PODCAST) {
1008  ap_rputs(" <guild>", r);
1009  send_url(r, q->uri, NULL, 1);
1010  ap_rputs("</guild>\n", r);
1011  localtime_r(&q->mtime, &time_buf);
1012  strftime(date_buf, 32, "%a, %e %b %Y %H:%M:%S %z", &time_buf);
1013  ap_rprintf(r, " <pubDate>%s</pubDate>\n", date_buf);
1014  if (q->length)
1015  ap_rprintf(r, "<itunes:duration>%u:%.2u</itunes:duration>",
1016  q->length / 60, q->length % 60);
1017  }
1018  }
1019 
1020  ap_rvputs(r, " <description>\n ", _("Artist"), " | ", _("Album"), " | ", _("Track"), " | ", _("Disc"),
1021  " | ", _("Length"), " | ", _("Genre"), " | ", _("Bitrate"), " | ", _("Freq"), " | ", _("Filetype"),
1022  " | ", _("Size"), "<br />\n", NULL);
1023  ap_rprintf(r, " %s | %s | ", q->artist ? ap_escape_html(r->pool, q->artist) : "",
1024  q->album ? ap_escape_html(r->pool, q->album) : "");
1025  if (q->track)
1026  ap_rprintf(r, "%u", q->track);
1027  ap_rputs(" | ", r);
1028  if (q->posn)
1029  ap_rprintf(r, "%u", q->posn);
1030  ap_rputs(" | ", r);
1031  if (q->length)
1032  ap_rprintf(r, "%u:%.2u", q->length / 60, q->length % 60);
1033  ap_rputs(" | ", r);
1034  ap_rprintf(r, "%s | ", q->genre ? ap_escape_html(r->pool, q->genre) : "");
1035  if (q->bitrate)
1036  ap_rprintf(r, "%lu %s", q->bitrate >> 10, (q->flags & EF_VBR) ? "VBR" : "");
1037  ap_rputs(" | ", r);
1038  if (q->freq)
1039  ap_rprintf(r, "%u", q->freq);
1040  ap_rputs(" | ", r);
1041  ap_rprintf(r, "%s | %lu\n", filetype[q->filetype % FT_MAX].nametype, q->size);
1042  ap_rputs(" </description>\n"
1043  " </item>\n", r);
1044 
1045  limit--;
1046  }
1047  ap_rputs(" </channel>\n"
1048  "</rss>\n", r);
1049 }
1050 
1067 void send_randomdir(request_rec *r)
1068 {
1069  const char *filename, *uri, *nextfile, *nexturi;
1070  unsigned short nb, test, i;
1071  unsigned int seed = time(NULL);
1072  DIR *dir;
1073  struct dirent *dirent;
1074 
1075  filename = apr_pstrdup(r->pool, r->filename);
1076  uri = apr_pstrdup(r->pool, r->uri);
1077 
1078  while (1) {
1079  /* XXX this is ugly */
1080  filename = apr_pstrcat(r->pool, filename, "/", NULL);
1081 
1082  /* open the current directory for scanning */
1083  dir = opendir(filename);
1084 
1085  /* XXX TODO optimize the loops */
1086  /* count the number of *valid* dir entries */
1087  nb = 0;
1088  while ((dirent = readdir(dir)))
1089  if ((dirent->d_name[0] != '.') && (access(r->filename, R_OK|X_OK) == 0))
1090  nb++;
1091 
1092  /* if nb == 0, we've got an empty directory. Don't go any further */
1093  if (!nb) {
1094  closedir(dir);
1095  apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, ap_escape_uri(r->pool, uri), r));
1096  return;
1097  }
1098 
1099  /* once it's done, go back to the begining of the dir stream */
1100  rewinddir(dir);
1101 
1102  /* generate a random position, using high-order bits. */
1103  test = (int)(1.0*nb*rand_r(&seed)/(RAND_MAX+1.0));
1104 
1105  /* cycle through the current dir until we've reached EOF or random position */
1106  i = 0;
1107  while((i <= test) && (dirent = readdir(dir)))
1108  if ((dirent->d_name[0] != '.') && (access(r->filename, R_OK|X_OK) == 0))
1109  i++;
1110 
1111  /* construct a new filename with the current dirent */
1112  nextfile = apr_pstrcat(r->pool, filename, dirent->d_name, NULL);
1113  nexturi = apr_pstrcat(r->pool, uri, dirent->d_name, NULL);
1114 
1115  /* done with the dir listing, close it */
1116  closedir(dir);
1117 
1118  /* if the new filename is a directory, start the loop again, else, break */
1119  if (!ap_is_directory(r->pool, nextfile)) {
1120  /* XXX this is probably not good either */
1121  apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, ap_escape_uri(r->pool, uri), r));
1122  return;
1123  }
1124  else {
1125  /* XXX gack! */
1126  filename = nextfile;
1127  uri = apr_pstrcat(r->pool, nexturi, "/", NULL);
1128  }
1129  }
1130 }
1131 
1143 void send_foot(request_rec *r, const STRUCTTV *tvbegin, const STRUCTTV *tvprocess)
1144 {
1145  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
1146  char *icn = NULL;
1147  request_rec *subreq = ap_sub_req_lookup_uri(conf->directory, r, NULL);
1148 #ifdef HAVE_GETTIMEOFDAY
1149  struct timeval tvend, tvres;
1150 #ifdef HAVE_SETLOCALE
1151  struct lconv *tlconv = NULL;
1152 #endif
1153 #endif
1154 
1155  ap_rputs("<!-- begin footer -->\n"
1156  "<!-- mod_musicindex v." MUSIC_VERSION_STRING " -->\n"
1157  "<!-- Authors: " MUSIC_AUTHORS_STRING " -->\n"
1158  "<div id=\"footer\">\n"
1159  " <div id=\"valid\">\n", r);
1160  /* if we have local icons, use them */
1161  /* valid xhtml 1.1 */
1162  if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/valid-xhtml11", NULL), R_OK)) {
1163  icn = apr_pstrcat(r->pool, conf->directory, "/valid-xhtml11", NULL);
1164  ap_rvputs(r, " <img src=\"", icn, "\" alt=\"Valid XHTML 1.1!\" />\n", NULL);
1165  }
1166  else
1167  ap_rputs(" <a href=\"http://validator.w3.org/check?uri=referer\">\n"
1168  " <img src=\"http://www.w3.org/Icons/valid-xhtml11\"\n"
1169  " alt=\"Valid XHTML 1.1!\" height=\"31\" width=\"88\" />\n"
1170  " </a>\n", r);
1171  /* valid CSS */
1172  if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/vcss", NULL), R_OK)) {
1173  icn = apr_pstrcat(r->pool, conf->directory, "/vcss", NULL);
1174  ap_rvputs(r, " <img src=\"", icn, "\" alt=\"Valid CSS!\" />\n", NULL);
1175  }
1176  else
1177  ap_rputs(" <a href=\"http://jigsaw.w3.org/css-validator/check/referer\">\n"
1178  " <img src=\"http://jigsaw.w3.org/css-validator/images/vcss\"\n"
1179  " alt=\"Valid CSS!\" height=\"31\" width=\"88\" />\n"
1180  " </a>\n", r);
1181  /* Valid RSS */
1182  if (subreq && !access(apr_pstrcat(r->pool, subreq->filename, "/valid-rss.png", NULL), R_OK)) {
1183  icn = apr_pstrcat(r->pool, conf->directory, "/valid-rss.png", NULL);
1184  ap_rvputs(r, " <img src=\"", icn, "\" alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" />\n", NULL);
1185  }
1186  else
1187  ap_rputs(" <a href=\"http://validator.w3.org/feed/check.cgi?uri=referer\">\n"
1188  " <img src=\"http://validator.w3.org/feed/images/valid-rss.png\"\n"
1189  " alt=\"[Valid RSS]\" title=\"Validate my RSS feed\" />\n"
1190  " </a>\n", r);
1191  ap_rputs(" </div>\n", r);
1192 
1193 #ifdef HAVE_GETTIMEOFDAY
1194  /* timing */
1195  gettimeofday(&tvend, NULL);
1196  timersub(&tvend, tvbegin, &tvres);
1197  ap_rputs(" <div id=\"timing\">", r);
1198  ap_rprintf(r, "<!-- processing completed in %u.%.6u s -->", tvprocess->tv_sec, tvprocess->tv_usec);
1199 #ifdef HAVE_SETLOCALE
1200  tlconv = localeconv();
1201  ap_rprintf(r, _("in %u%s%.6u s"), tvres.tv_sec, tlconv->decimal_point, tvres.tv_usec);
1202 #else
1203  ap_rprintf(r, _("in %u.%.6u s"), tvres.tv_sec, tvres.tv_usec);
1204 #endif
1205  ap_rputs("</div>\n", r);
1206 #endif
1207 
1208  /* Signature */
1209  ap_rputs(" <div id=\"name\">"
1210  "<a href=\"http://freecode.com/projects/musicindex/\">"
1211  "MusicIndex v." MUSIC_VERSION_STRING "</a></div>\n"
1212  "</div>\n"
1213  "<!-- end footer -->\n\n"
1214  "</body>\n"
1215  "</html>", r);
1216 }