/*
 *  http.c
 *  mod_musicindex
 *
 *  $Id: http.c 1003 2012-07-31 09:53:17Z varenet $
 *
 *  Created by Regis BOUDIN on Sun Jun 13 2004.
 *  Copyright (c) 2003-2004 Regis BOUDIN
 *  Copyright (c) 2006-2007 Thibaut VARENE
 *  
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License version 2.1,
 *  as published by the Free Software Foundation.
 *
 */

/**
 * @file 
 * HTTP features handling.
 *
 * @author Regis Boudin
 * @author Thibaut Varene
 * @version $Revision: 1003 $
 * @date 2004-2007
 *
 * This file is here to deal with HTTP related
 * features which are not directly handled by
 * Apache. It includes parsing of arguments
 * received via GET or POST requests and
 * cookie management for cross-directories
 * playlists.
 *
 * @section cookies Cookies
 * We're gonna base64 encode all strings stored to the cookie. base64 uses
 * only the following characters: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
 * so we should be perfectly fine dealing with '&' and friends.
 *
 * We store in the cookie file identifier as follows:
 * base64_encoded_uri&"
 * We store the unescaped base64 string in the cookie.
 *
 * Also note that according to: http://www.15seconds.com/faq/Cookies/388.htm @n
 * According to the draft specification issued by Netscape Communications, the
 * limits regarding the size of cookie and space occupied by all cookies is:
 *  - 4 KB per cookie maximum
 *  - 300 total cookies, for a total of 1.2 Mbytes maximum
 *  - 20 cookies accepted from a particular server or domain
 *
 * Which means that we don't have infinite space, and can't store big custom
 * lists. Reports shows that ~50 songs is a max, but if depends on the length
 * of the path anyway.
 *
 * @todo Test, test, test.
 * @todo Use cookie for session identifer, and store custom lists on the server.
 *
 * @bug Beware of "big lists"
 * @todo Store custom list locally, and store only a session reference in the cookie.
 */

#include "http.h"
#include "sort.h"
#include "playlist.h"
#ifdef HAVE_STDLIB_H
#include <stdlib.h>	/* atoi */
#endif

/**
 * Treats the GET arguments
 *
 * This function searches for keywords passed as URL arguments (with "?xxx")
 * and sets the handler options accordingly.
 * 
 * @param r Apache request_rec struct to handle connection details.
 */
void treat_get_args(request_rec *r)
{
	mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const char *s = r->args;
	const char *p;
	register unsigned short i;

	if (s == NULL)
		return;
	
	conf->custom_list = r->args;

	while (s[0]) {
		p = ap_getword(r->pool, &s, '&');
		if (!strncmp(p, "action=", 7)) {
			p += 7;
			
			if (!strcmp(p, "randomdir")) {
				conf->options |= MI_RANDOMDIR;
				return;
			}
			
			if (conf->options & MI_ALLOWTARBALL) {
				if (!strcmp(p, "tarball")) {
					conf->options |= (MI_DWNLDALL | MI_QUICKPL) ;
					conf->order[0] = SB_DIR;
					conf->order[1] = SB_URI;
					continue;
				}
			}
			
			if (conf->options & MI_ALLOWSTREAM) {
				if (!strcmp(p, "playall")) {
					conf->options |= MI_STREAMALL;
					continue;
				}
			}
			
			if (conf->rss_items > 0) {
				if (!strcmp(p, "RSS")) {
					conf->options |= MI_RSS;
					conf->order[0] = SB_MTIME;
					conf->order[1] = SB_URI;
					conf->options &= ~MI_RECURSIVE;
					continue;
				}

				else if (!strcmp(p, "podcast")) {
					conf->options |= MI_RSS|MI_PODCAST;
					conf->order[0] = SB_MTIME;
					conf->order[1] = SB_URI;
					conf->options &= ~MI_RECURSIVE;
					continue;
				}
			}
		}
		else if (!strncmp(p, "sort=", 5)) {
			p += 5;
		/* Temporary workaround : only have one sort param */
			for (i = SB_MAX; i > 0; i--)
				conf->order[i] = conf->order[i-1];
			conf->order[0] = (atoi(p) % SB_MAX);	/* avoid out of bound values */
		}
		else if (!strncmp(p, "option=", 7)) {
			p += 7;
			if (!strcmp(p, "recursive"))
				conf->options |= MI_RECURSIVE;
			else if (!strcmp(p, "shuffle")) {
				conf->order[0] = SB_RANDOM;
				conf->order[1] = SB_DEFAULT;
			}
			else if (!strcmp(p, "quick"))
				conf->options |= MI_QUICKPL;
		}
		else if (!strncmp(p, "limit=", 6)) {
			if (conf->rss_items > 0)
				conf->rss_items = atoi(p+6);
		}
	}
}

