/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/* Cherokee
 *
 * Authors:
 *      Alvaro Lopez Ortega <alvaro@alobbs.com>
 *
 * Copyright (C) 2001-2011 Alvaro Lopez Ortega
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

#include "common-internal.h"
#include "handler_dirlist.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/types.h>
#include <limits.h>

#ifdef HAVE_PWD_H
#  include <pwd.h>
#endif

#ifdef HAVE_GRP_H
#  include <grp.h>
#endif

#include "list.h"
#include "util.h"
#include "connection.h"
#include "connection-protected.h"
#include "server.h"
#include "server-protected.h"
#include "plugin_loader.h"
#include "icons.h"
#include "common.h"
#include "human_strcmp.h"
#include "match.h"

#define ICON_WEB_DIR_DEFAULT "/icons"
#define ENTRIES "handler,dirlist"


struct file_entry {
	cherokee_list_t   list_node;
	struct stat       stat;
	struct stat       rstat;
	cherokee_buffer_t realpath;
	cuint_t           name_len;
	struct dirent     info;          /* It *must* be the last entry */
};
typedef struct file_entry file_entry_t;

struct file_match {
	cherokee_list_t    list_node;
	cherokee_buffer_t  filename;
	cherokee_boolean_t is_wildcard;
};
typedef struct file_match file_match_t;


/* Plug-in initialization
 */
PLUGIN_INFO_HANDLER_EASIEST_INIT (dirlist, http_get | http_options);


/* Private type
 */
static ret_t
file_match_new (file_match_t **file)
{
	file_match_t *n;

	n = (file_match_t *) malloc (sizeof(file_match_t));
	if (unlikely(n == NULL)) {
		return ret_nomem;
	}

	INIT_LIST_HEAD (&n->list_node);
	cherokee_buffer_init (&n->filename);
	n->is_wildcard = false;

	*file = n;
	return ret_ok;
}

static void
file_match_free (file_match_t *file)
{
	if (file == NULL)
		return;

	cherokee_buffer_mrproper (&file->filename);
	free (file);
}


/* Private type
 */
static ret_t
file_entry_new (file_entry_t **file, cuint_t extra_size)
{
	file_entry_t *n;

	n = (file_entry_t *) malloc (sizeof(file_entry_t) + extra_size);
	if (unlikely(n == NULL)) {
		return ret_nomem;
	}

	INIT_LIST_HEAD (&n->list_node);
	cherokee_buffer_init (&n->realpath);

	*file = n;
	return ret_ok;
}

static void
file_entry_free (file_entry_t *file)
{
	cherokee_buffer_mrproper (&file->realpath);
	free (file);
}


/* Methods implementation
 */
static ret_t
load_theme_load_file (cherokee_buffer_t *theme_path, const char *file, cherokee_buffer_t *output)
{
	cherokee_buffer_t path = CHEROKEE_BUF_INIT;

	cherokee_buffer_add_buffer (&path, theme_path);
	cherokee_buffer_add (&path, file, strlen(file));

	cherokee_buffer_clean (output);
	cherokee_buffer_read_file (output, path.buf);

	cherokee_buffer_mrproper (&path);
	return ret_ok;
}


static ret_t
parse_if (cherokee_buffer_t *buf, const char *if_entry, size_t len_entry, cherokee_boolean_t show)
{
	char              *begin;
	char              *end;
	cherokee_buffer_t  token = CHEROKEE_BUF_INIT;

	cherokee_buffer_add_str (&token, "%if ");
	cherokee_buffer_add (&token, if_entry, len_entry);
	cherokee_buffer_add_str (&token, "%");

	begin = strstr (buf->buf, token.buf);
	if (begin == NULL)
		goto error;

	end = strstr (begin, "%fi%");
	if (end == NULL)
		goto error;

	if (show) {
		cherokee_buffer_remove_chunk (buf, end - buf->buf, 4);
		cherokee_buffer_remove_chunk (buf, begin - buf->buf, token.len);
	} else {
		cherokee_buffer_remove_chunk (buf, begin - buf->buf, (end+4) - begin);
	}

	cherokee_buffer_mrproper (&token);
	return ret_ok;
error:
	cherokee_buffer_mrproper (&token);
	return ret_error;
}


static ret_t
parse_macros_in_buffer (cherokee_buffer_t *buf, cherokee_handler_dirlist_props_t *props)
{
#define STR_SZ(str)	str, CSZLEN(str)

	parse_if (buf, STR_SZ("size"),  props->show_size);
	parse_if (buf, STR_SZ("date"),  props->show_date);
	parse_if (buf, STR_SZ("user"),  props->show_user);
	parse_if (buf, STR_SZ("group"), props->show_group);
	parse_if (buf, STR_SZ("icon"),  props->show_icons);

#undef STR_SZ
	return ret_ok;
}


static ret_t
load_theme (cherokee_buffer_t *theme_path, cherokee_handler_dirlist_props_t *props)
{
	load_theme_load_file (theme_path, "header.html", &props->header);
	load_theme_load_file (theme_path, "footer.html", &props->footer);
	load_theme_load_file (theme_path, "entry.html", &props->entry);
	load_theme_load_file (theme_path, "theme.css", &props->css);

	/* Check it
	 */
	if ((cherokee_buffer_is_empty (&props->header) ||
	     cherokee_buffer_is_empty (&props->entry) ||
	     cherokee_buffer_is_empty (&props->footer)))
	    return ret_error;

	/* Parse conditional blocks
	 */
	parse_macros_in_buffer (&props->header, props);
	parse_macros_in_buffer (&props->footer, props);
	parse_macros_in_buffer (&props->entry,  props);

	return ret_ok;
}


