/*
 * Copyright (C) 2008 Martin Willi
 * Copyright (C) 2008 Philip Boetschi, Adrian Doerig
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE
#include <string.h>

#include "user_controller.h"

#include <library.h>

typedef struct private_user_controller_t private_user_controller_t;

/**
 * private data of the user_controller
 */
struct private_user_controller_t {

	/**
	 * public functions
	 */
	user_controller_t public;

	/**
	 * database connection
	 */
	database_t *db;

	/**
	 * user session
	 */
	user_t *user;

	/**
	 * minimum required password length
	 */
	u_int password_length;
};

/**
 * hash the password for database storage
 */
static chunk_t hash_password(char *login, char *password)
{
	hasher_t *hasher;
	chunk_t hash, data;

	hasher = lib->crypto->create_hasher(lib->crypto, HASH_SHA1);
	if (!hasher)
	{
		return chunk_empty;
	}
	data = chunk_cata("cc",	chunk_create(login, strlen(login)),
							chunk_create(password, strlen(password)));
	if (!hasher->allocate_hash(hasher, data, &hash))
	{
		hasher->destroy(hasher);
		return chunk_empty;
	}
	hasher->destroy(hasher);
	return hash;
}

/**
 * Login a user.
 */
static void login(private_user_controller_t *this, fast_request_t *request)
{
	if (request->get_query_data(request, "submit"))
	{
		char *login, *password;

		login = request->get_query_data(request, "login");
		password = request->get_query_data(request, "password");

		if (login && password)
		{
			enumerator_t *query;
			u_int id = 0;
			chunk_t hash;

			hash = hash_password(login, password);
			query = this->db->query(this->db,
						"SELECT id FROM user WHERE login = ? AND password = ?",
						DB_TEXT, login, DB_BLOB, hash, DB_UINT);
			if (query)
			{
				query->enumerate(query, &id);
				query->destroy(query);
			}
			free(hash.ptr);
			if (id)
			{
				this->user->set_user(this->user, id);
				return request->redirect(request, "peer/list");
			}
		}
		request->setf(request, "error=Invalid username or password.");
	}
	request->render(request, "templates/user/login.cs");
}

/**
 * Logout a user.
 */
static void logout(private_user_controller_t *this, fast_request_t *request)
{
	request->redirect(request, "user/login");
	request->close_session(request);
}

/**
 * verify a user entered username for validity
 */
static bool verify_login(private_user_controller_t *this,
						 fast_request_t *request, char *login)
{
	if (!login || *login == '\0')
	{
		request->setf(request, "error=Username is missing.");
		return FALSE;
	}
	while (*login != '\0')
	{
		switch (*login)
		{
			case 'a' ... 'z':
			case 'A' ... 'Z':
			case '0' ... '9':
			case '-':
			case '_':
			case '@':
			case '.':
				login++;
				continue;
			default:
				request->setf(request, "error=Username invalid, "
							  "valid characters: A-Z a-z 0-9 - _ @ .");
		}
	}
	return TRUE;
}

/**
 * verify a user entered password for validity
 */
static bool verify_password(private_user_controller_t *this,
							fast_request_t *request,
							char *password, char *confirm)
{
	if (!password || *password == '\0')
	{
		request->setf(request, "error=Password is missing.");
		return FALSE;
	}
	if (strlen(password) < this->password_length)
	{
		request->setf(request, "error=Password requires at least %d characters.",
					  this->password_length);
		return FALSE;
	}
	if (!confirm || !streq(password, confirm))
	{
		request->setf(request, "error=Password not confirmed.");
		return FALSE;
	}
	return TRUE;
}

/**
 * Register a user.
 */
static void add(private_user_controller_t *this, fast_request_t *request)
{
	char *login = "";

	while (request->get_query_data(request, "register"))
	{
		char *password, *confirm;
		chunk_t hash;
		u_int id;

		login = request->get_query_data(request, "new_login");
		password = request->get_query_data(request, "new_password");
		confirm = request->get_query_data(request, "confirm_password");

		if (!verify_login(this, request, login) ||
			!verify_password(this, request, password, confirm))
		{
			break;
		}

		hash = hash_password(login, password);
		if (!hash.ptr || this->db->execute(this->db, &id,
							"INSERT INTO user (login, password) VALUES (?, ?)",
							DB_TEXT, login, DB_BLOB, hash) < 0)
		{
			request->setf(request, "error=Username already exists.");
			free(hash.ptr);
			break;
		}
		free(hash.ptr);
		this->user->set_user(this->user, id);
		return request->redirect(request, "peer/list");
	}
	request->set(request, "new_login", login);
	request->setf(request, "password_length=%d", this->password_length);
	request->render(request, "templates/user/add.cs");
}