/**
 * Treats the POST arguments
 *
 * This function searches for keywords in arguments sent in a POST
 * request. It only sets option flags and the search string when they are
 * needed.
 * 
 * @warning This function is fairly inefficient as-is. It does a lot of useless checks
 * and string comparisons, because it will only deal with 1 action at a time, so
 * as soon as we have one hit, we could give up. We could add 'return's everywhere
 * but it's pretty ugly as well.
 *
 * @param r Apache request_rec struct to handle connection details.
 */
void treat_post_args(request_rec *r)
{
	mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	char buffer[MAX_STRING];
	const char *s = NULL;
	const char *p = s;
	short i = 0;

	ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK);

	do {
		i = ap_get_client_block(r, buffer, MAX_STRING-1);
		buffer[i] = '\0';
		if (s == NULL)
			s = apr_pstrdup(r->pool, buffer);
		else
			s = apr_pstrcat(r->pool, s, buffer, NULL);
	} while (i == (MAX_STRING-1));

	conf->custom_list = s;

	/* Find the search string */
	p = strstr(s, "&search=");

	if (p != NULL)
		p += 1;
	else if (strncmp(s, "search=", 7) == 0)
		p = s;
	
	if (p != NULL) {
		p += 7;
		conf->search = ap_getword(r->pool, &p, '&');
		for (i=0; p[i]; i++) {
			/* x-www-form-urlencoded changes spaces to '+' */
			if (conf->search[i] == '+')
				conf->search[i] = ' ';
		}
		ap_unescape_url(conf->search); /* x-www-form-urlencoded forms */
	}

	/* Find the sort string */
	p = strstr(s, "&sort=");

	if (p != NULL)
		p += 1;
	else if (strncmp(s, "sort=", 5) == 0)
		p = s;
	
	if (p != NULL) {
		p += 5;
		/* The client sends us a complete sort sequence.
		 * The modulo works around potentially (forged?) out of bound values */
		for (i = 0; p[i] && (p[i] != '&') && (i < SB_MAX); i++)
			conf->order[i] = ((p[i]-'`') % SB_MAX);	/* unshift from letters */
	}

	/* Find the action string. We expect to have only one action at a time.
	 * Otherwise, supplementary actions will be ignored. */
	p = strstr(s, "&action=");

	if (p != NULL)
		p += 1;
	else if (strncmp(s, "action=", 7) == 0)
		p = s;
	
	if (p != NULL) {
		p += 7;
	
		p = ap_getword(r->pool, &p, '&');
		
		if ((conf->options & MI_ALLOWSEARCH) && conf->search && conf->search[0]) {
			if (!strcmp(p, "Search")) {
				if (!conf->cache)
					conf->options |= MI_QUICKPL;
			}
			else if (!strcmp(p, "RecursiveSearch")) {
				conf->options |= MI_RECURSIVE;
				conf->order[0] = SB_DIR;
				conf->order[1] = SB_URI;
				if (!conf->cache)
					conf->options |= MI_QUICKPL;
			}
		}
		
		/* In any situation, no recursive directories
		scan for Cookie operations */
		if (conf->options & MI_COOKIEOP)
			conf->options &= ~MI_RECURSIVE;

		if (conf->options & MI_ALLOWSTREAM) {
			if (!strcmp(p, "PlaySelected")) {
				conf->options |= MI_STREAMLST;
			} else if (!strcmp(p, "PlayAll")) {
				conf->options |= MI_STREAMALL;
			} else if (!strcmp(p, "ShuffleAll")) {
				conf->options |= MI_STREAMALL;
				conf->order[0] = SB_RANDOM;
				conf->order[1] = SB_DEFAULT;
			}
		}
		
		if (conf->options & MI_ALLOWTARBALL) {
			if (!strcmp(p, "DownloadAll")) {
				conf->options |= MI_DWNLDALL;
			} else if (!strcmp(p, "DownloadSelected")) {
				conf->options |= MI_DWNLDLST;
			}
		}

		if ((conf->options & (MI_ALLOWSTREAM|MI_ALLOWTARBALL))) {
			if (!strcmp(p, "AddToPlaylist")) {
				conf->options |= MI_COOKIEADDLST;
			} else if (!strcmp(p, "AddAllToPlaylist")) {
				conf->options |= MI_COOKIEADDALL;
			}
		}

		if (!strcmp(p, "RemoveFromPlaylist")) {
			conf->options |= MI_COOKIEDELLST;
		} else if (!strcmp(p, "ClearPlaylist")) {
			conf->options |= MI_COOKIEPURGE;
		} else if (!strcmp(p, "StreamPlaylist")) {
			conf->options |= MI_COOKIESTREAM;
		} else if (!strcmp(p, "DownloadPlaylist")) {
			conf->options |= MI_COOKIEDWNLD;
		}
	}
}