ret_t
cherokee_handler_dirlist_props_free  (cherokee_handler_dirlist_props_t *props)
{
	cherokee_list_t *i, *tmp;

	list_for_each_safe (i, tmp, &props->notice_files) {
		file_match_free ((file_match_t*)i);
	}

	list_for_each_safe (i, tmp, &props->hidden_files) {
		file_match_free ((file_match_t*)i);
	}

	cherokee_buffer_mrproper (&props->header);
	cherokee_buffer_mrproper (&props->footer);
	cherokee_buffer_mrproper (&props->entry);
	cherokee_buffer_mrproper (&props->css);
	cherokee_buffer_mrproper (&props->icon_web_dir);

	return cherokee_handler_props_free_base (HANDLER_PROPS(props));
}


static ret_t
file_match_add_cb (char *entry, void *data)
{
	ret_t            ret;
	file_match_t    *new_match = NULL;
	cherokee_list_t *list      = LIST(data);

	ret = file_match_new (&new_match);
	if (unlikely ((ret != ret_ok) || (new_match == NULL)))
		return ret_error;

	if ((strchr (entry, '*')) || (strchr (entry, '?')))
	{
		new_match->is_wildcard = true;
	}

	cherokee_buffer_add (&new_match->filename, entry, strlen(entry));

	TRACE(ENTRIES, "Match file entry: '%s' (wildcard: %s)\n",
	      new_match->filename.buf,
	      new_match->is_wildcard ? "yes" : "no");

	cherokee_list_add_tail (&new_match->list_node, list);
	return ret_ok;
}


ret_t
cherokee_handler_dirlist_configure (cherokee_config_node_t   *conf,
				    cherokee_server_t        *srv,
				    cherokee_module_props_t **_props)
{
	ret_t                             ret;
	cherokee_list_t                  *i;
	cherokee_handler_dirlist_props_t *props;
	const char                       *theme      = NULL;
	cherokee_buffer_t                 theme_path = CHEROKEE_BUF_INIT;

	UNUSED(srv);

	if (*_props == NULL) {
		CHEROKEE_NEW_STRUCT (n, handler_dirlist_props);

		cherokee_handler_props_init_base (HANDLER_PROPS(n),
			MODULE_PROPS_FREE(cherokee_handler_dirlist_props_free));

		n->show_size      = true;
		n->show_date      = true;
		n->show_user      = false;
		n->show_group     = false;
		n->show_icons     = true;
		n->show_symlinks  = true;
		n->redir_symlinks = false;

		n->show_hidden    = false;
		n->show_backup    = false;

		cherokee_buffer_init (&n->header);
		cherokee_buffer_init (&n->footer);
		cherokee_buffer_init (&n->entry);
		cherokee_buffer_init (&n->css);

		cherokee_buffer_init (&n->icon_web_dir);
		cherokee_buffer_add_str (&n->icon_web_dir, ICON_WEB_DIR_DEFAULT);

		INIT_LIST_HEAD (&n->notice_files);
		INIT_LIST_HEAD (&n->hidden_files);

		*_props = MODULE_PROPS(n);
	}

	props = PROP_DIRLIST(*_props);

	cherokee_config_node_foreach (i, conf) {
		cherokee_config_node_t *subconf = CONFIG_NODE(i);

		/* Convert the properties
		 */
		if (equal_buf_str (&subconf->key, "size")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_size);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "date")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_date);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "user")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_user);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "group")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_group);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "symlinks")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_symlinks);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "redir_symlinks")) {
			ret = cherokee_atob (subconf->val.buf, &props->redir_symlinks);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "hidden")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_hidden);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "backup")) {
			ret = cherokee_atob (subconf->val.buf, &props->show_backup);
			if (ret != ret_ok) return ret;

		} else if (equal_buf_str (&subconf->key, "theme")) {
			theme = subconf->val.buf;

		} else if (equal_buf_str (&subconf->key, "icon_dir")) {
			cherokee_buffer_clean (&props->icon_web_dir);
			cherokee_buffer_add_buffer (&props->icon_web_dir, &subconf->val);

		} else if (equal_buf_str (&subconf->key, "notice_files")) {
			ret = cherokee_config_node_read_list (subconf, NULL, file_match_add_cb, &props->notice_files);
			if (unlikely (ret != ret_ok))
				return ret;

		} else if (equal_buf_str (&subconf->key, "hidden_files")) {
			ret = cherokee_config_node_read_list (subconf, NULL, file_match_add_cb, &props->hidden_files);
			if (unlikely (ret != ret_ok))
				return ret;
		}
	}

	/* Load the theme
	 */
	if (theme == NULL)
		theme = "default";

	cherokee_buffer_add_buffer (&theme_path, &srv->themes_dir);
	cherokee_buffer_add_va     (&theme_path, "/%s/", theme);

	ret = load_theme (&theme_path, props);
	if (ret != ret_ok) {
		LOG_ERROR (CHEROKEE_ERROR_HANDLER_DIRLIST_THEME, theme, theme_path.buf);
	}
	cherokee_buffer_mrproper (&theme_path);
	return ret;
}