/**
 * Edit the logged in user
 */
static void edit(private_user_controller_t *this, fast_request_t *request)
{
	enumerator_t *query;
	char *old_login;

	/* lookup old login */
	query = this->db->query(this->db, "SELECT login FROM user WHERE id = ?",
							DB_INT, this->user->get_user(this->user),
							DB_TEXT);
	if (!query || !query->enumerate(query, &old_login))
	{
		DESTROY_IF(query);
		request->close_session(request);
		return request->redirect(request, "user/login");
	}
	old_login = strdupa(old_login);
	query->destroy(query);

	/* back pressed */
	if (request->get_query_data(request, "back"))
	{
		return request->redirect(request, "peer/list");
	}
	/* delete pressed */
	if (request->get_query_data(request, "delete"))
	{
		this->db->execute(this->db, NULL, "DELETE FROM user WHERE id = ?",
						  DB_UINT, this->user->get_user(this->user));
		this->db->execute(this->db, NULL,
						  "DELETE FROM peer WHERE user = ?",
						  DB_UINT, this->user->get_user(this->user));
		return logout(this, request);
	}
	/* save pressed */
	while (request->get_query_data(request, "save"))
	{
		char *new_login, *old_pass, *new_pass, *confirm;
		chunk_t old_hash, new_hash;

		new_login = request->get_query_data(request, "old_login");
		old_pass = request->get_query_data(request, "old_password");
		new_pass = request->get_query_data(request, "new_password");
		confirm = request->get_query_data(request, "confirm_password");

		if (!verify_login(this, request, new_login) ||
			!verify_password(this, request, new_pass, confirm))
		{
			old_login = new_login;
			break;
		}
		old_hash = hash_password(old_login, old_pass);
		new_hash = hash_password(new_login, new_pass);

		if (this->db->execute(this->db, NULL,
			"UPDATE user SET login = ?, password = ? "
			"WHERE id = ? AND password = ?",
			DB_TEXT, new_login, DB_BLOB, new_hash,
			DB_UINT, this->user->get_user(this->user), DB_BLOB, old_hash) <= 0)
		{
			free(new_hash.ptr);
			free(old_hash.ptr);
			old_login = new_login;
			request->setf(request, "error=Password verification failed.");
			break;
		}
		free(new_hash.ptr);
		free(old_hash.ptr);
		return request->redirect(request, "peer/list");
	}
	/* on error/template rendering */
	request->set(request, "old_login", old_login);
	request->setf(request, "password_length=%d", this->password_length);
	request->render(request, "templates/user/edit.cs");
}

METHOD(fast_controller_t, get_name, char*,
	private_user_controller_t *this)
{
	return "user";
}

METHOD(fast_controller_t, handle, void,
	private_user_controller_t *this, fast_request_t *request, char *action,
	char *p2, char *p3, char *p4, char *p5)
{
	if (action)
	{
		if (streq(action, "add"))
		{
			return add(this, request);
		}
		if (streq(action, "login"))
		{
			return login(this, request);
		}
		else if (streq(action, "logout"))
		{
			return logout(this, request);
		}
		else if (streq(action, "edit"))
		{
			return edit(this, request);
		}
		else if (streq(action, "help"))
		{
			return request->render(request, "templates/user/help.cs");
		}
	}
	request->redirect(request, "user/login");
}

METHOD(fast_controller_t, destroy, void,
	private_user_controller_t *this)
{
	free(this);
}

/*
 * see header file
 */
fast_controller_t *user_controller_create(user_t *user, database_t *db)
{
	private_user_controller_t *this;

	INIT(this,
		.public = {
			.controller = {
				.get_name = _get_name,
				.handle = _handle,
				.destroy = _destroy,
			},
		},
		.user = user,
		.db = db,
		.password_length = lib->settings->get_int(lib->settings,
												  "medsrv.password_length", 6),
	);

	return &this->public.controller;
}