static const char *find_playlist(request_rec *r, apr_pool_t *subpool)
{
	const char *cookie = NULL, *playlist_cookie = NULL;

	cookie = apr_table_get(r->headers_in, "Cookie");
	if (cookie) {
		playlist_cookie = strstr(cookie, "playlist=");
		if (playlist_cookie)
			playlist_cookie = ap_getword(subpool, &playlist_cookie, ';');
	}

	return playlist_cookie;
}

static const char *find_or_create_playlist(request_rec *r, apr_pool_t *subpool)
{
	const char *playlist = NULL;

	playlist = find_playlist(r, subpool);
	if (!playlist)
		playlist = apr_pstrdup(subpool, "playlist=");

	return playlist;
}

/**
 * Adds file(s) to the cookie holding the custom playlist.
 *
 * This function takes a list of files to add to the custom playlist cookie in
 * the form of base64 encoded uris stored in the conf->custom_list field, comming from
 * the POST arguments. 
 *
 * @param r the apache request
 * @param subpool the current subpool to use for mem allocations
 * @return a string to store in the cookie
 */
static const char *cookie_add(request_rec *r, apr_pool_t *subpool)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const char	*new_cookie_string = NULL;
	const char	*args = conf->custom_list;
	char		*p = NULL;

	new_cookie_string = find_or_create_playlist(r, subpool);

	while (*args) {
		p = ap_getword(subpool, &args, '&');
		if (strncmp(p, "file=", 5))
			continue;
		else
			p+=5;
		
		ap_unescape_url(p);	/* browser will escape '=' */
		if (strstr(new_cookie_string, p) == NULL)
			new_cookie_string = apr_pstrcat(subpool, new_cookie_string, p, "&", NULL);
	}
	return new_cookie_string;
}

/**
 * Adds all files in a directory to the cookie holding the custom playlist.
 *
 * This function runs a make_music_entry on the local folder and adds to the
 * cookie all the files in this directory, and stores
 * their absolute unescaped URI base64 encoded.
 *
 * @param r the apache request
 * @param subpool the current subpool to use for mem allocations
 * @return a string to store in the cookie
 */