static cherokee_boolean_t
is_file_in_list (cherokee_list_t *list, char *filename, cuint_t len)
{
	ret_t            ret;
	cherokee_list_t *i;

	list_for_each (i, list) {
		file_match_t *file_match = ((file_match_t *)i);

		if (file_match->is_wildcard) {
			ret = cherokee_wildcard_match (file_match->filename.buf, filename);
			if (ret == ret_ok) {
				return true;
			}
			continue;
		}

		if (cherokee_buffer_cmp (&file_match->filename, filename, len) == 0) {
			return true;
		}
	}

	return false;
}


static ret_t
realpath_buf (cherokee_buffer_t *in,
	      cherokee_buffer_t *resolved)
{
	char *re;

	cherokee_buffer_ensure_size (resolved, PATH_MAX);

	re = realpath (in->buf, resolved->buf);
	if (re == NULL) {
		return ret_error;
	}

	resolved->len = strlen (resolved->buf);
	return ret_ok;
}


static ret_t
generate_file_entry (cherokee_handler_dirlist_t  *dhdl,
		     DIR                         *dir,
		     cherokee_buffer_t           *path,
		     cherokee_buffer_t           *local_realpath,
		     file_entry_t               **ret_entry)
{
	int            re;
	ret_t          ret;
	file_entry_t  *n;
	char          *name;
	cuint_t        extra;
	struct dirent *entry;

#ifdef _PC_NAME_MAX
	extra = pathconf(path->buf, _PC_NAME_MAX);
#else
	extra = PATH_MAX;
#endif

	/* New object
	 */
	ret = file_entry_new (&n, path->len + extra + 3);
	if (unlikely (ret != ret_ok)) {
		return ret;
	}

	for (;;) {
		/* Read a new directory entry
		 */
		cherokee_readdir (dir, &n->info, &entry);
		if (entry == NULL) {
			file_entry_free (n);
			return ret_eof;
		}

		name = (char *)entry->d_name;
		n->name_len = strlen(name);

		/* Skip some files
		 */
		if ((! HDL_DIRLIST_PROP(dhdl)->show_hidden) &&
		    (name[0] == '.'))
			continue;

		if ((! HDL_DIRLIST_PROP(dhdl)->show_backup) &&
		    ((name[0] == '#') || (name[n->name_len-1] == '~')))
			continue;

		if (is_file_in_list (&HDL_DIRLIST_PROP(dhdl)->notice_files, name, n->name_len) ||
		    is_file_in_list (&HDL_DIRLIST_PROP(dhdl)->hidden_files, name, n->name_len))
			continue;

		/* Build the local path, stat and clean
		 */
		cherokee_buffer_add (path, name, n->name_len);

		/* Check the filename size
		 */
		if (n->name_len > dhdl->longest_filename)
			dhdl->longest_filename = n->name_len;

		/* Path
		 */
		re = cherokee_lstat (path->buf, &n->stat);
		if (re < 0) {
			cherokee_buffer_drop_ending (path, n->name_len);

			file_entry_free (n);
			return ret_error;
		}

	        if (S_ISLNK(n->stat.st_mode)) {
			/* Info about the (target) linked file
			 */
			ret = cherokee_stat (path->buf, &n->rstat);
			if (ret != ret_ok) {
				/* Broken link */
				memcpy (&n->rstat, &n->stat, sizeof(struct stat));
			}

			if (HDL_DIRLIST_PROP(dhdl)->redir_symlinks) {
				/* The local directory realpath is build lazily
				 */
				if (cherokee_buffer_is_empty (local_realpath))
				{
					cherokee_buffer_drop_ending (path, n->name_len);

					ret = realpath_buf (path, local_realpath);
					if (unlikely (ret != ret_ok)) {
						file_entry_free (n);
						return ret_error;
					}

					cherokee_buffer_add (path, name, n->name_len);
				}

				/* Read the real path of the entry
				 */
				ret = realpath_buf (path, &n->realpath);
				if (unlikely (ret != ret_ok)) {
					cherokee_buffer_drop_ending (path, n->name_len);

					file_entry_free (n);
					return ret_error;
				}

				/* Check it is within the limits
				 */
				re = strncmp (n->realpath.buf, local_realpath->buf, local_realpath->len);
				if (re != 0) {
					cherokee_buffer_drop_ending (path, n->name_len);
					file_entry_free (n);
					return ret_error;
				}

				cherokee_buffer_move_to_begin (&n->realpath, local_realpath->len +1);
			}
		}

		/* Clean up and exit
		 */
		cherokee_buffer_drop_ending (path, n->name_len);

		*ret_entry = n;
		return ret_ok;
	}

	return ret_eof;
}


ret_t
cherokee_handler_dirlist_new (cherokee_handler_t **hdl, void *cnt, cherokee_module_props_t *props)
{
	ret_t              ret;
	cherokee_buffer_t *value;
	CHEROKEE_NEW_STRUCT (n, handler_dirlist);

	TRACE_CONN(cnt);

	/* Init the base class object
	 */
	cherokee_handler_init_base (HANDLER(n), cnt, HANDLER_PROPS(props), PLUGIN_INFO_HANDLER_PTR(dirlist));

	MODULE(n)->init         = (handler_func_init_t) cherokee_handler_dirlist_init;
	MODULE(n)->free         = (module_func_free_t) cherokee_handler_dirlist_free;
	HANDLER(n)->step        = (handler_func_step_t) cherokee_handler_dirlist_step;
	HANDLER(n)->add_headers = (handler_func_add_headers_t) cherokee_handler_dirlist_add_headers;

	/* Supported features
	 */
	HANDLER(n)->support     = hsupport_nothing;

	/* Process the request_string, and build the arguments table..
	 */
	cherokee_connection_parse_args (cnt);

	/* Init
	 */
	INIT_LIST_HEAD (&n->files);
	INIT_LIST_HEAD (&n->dirs);

	/* State
	 */
	n->dir_ptr          = NULL;
	n->file_ptr         = NULL;
	n->longest_filename = 0;

	/* Check if icons can be used
	 */
	if (HDL_DIRLIST_PROP(n)->show_icons) {
		HDL_DIRLIST_PROP(n)->show_icons = (HANDLER_SRV(n)->icons != NULL);
	}

	/* Choose the sorting key
	 */
	n->phase = dirlist_phase_add_header;
	n->sort  = Name_Down;

	ret = cherokee_avl_get_ptr (HANDLER_CONN(n)->arguments, "order", (void **) &value);
	if (ret == ret_ok) {
		if      (value->buf[0] == 'N') n->sort = Name_Up;
		else if (value->buf[0] == 'n') n->sort = Name_Down;
		else if (value->buf[0] == 'D') n->sort = Date_Up;
		else if (value->buf[0] == 'd') n->sort = Date_Down;
		else if (value->buf[0] == 'S') n->sort = Size_Up;
		else if (value->buf[0] == 's') n->sort = Size_Down;
	}

	/* Properties
	 */
	cherokee_buffer_init (&n->header);
	cherokee_buffer_init (&n->public_dir);

	/* Check the theme
	 */
	if (cherokee_buffer_is_empty (&HDL_DIRLIST_PROP(n)->entry)  ||
	    cherokee_buffer_is_empty (&HDL_DIRLIST_PROP(n)->header) ||
	    cherokee_buffer_is_empty (&HDL_DIRLIST_PROP(n)->footer))
	{
		LOG_CRITICAL_S (CHEROKEE_ERROR_HANDLER_DIRLIST_BAD_THEME);
		cherokee_handler_free (n);
		return ret_error;
	}

	*hdl = HANDLER(n);
	return ret_ok;
}


ret_t
cherokee_handler_dirlist_free (cherokee_handler_dirlist_t *dhdl)
{
	cherokee_list_t *i, *tmp;

	cherokee_buffer_mrproper (&dhdl->header);
	cherokee_buffer_mrproper (&dhdl->public_dir);

	list_for_each_safe (i, tmp, &dhdl->dirs) {
		cherokee_list_del (i);
		file_entry_free ((file_entry_t *)i);
	}

	list_for_each_safe (i, tmp, &dhdl->files) {
		cherokee_list_del (i);
		file_entry_free ((file_entry_t *)i);
	}

	return ret_ok;
}


static ret_t
check_request_finish_with_slash (cherokee_handler_dirlist_t *dhdl)
{
	cherokee_connection_t *conn = HANDLER_CONN(dhdl);

	if ((cherokee_buffer_is_empty (&conn->request)) ||
	    (!cherokee_buffer_is_ending (&conn->request, '/')))
	{
		cherokee_buffer_add_str (&conn->request, "/");
		cherokee_connection_set_redirect (conn, &conn->request);
		cherokee_buffer_drop_ending (&conn->request, 1);

		conn->error_code = http_moved_permanently;
		return ret_error;
	}

	return ret_ok;
}


static int
cmp_name_down (cherokee_list_t *a, cherokee_list_t *b)
{
	file_entry_t *f1 = (file_entry_t *)a;
	file_entry_t *f2 = (file_entry_t *)b;

	return cherokee_human_strcmp ((const char *) &f1->info.d_name,
				      (const char *) &f2->info.d_name);
}


static int
cmp_size_down (cherokee_list_t *a, cherokee_list_t *b)
{
	file_entry_t *f1 = (file_entry_t *)a;
	file_entry_t *f2 = (file_entry_t *)b;

	if (f1->stat.st_size > f2->stat.st_size)
		return 1;

	if (f1->stat.st_size < f2->stat.st_size)
		return -1;

	return cmp_name_down (a,b);
}


static int
cmp_date_down (cherokee_list_t *a, cherokee_list_t *b)
{
	file_entry_t *f1 = (file_entry_t *)a;
	file_entry_t *f2 = (file_entry_t *)b;

	if (f1->stat.st_mtime > f2->stat.st_mtime)
		return 1;

	if (f1->stat.st_mtime < f2->stat.st_mtime)
		return -1;

	return cmp_name_down (a,b);
}


static int
cmp_name_up (cherokee_list_t *a, cherokee_list_t *b)
{
	return -cmp_name_down(a,b);
}


static int
cmp_size_up (cherokee_list_t *a, cherokee_list_t *b)
{
	return -cmp_size_down(a,b);
}