static const char *cookie_addall(request_rec *r, apr_pool_t *subpool)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const mu_ent	*custom = NULL;
	const char 	*new_cookie_string = NULL;
	char		*codeduri = NULL;
	mu_pack cookie_pack = {
		.head = NULL,
		.fhead = NULL,
		.dirnb = 0,
		.filenb = 0,
		.fsize = 0,
	};

	new_cookie_string = find_or_create_playlist(r, subpool);
	
	make_music_entry(r, subpool, &cookie_pack, NULL, MI_RECURSIVE);
	listsort(&cookie_pack, conf->order);	/* this sets cookie_pack.fhead */

	for (custom = cookie_pack.fhead; custom; custom = custom->next) {
		codeduri = (char *)realloc(codeduri, 1 + apr_base64_encode_len(strlen(custom->uri)));
		if (!codeduri)
			return NULL;
		apr_base64_encode(codeduri, custom->uri, strlen(custom->uri));
		
		if (strstr(new_cookie_string, codeduri) == NULL)
			new_cookie_string = apr_pstrcat(subpool, new_cookie_string, codeduri, "&", NULL);
	}

	free(codeduri);
	return new_cookie_string;
}

/**
 * Removes file(s) from the cookie holding the custom playlist.
 *
 * This function takes a list of files to remove to the custom playlist cookie in
 * the form of base64 uris stored in the conf->custom_list field, comming from
 * the POST arguments. It then copies the old base64 encoded URIs from
 * the existing cookie into a new one only if it can't find a match based on
 * the custom_list.
 *
 * @param r the apache request
 * @param subpool the current subpool to use for mem allocations
 * @return the new cookie string deprived of matching files
 */
static const char *cookie_remove(request_rec *r, apr_pool_t *subpool)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const char	*new_cookie_string = NULL, *p = NULL, *args;
	char		*rlist;

	args = find_playlist(r, subpool);

	if (!args)
		return NULL;
	
	args = strstr(args, "playlist=");
	
	if (!args)
		return NULL;
	args += 9;
	
	/* at this point, args contains the series of base64& existing list	
	   of files, and conf->custom_list contains the escaped base64 uris of
	   the files to remove */
	rlist = apr_pstrdup(subpool, conf->custom_list);
	ap_unescape_url(rlist);	/* browser will escape '=' etc */
	
	/* we are going to duplicate all but the selected files from the original
	   cookie to a newly created one */
	new_cookie_string = apr_pstrdup(subpool, "playlist=");

	/* If our token was found, copy from the client string
	to our cookie_header */
	while ((*args != '\0') && (*args != ';')) {
		p = ap_getword(subpool, &args, '&');	/* find the base64 string */
		
		/* if we don't find the base64 string in the remove list, we can copy it */
		if (strstr(rlist, p) == NULL)
			new_cookie_string = apr_pstrcat(subpool, new_cookie_string, p, "&", NULL);
	}

	return new_cookie_string;
}

void cookie_and_stream_work(request_rec *r)
{
	mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	const char *end_string = NULL, *args = NULL;
	apr_pool_t *subpool = NULL;

	/* Create a subpool we will clear sometimes. This is to save memory, as
	some operations are based on many ap_{palloc,strcat,strdup,...} for 
	playlists */
	apr_pool_create(&subpool, r->pool);

	if (subpool == NULL)
		subpool = r->pool;
	
	switch (conf->options & MI_ALLOPS) {
		case MI_COOKIEADDALL:
			args = cookie_addall(r, subpool);
			break;
		case MI_COOKIEADDLST:
			args = cookie_add(r, subpool);
			break;
		case MI_COOKIEDELLST:
			args = cookie_remove(r, subpool);
			break;
		case MI_COOKIEPURGE:
			args = apr_pstrdup(subpool, "playlist=");
			break;
		case MI_COOKIESTREAM:
			args = apr_pstrdup(subpool, "playlist=");
		default:
			conf->custom_list = NULL;
			args = find_playlist(r, subpool);
			break;
	}

	if (args) {
		end_string = apr_psprintf(subpool, "; Version=1; Max-Age=%d; Path=/",
			(args[9] == '\0') ? 0 : conf->cookie_life);
		conf->custom_list = apr_pstrcat(r->pool, args, end_string, NULL);
	}
	else
		conf->custom_list = NULL;

	/* We do not need the subpool any more */
	if (subpool != r->pool)
		apr_pool_destroy(subpool);
}