static int
cmp_date_up (cherokee_list_t *a, cherokee_list_t *b)
{
	return -cmp_date_down(a,b);
}


static void
list_sort_by_type (cherokee_list_t *list, cherokee_dirlist_sort_t sort)
{
	switch (sort) {
	case Name_Down:
		cherokee_list_sort (list, cmp_name_down);
		break;
	case Name_Up:
		cherokee_list_sort (list, cmp_name_up);
		break;
	case Size_Down:
		cherokee_list_sort (list, cmp_size_down);
		break;
	case Size_Up:
		cherokee_list_sort (list, cmp_size_up);
		break;
	case Date_Down:
		cherokee_list_sort (list, cmp_date_down);
		break;
	case Date_Up:
		cherokee_list_sort (list, cmp_date_up);
		break;
	}
}


static ret_t
build_file_list (cherokee_handler_dirlist_t *dhdl)
{
	DIR                   *dir;
	file_entry_t          *item;
	int                    is_dir;
	int                    is_link;
	cherokee_connection_t *conn           = HANDLER_CONN(dhdl);
	cherokee_buffer_t      local_realpath = CHEROKEE_BUF_INIT;

	/* Build the local directory path
	 */
	cherokee_buffer_add_buffer (&conn->local_directory, &conn->request);     /* 1 */

	dir = cherokee_opendir (conn->local_directory.buf);
	if (dir == NULL) {
		conn->error_code = http_not_found;
		return ret_error;
	}

	/* Read the files
	 */
	for (;;) {
		ret_t ret;

		ret = generate_file_entry (dhdl, dir, &conn->local_directory, &local_realpath, &item);
		if (ret == ret_eof)
			break;
		if ((ret == ret_nomem) ||
		    (ret == ret_error))
			continue;

		is_link = S_ISLNK(item->stat.st_mode);
		if (is_link) {
			is_dir = S_ISDIR(item->rstat.st_mode);
		} else {
			is_dir = S_ISDIR(item->stat.st_mode);
		}

		if (is_dir) {
			cherokee_list_add (LIST(item), &dhdl->dirs);
		} else {
			cherokee_list_add (LIST(item), &dhdl->files);
		}
	}

	/* Clean
	 */
	cherokee_closedir(dir);
	cherokee_buffer_drop_ending (&conn->local_directory, conn->request.len); /* 2 */

	/* Free local_realpath. It might have been built lazily,
	 * inside the generate_file_entry() function.
	 */
	cherokee_buffer_mrproper (&local_realpath);

	/* Sort the file list
	 */
	if (! cherokee_list_empty(&dhdl->files)) {
		list_sort_by_type (&dhdl->files, dhdl->sort);
		dhdl->file_ptr = dhdl->files.next;
	}

	/* Sort the directories list:
	 * it doesn't make sense to look for the size
	 */
	if (! cherokee_list_empty (&dhdl->dirs)) {
		cherokee_dirlist_sort_t sort = dhdl->sort;

		if (sort == Size_Down) sort = Name_Down;
		if (sort == Size_Up)   sort = Name_Up;

		list_sort_by_type (&dhdl->dirs, sort);
		dhdl->dir_ptr = dhdl->dirs.next;
	}

	return ret_ok;
}


static ret_t
build_public_path (cherokee_handler_dirlist_t *dhdl, cherokee_buffer_t *buf)
{
	cherokee_connection_t *conn = HANDLER_CONN(dhdl);

	if (!cherokee_buffer_is_empty (&conn->userdir)) {
		/* ~user local dir request
 		 */
		cherokee_buffer_add_str (buf, "/~");
		cherokee_buffer_add_buffer (buf, &conn->userdir);
	}

	if (cherokee_buffer_is_empty (&conn->request_original))
		cherokee_buffer_add_buffer (buf, &conn->request);
	else
		cherokee_buffer_add_buffer (buf, &conn->request_original);

	return ret_ok;
}


static ret_t
read_notice_file (cherokee_handler_dirlist_t *dhdl)
{
	ret_t                  ret;
	cherokee_list_t       *i;
	cherokee_connection_t *conn = HANDLER_CONN(dhdl);

	list_for_each (i, &HDL_DIRLIST_PROP(dhdl)->notice_files) {
		file_match_t      *file_match = ((file_match_t*)(i));
		cherokee_buffer_t *filename   = &file_match->filename;

		cherokee_buffer_clean (&dhdl->header);

		if (filename->buf[0] != '/') {
			cherokee_buffer_add_buffer (&conn->local_directory, &conn->request);                     /* do   */
			cherokee_buffer_add_buffer (&conn->local_directory, filename);

			ret = cherokee_buffer_read_file (&dhdl->header, conn->local_directory.buf);
			cherokee_buffer_drop_ending (&conn->local_directory, conn->request.len + filename->len); /* undo */
		} else {
			ret = cherokee_buffer_read_file (&dhdl->header, filename->buf);
		}

		if (ret == ret_ok)
			break;
	}

	return ret_ok;
}


ret_t
cherokee_handler_dirlist_init (cherokee_handler_dirlist_t *dhdl)
{
	ret_t ret;

	/* The request must end with a slash..
	 */
	ret = check_request_finish_with_slash (dhdl);
	if (ret != ret_ok)
		return ret;

	/* OPTIONS request: no need to build the file list
	 */
	if (HANDLER_CONN(dhdl)->header.method == http_options) {
		return ret_ok;
	}

	/* Read the Notice file
	 */
	if (! cherokee_list_empty (&HDL_DIRLIST_PROP(dhdl)->notice_files)) {
		ret = read_notice_file (dhdl);
		if (ret != ret_ok)
			return ret;
	}

	/* Build de local request
	 */
	ret = build_file_list (dhdl);
	if (unlikely(ret < ret_ok))
		return ret;

	/* Build public dir string
	 */
	ret = build_public_path (dhdl, &dhdl->public_dir);
	if (unlikely (ret != ret_ok))
		return ret;

 	return ret_ok;
}


/* Substitute all instances of (token) found in vbuf[*pidx_buf];
 * if at least one (token) instance is found, then contents
 * resulting from substitution(s) are copied to the buffer
 * in next position of (vbuf) and (*pidx_buf) is updated to point
 * to current content buffer.
 * NOTE: only the first 2 positions in vbuf are used
 *       (flip/flop algorithm).
 */
static ret_t
substitute_vbuf_token (cherokee_buffer_t **vbuf,
		       size_t             *pidx_buf,
		       const char         *token,
		       int                 token_len,
		       const char         *replacement)
{
	ret_t ret;

	if (replacement == NULL)
		replacement = "";

	/* Substitute all instances of token in vbuf[*pidx_buf],
	 * if everything goes well then result is copied in next index position
	 * of vbuf[] and in this case we can increment the index position.
	 * NOTE: *pidx_buf ^= 1 is faster than *pidx_buf = (*pidx_buf + 1) % 2
	 */
	ret = cherokee_buffer_substitute_string (vbuf[*pidx_buf],
						 vbuf[*pidx_buf ^ 1],
						 (char *)token, token_len,
						 (char *)replacement, strlen(replacement));
	if (ret == ret_ok)
		*pidx_buf ^= 1;

	return ret;
}


/* Useful macros to declare, to initialize and to handle an array of
 * two flip/flop temporary buffers (used for substitution purposes).
 */

#define VTMP_INIT_SUBST(thread, vtmp, buffer_pattern) \
	vtmp[0] = THREAD_TMP_BUF1(thread);	      \
	vtmp[1] = THREAD_TMP_BUF2(thread);	      \
	cherokee_buffer_clean (vtmp[0]);              \
	cherokee_buffer_clean (vtmp[1]);	      \
	cherokee_buffer_add_buffer (vtmp[0], (buffer_pattern))

#define VTMP_SUBSTITUTE_TOKEN(token, val)             \
	substitute_vbuf_token (vtmp, &idx_tmp, (token), sizeof(token)-1, (val))


static ret_t
render_file (cherokee_handler_dirlist_t *dhdl, cherokee_buffer_t *buffer, file_entry_t *file)
{
	ret_t                             ret;
	cherokee_buffer_t                *vtmp[2];
	cherokee_buffer_t                 name_buf;
	int                               is_dir;
	int                               is_link;
	const char                       *alt      = NULL;
	cherokee_buffer_t                *icon     = NULL;
	const char                       *name     = (char *) &file->info.d_name;
	cherokee_icons_t                 *icons    = HANDLER_SRV(dhdl)->icons;
	cherokee_buffer_t                *tmp      = &dhdl->header;
	cherokee_handler_dirlist_props_t *props    = HDL_DIRLIST_PROP(dhdl);
	cherokee_thread_t                *thread   = HANDLER_THREAD(dhdl);
	size_t                            idx_tmp  = 0;

	/* Initialize temporary substitution buffers
	 */
	VTMP_INIT_SUBST (thread, vtmp, &props->entry);

	is_link = S_ISLNK(file->stat.st_mode);
	if (is_link) {
		is_dir = S_ISDIR(file->rstat.st_mode);
	} else {
		is_dir = S_ISDIR(file->stat.st_mode);
	}

	/* Check whether it is a symlink that we should skip
	 */
	if (is_link) {
		if (! props->show_symlinks) {
			return ret_not_found;
		}
	}

	/* Name buffer
	 */
	cherokee_buffer_fake (&name_buf, name, strlen(name));

	/* Add the icon
	 */
	if (props->show_icons) {
		if (is_dir) {
			icon = &icons->directory_icon;
		} else {
			ret = cherokee_icons_get_icon (icons, &name_buf, &icon);
			if (ret != ret_ok)
				return ret;
		}
	}

	/* Icon's 'alt'
	 */
	alt = (is_dir) ? "[DIR]" : "[   ]";
	VTMP_SUBSTITUTE_TOKEN ("%icon_alt%", alt);
	VTMP_SUBSTITUTE_TOKEN ("%icon_dir%", props->icon_web_dir.buf);

	if (icon) {
		cherokee_buffer_clean (tmp);
		cherokee_buffer_add_buffer (tmp, &props->icon_web_dir);
		cherokee_buffer_add_char (tmp, '/');
		cherokee_buffer_add_buffer (tmp, icon);
		VTMP_SUBSTITUTE_TOKEN ("%icon%", tmp->buf);
	} else {
		VTMP_SUBSTITUTE_TOKEN ("%icon%", NULL);
	}

	/* File
	 */
	cherokee_buffer_clean (tmp);
	cherokee_buffer_add_escape_html (tmp, &name_buf);
	VTMP_SUBSTITUTE_TOKEN ("%file_name%", tmp->buf);

	if ((is_link) && (props->redir_symlinks)) {
		if (file->realpath.len <= 0) {
			return ret_not_found;
		}

		cherokee_buffer_clean (tmp);
		cherokee_buffer_escape_uri_delims (tmp, &file->realpath);
		VTMP_SUBSTITUTE_TOKEN ("%file_link%", tmp->buf);

	} else if (! is_dir) {
		cherokee_buffer_clean (tmp);
		cherokee_buffer_escape_uri_delims (tmp, &name_buf);
		VTMP_SUBSTITUTE_TOKEN ("%file_link%", tmp->buf);

	} else {
		cherokee_buffer_clean (tmp);
		cherokee_buffer_escape_uri_delims (tmp, &name_buf);
		cherokee_buffer_add_str (tmp, "/");
		VTMP_SUBSTITUTE_TOKEN ("%file_link%", tmp->buf);
	}

	/* Date
	 */
	if (props->show_date) {
		struct tm *ltime;
		struct tm  ltime_buf;

		cherokee_buffer_clean (tmp);
		cherokee_buffer_ensure_size (tmp, 33);

		ltime = cherokee_localtime (&file->stat.st_mtime, &ltime_buf);
		if (ltime != NULL) {
			strftime (tmp->buf, 32, "%d-%b-%Y %H:%M", ltime);
		}

		VTMP_SUBSTITUTE_TOKEN ("%date%", tmp->buf);
	}

	/* Size
	 */
	if (props->show_size) {
		if (is_link) {
			VTMP_SUBSTITUTE_TOKEN ("%size_unit%", NULL);
			VTMP_SUBSTITUTE_TOKEN ("%size%", "link");
		} else if (is_dir) {
			VTMP_SUBSTITUTE_TOKEN ("%size_unit%", NULL);
			VTMP_SUBSTITUTE_TOKEN ("%size%", "-");
		} else {
			char *unit;

			cherokee_buffer_clean (tmp);
			cherokee_buffer_ensure_size (tmp, 8);
			cherokee_buffer_add_fsize (tmp, file->stat.st_size);

			unit = tmp->buf;
			while ((*unit >= '0')  && (*unit <= '9')) unit++;

			VTMP_SUBSTITUTE_TOKEN ("%size_unit%", unit);
			*unit = '\0';
			VTMP_SUBSTITUTE_TOKEN ("%size%", tmp->buf);
		}
	}

	/* User
	 */
	if (props->show_user) {
		struct passwd *user;
		const char    *name;

		user = getpwuid (file->stat.st_uid);
		name = (char *) (user->pw_name) ? user->pw_name : "unknown";

		VTMP_SUBSTITUTE_TOKEN ("%user%", name);
	}

	/* Group
	 */
	if (props->show_group) {
		struct group *user;
		const char   *group;

		user = getgrgid (file->stat.st_gid);
		group = (char *) (user->gr_name) ? user->gr_name : "unknown";

		VTMP_SUBSTITUTE_TOKEN ("%group%", group);
	}

	/* Add final result to buffer
	 */
	cherokee_buffer_add_buffer (buffer, vtmp[idx_tmp]);

	return ret_ok;
}


static ret_t
render_parent_directory (cherokee_handler_dirlist_t *dhdl, cherokee_buffer_t *buffer)
{
	cherokee_buffer_t                *vtmp[2];
	cherokee_buffer_t                *icon     = NULL;
	cherokee_icons_t                 *icons    = HANDLER_SRV(dhdl)->icons;
	cherokee_handler_dirlist_props_t *props    = HDL_DIRLIST_PROP(dhdl);
 	cherokee_thread_t                *thread   = HANDLER_THREAD(dhdl);
	cherokee_buffer_t                *tmp      = &dhdl->header;
	size_t                            idx_tmp  = 0;

	/* Initialize temporary substitution buffers
	 */
	VTMP_INIT_SUBST (thread, vtmp, &props->entry);

	if (props->show_icons) {
		icon = &icons->parentdir_icon;
	}

	if (icon) {
		cherokee_buffer_clean (tmp);
		cherokee_buffer_add_buffer (tmp, &props->icon_web_dir);
		cherokee_buffer_add_char (tmp, '/');
		cherokee_buffer_add_buffer (tmp, icon);
		VTMP_SUBSTITUTE_TOKEN ("%icon%", tmp->buf);
	} else {
		VTMP_SUBSTITUTE_TOKEN ("%icon%", NULL);
	}

	VTMP_SUBSTITUTE_TOKEN ("%icon_alt%", "[DIR]");
	VTMP_SUBSTITUTE_TOKEN ("%icon_dir%", props->icon_web_dir.buf);

	VTMP_SUBSTITUTE_TOKEN ("%file_link%", "../");
	VTMP_SUBSTITUTE_TOKEN ("%file_name%", "Parent Directory");

	VTMP_SUBSTITUTE_TOKEN ("%date%", NULL);
	VTMP_SUBSTITUTE_TOKEN ("%size_unit%", NULL);
	VTMP_SUBSTITUTE_TOKEN ("%size%", "-");
	VTMP_SUBSTITUTE_TOKEN ("%user%", NULL);
	VTMP_SUBSTITUTE_TOKEN ("%group%", NULL);

	/* Add final result to buffer
	 */
	cherokee_buffer_add_buffer (buffer, vtmp[idx_tmp]);

	return ret_ok;
}


static ret_t
render_header_footer_vbles (cherokee_handler_dirlist_t *dhdl,
			    cherokee_buffer_t          *buffer,
			    cherokee_buffer_t          *buf_pattern)
{
	cherokee_buffer_t                *vtmp[2];
	cherokee_thread_t                *thread   = HANDLER_THREAD(dhdl);
	size_t                            idx_tmp  = 0;
	cherokee_bind_t                  *bind     = CONN_BIND(HANDLER_CONN(dhdl));
	cherokee_handler_dirlist_props_t *props    = HDL_DIRLIST_PROP(dhdl);

	/* Initialize temporary substitution buffers
	 */
	VTMP_INIT_SUBST (thread, vtmp, buf_pattern);

	/* Replacements
	 */
	VTMP_SUBSTITUTE_TOKEN ("%public_dir%",      dhdl->public_dir.buf);
	VTMP_SUBSTITUTE_TOKEN ("%server_software%", bind->server_string_w_port.buf);
	VTMP_SUBSTITUTE_TOKEN ("%notice%",          dhdl->header.buf);
	VTMP_SUBSTITUTE_TOKEN ("%icon_dir%",        props->icon_web_dir.buf);

	/* Orders
	 */
	VTMP_SUBSTITUTE_TOKEN ("%order_name%", (dhdl->sort == Name_Down) ? "N" : "n");
	VTMP_SUBSTITUTE_TOKEN ("%order_size%", (dhdl->sort == Size_Down) ? "S" : "s");
	VTMP_SUBSTITUTE_TOKEN ("%order_date%", (dhdl->sort == Date_Down) ? "D" : "d");

	/* Add final result to buffer
	 */
	cherokee_buffer_add_buffer (buffer, vtmp[idx_tmp]);

	return ret_ok;
}


ret_t
cherokee_handler_dirlist_step (cherokee_handler_dirlist_t *dhdl,
			       cherokee_buffer_t          *buffer)
{
	ret_t                             ret = ret_ok;
	cherokee_handler_dirlist_props_t *props = HDL_DIRLIST_PROP(dhdl);

	/* OPTIONS request
	 */
	if (HANDLER_CONN(dhdl)->header.method == http_options) {
		return ret_eof;
	}

	/* GET request
	 */
	switch (dhdl->phase) {
	case dirlist_phase_add_header:
		/* Add the theme header
		 */
		ret = render_header_footer_vbles (dhdl, buffer, &props->header);
		if (unlikely (ret != ret_ok)) return ret;
		if (buffer->len > DEFAULT_READ_SIZE)
			return ret_ok;
		dhdl->phase = dirlist_phase_add_parent_dir;

	case dirlist_phase_add_parent_dir:
		ret = render_parent_directory (dhdl, buffer);
		if (unlikely (ret != ret_ok)) return ret;
		dhdl->phase = dirlist_phase_add_entries;

	case dirlist_phase_add_entries:
		/* Print the directories first
		 */
		while (dhdl->dir_ptr) {
			if (dhdl->dir_ptr == &dhdl->dirs) {
				dhdl->dir_ptr = NULL;
				break;
			}
			render_file (dhdl, buffer, (file_entry_t *)dhdl->dir_ptr);
			dhdl->dir_ptr = dhdl->dir_ptr->next;

			/* Maybe it has read enough data
			 */
			if (buffer->len > DEFAULT_READ_SIZE)
				return ret_ok;
		}

		/* then the file list
		 */
		while (dhdl->file_ptr) {
			if (dhdl->file_ptr == &dhdl->files) {
				dhdl->file_ptr = NULL;
				break;
			}
			render_file (dhdl, buffer, (file_entry_t *) dhdl->file_ptr);
			dhdl->file_ptr = dhdl->file_ptr->next;

			/* Maybe it has read enough data
			 */
			if (buffer->len > DEFAULT_READ_SIZE)
				return ret_ok;
		}
		dhdl->phase = dirlist_phase_add_footer;

	case dirlist_phase_add_footer:
		/* Add the theme footer
		 */
		ret = render_header_footer_vbles (dhdl, buffer, &props->footer);
		if (unlikely (ret != ret_ok)) return ret;

		dhdl->phase = dirlist_phase_finished;
		return ret_eof_have_data;

	case dirlist_phase_finished:
		break;
	}

	return ret_eof;
}


ret_t
cherokee_handler_dirlist_add_headers (cherokee_handler_dirlist_t *dhdl,
				      cherokee_buffer_t          *buffer)
{
	UNUSED(dhdl);

	/* OPTIONS request
	 */
	if (HANDLER_CONN(dhdl)->header.method == http_options) {
		cherokee_buffer_add_str (buffer, "Content-Length: 0"CRLF);
		cherokee_handler_add_header_options (HANDLER(dhdl), buffer);
		return ret_ok;
	}

	/* GET request
	 */
	cherokee_buffer_add_str (buffer, "Content-Type: text/html; charset=utf-8"CRLF);
	return ret_ok;
}
