/* PureAdmin
 * Copyright (C) 2003 Isak Savo
 *
 *  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.
 *
 *  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * Purepw (password and db) functions
 *
 * Copyright (C) 2003 Isak Savo
 */

/* All functions take strings in UTF-8 format and handles encoding conversion before
 * writing to disk.
 * All strings returned are valid UTF-8
 */
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <glib.h>
#include <gtk/gtk.h>

#include "cfg.h"
#include "globals.h"
#include "helper.h"
#include "usr_manager.h"
#include "purepw.h"

/* Rebuilds the puredb by calling "pure-pw mkdb" on the password file */
static
gboolean rebuild_puredb (GError **err)
{
	gchar *fullpath, *cmd;
	gboolean ret;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);
	
	if ((fullpath = g_find_program_in_path (cfg.cmd_purepw)) == NULL)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_COMMAND_NOT_FOUND,
			     "The command %s could not be found",
			     cfg.cmd_purepw);
		g_free (fullpath);
		return FALSE;
	}

	cmd = g_strdup_printf ("%s mkdb %s -f %s", fullpath, cfg.pdbfile, cfg.pwfile);
	ret = g_spawn_command_line_async (cmd, err);

	g_free (fullpath);
	g_free (cmd);
	
	return ret;
}


/* pw_add_line: Adds or modifies a password entry in the plain-text password-file used by pure-pw.
 *		If 'user' already exists, do an update; otherwise add a new line.
 * Args:
 *	line: The already formatted line to add.
 *	user: The username in vuser locale
 * Returns: 
 *	FALSE - Couldn't add line. See err for details!
 *	TRUE - Everything updated successfully.
 */
static
gboolean pw_add_line (const gchar *line, const gchar *user, GError **err)
{
	FILE *fl, *fl_tmp;
	gint fd_tmp;
	gchar *tmp_fname, buf[LINE_MAX], *dir;
	gboolean added = FALSE, pwfile_exists = TRUE;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	dir = g_path_get_dirname (cfg.pwfile);
	tmp_fname = g_strdup_printf ("%s/pureadmin_XXXXXX", dir);
	if ((fl = fopen (cfg.pwfile, "r")) == NULL)
	{
		pur_log_wrn ("Unable to open password file: %s", strerror(errno));
		if (g_file_test (cfg.pwfile, G_FILE_TEST_EXISTS))
		{
			g_set_error (err,
				     PUREADMIN_USERMANAGER_ERROR,
				     PA_USR_ERROR_PERMISSION_DENIED,
				     "Couldn't create temporary password file %s",
				     tmp_fname);
			g_free(tmp_fname);
			return FALSE;
		}
		else
			/* The file doesn't exist, but a new line could be added anyway */
			pwfile_exists = FALSE;
	}
	fd_tmp =  g_mkstemp (tmp_fname);
	if (fd_tmp == -1)
	{
		/* Not likely to happen, but lets check for it anyway! */
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Couldn't create temporary file [%s]",
			     tmp_fname);
		g_free(tmp_fname);
		return FALSE;
	}
	if (((fl_tmp = fdopen (fd_tmp, "w")) == NULL))
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Couldn't open temporary file: %s", strerror (errno));
		close (fd_tmp);
		g_free(tmp_fname);
		return FALSE;
	}
   
	while (pwfile_exists && fgets (buf, LINE_MAX, fl))
	{
		if (strncmp (buf, user, strlen (user)) == 0)
		{
			fputs (line, fl_tmp);
			fputc ('\n', fl_tmp);
			added = TRUE;
		}
		else
			fputs (buf, fl_tmp);
	}
	if (!added)
	{
		fputs (line, fl_tmp);
		fputc ('\n', fl_tmp);
	}
	fclose (fl_tmp);
	if (pwfile_exists)
	{
		fclose (fl);
		if (remove (cfg.pwfile) != 0)
		{
			g_set_error (err,
				     PUREADMIN_USERMANAGER_ERROR,
				     PA_USR_ERROR_PERMISSION_DENIED,
				     "Unable to remove password-file");
			g_free(tmp_fname);
			return FALSE;
		}
	}
	if (rename (tmp_fname, cfg.pwfile) != 0)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Unable to rename [%s] to [%s]: %s",
			     tmp_fname, cfg.pwfile, strerror (errno));
		g_free(tmp_fname);
		return FALSE;
	}
	g_free(tmp_fname);
	return TRUE;
}


static
gboolean pw_remove_line (const gchar *user, gchar **line, GError **err)
{
	FILE *fl, *fl_tmp;
	gint fd_tmp;
	guint len;
	gchar *tmp_fname, buf[LINE_MAX];
	gboolean removed = FALSE;
	gchar *dir, *sep_pos, *buf_start;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	if ((fl = fopen (cfg.pwfile, "r")) == NULL)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_FILE_NOT_FOUND,
			     "Unable to open password file");
		return FALSE;
	}
	dir = g_path_get_dirname (cfg.pwfile);
       	tmp_fname = g_strdup_printf ("%s/pureadmin_XXXXXX", dir);
	g_free (dir);
	fd_tmp = g_mkstemp (tmp_fname);
	if ((fl_tmp = fdopen (fd_tmp, "w")) == NULL)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Unable to open temporary password-file");
		g_free (tmp_fname);
		return FALSE;
	}
	buf_start = buf;
	while (fgets (buf, LINE_MAX, fl))
	{
		if ((sep_pos = strchr (buf, PW_LINE_SEP_CH)) == NULL)
			/* No ':' found in string */
			continue;
		len = GPOINTER_TO_INT(sep_pos) - GPOINTER_TO_INT(buf_start);
		if (strlen (user) == len && strncmp (user, buf, len) == 0)
		{
			removed = TRUE;
			*line = strdup (buf);
		}
		else
			fputs (buf, fl_tmp);
	}
	fclose (fl_tmp);
	fclose (fl);
	if (removed == FALSE)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_USER_NOT_FOUND,
			     "The user %s does not exist", user);
		remove (tmp_fname);
		g_free (tmp_fname);
		return FALSE;
	}
	if (remove (cfg.pwfile) != 0)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Unable to remove password-file [%s]: %s ",
			     cfg.pwfile, strerror (errno));
		g_free (tmp_fname);
		return FALSE;
	}
	if (rename (tmp_fname, cfg.pwfile) != 0)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Unable to rename [%s] to [%s]: %s",
			     tmp_fname, cfg.pwfile, strerror (errno));
		g_free (tmp_fname);
		return FALSE;
	}
	g_free (tmp_fname);
	
	return TRUE;
}


/* pw_parse_line: Parses a line read from a pure-ftpd password file.
 * Args: line	 the line to parse.
 *       pwinfo: preallocated struct where the info will be stored. (UTF-8)
 *       user:   if not NULL, then return failure if users doesn't match.
 *
 * Returns: TRUE upon success, otherwise FALSE
*/
static
gboolean pw_parse_line(gchar *line, PWInfo * const pwinfo, const gchar *user)
{
	gchar **arr;
   
	memset (pwinfo, 0, sizeof (PWInfo));
	arr = g_strsplit (line, PW_LINE_SEP, 0);
	
	if (arr_count(arr) < 17)
		return FALSE;
	pwinfo->login = string_from_vuser_locale (arr[0]);
	if (user && !g_str_equal (user, arr[0]))
	{
		g_strfreev (arr);
		return FALSE;
	}
	pwinfo->pwd = g_strdup (arr[1]);
	pwinfo->uid = (uid_t) strtoul(arr[2], NULL, 10);
	pwinfo->gid = (gid_t) strtoul(arr[3], NULL, 10);
	if (pwinfo->uid <= (uid_t) 0 || pwinfo->gid <= (gid_t) 0) {
		g_strfreev (arr);
		return FALSE;
	}
	pwinfo->gecos = string_to_utf8 (arr[4]);
	if (*(arr[5]) != '/')
	{
		g_strfreev (arr);
		return FALSE;
	}
	pwinfo->home = g_filename_to_utf8 (arr[5], -1, NULL, NULL, NULL);    
	if (*(arr[6])) {
		pwinfo->bw_ul = strtoul(arr[6], NULL, 10) / 1024UL;
	}
	if (*(arr[7])) {
		pwinfo->bw_dl = strtoul(arr[7], NULL, 10) / 1024UL;
	}
	if (*(arr[8])) {
		pwinfo->ul_ratio = (unsigned int) strtoul(arr[8], NULL, 10);
	}
	if (*(arr[9])) {
		pwinfo->dl_ratio = (unsigned int) strtoul(arr[9], NULL, 10);
	}
	if (*(arr[10])) {
		pwinfo->per_user_max = (guint) strtoul(arr[10], NULL, 10);;
	}
	if (*(arr[11])) {
		pwinfo->quota_files = strtoul(arr[11], NULL, 10);
	}
	if (*(arr[12])) {
		pwinfo->quota_size = strtoull(arr[12], NULL, 10) / (1024ULL * 1024ULL);
	}
	pwinfo->allow_local_ip  = g_strdup (arr[13]);
	pwinfo->deny_local_ip   = g_strdup (arr[14]);
	pwinfo->allow_client_ip = g_strdup (arr[15]);
	pwinfo->deny_client_ip  = g_strdup (arr[16]);
	if (sscanf(arr[17], "%u-%u", &pwinfo->time_begin, &pwinfo->time_end) == 2 &&
	    pwinfo->time_begin < 2360 && (pwinfo->time_begin % 100) < 60 &&
	    pwinfo->time_end < 2360 && (pwinfo->time_end % 100) < 60)
	{
		/* nothing */
	}
	else
	{
		pwinfo->time_begin = 0;
		pwinfo->time_end = 0;
	}
	g_strfreev (arr);

	return TRUE;
}

/*** Global functions ***/

/* pw_add_user: Adds a user to the database.
 *
 * Arguments: user	The username to add (UTF8)
 *	      passwd	The password	    (UTF8)
 *
 * Returns: TRUE if the user was added
 *	    FALSE upon failure. Check 'err' for details
 */
gboolean pw_add_user (const gchar *user, const gchar *passwd, GError **err)
{
	GString *line;
	gint ret;
	gchar *conv;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	conv = string_to_vuser_locale (user);
	line = g_string_new (conv);
	g_free (conv);
	g_string_append_printf (line, ":%s:", passwd);
	
	g_string_append_printf (line, "%d:%d::", cfg.default_uid, cfg.default_gid);
	g_string_append_printf (line, "%s/./", cfg.default_home); /* Make chroot by default */
	g_string_append (line, "::::::::::::");
	
	ret = pw_add_line (line->str, user, err);
	g_string_free (line, TRUE);
	if (ret == FALSE)
		return FALSE;
	
	return rebuild_puredb (err);
}

/* FIXME: This can cause user lost in rare cases.. */
/* pw_remove_user: Removes a user from the database.
 *
 * Arguments: user	The username to remove (UTF-8)
 *	      err	The return location for errors, NULL to ignore
 *
 * Returns: TRUE if the user was removed completely
 *	    FALSE if something went wrong, see 'err' for details
 */
gboolean pw_remove_user (const gchar *user, GError **err)
{
	gchar *line = NULL;
	GError *our_err = NULL;
	gchar *conv;
	

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	if (!user || *user == 0)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_FAILED,
			     "Invalid username");
		return FALSE;
	}
	conv = string_to_vuser_locale (user);
	if (!pw_remove_line (conv, &line, err)) {
		g_free (conv);
		return FALSE;
	}
	g_free (conv);
	
	if (!rebuild_puredb (&our_err))
	{
		g_propagate_error (err, our_err);
		g_error_free (our_err);
		our_err = NULL;
		if (!pw_add_line (line, conv, &our_err))
		{
			/* We're screwed! */
			g_error_free (*err);
			*err = NULL;
			pur_log_err ("Couldn't re-add removed line[%s] to password file: %s", line, our_err->message);
			g_set_error (err,
				     PUREADMIN_USERMANAGER_ERROR,
				     PA_USR_ERROR_FAILED,
				     "User %s is lost :-(", user);
			return FALSE;
		}
		return FALSE;
	}
	
	g_free (line);
	return TRUE;
}

/* pw_user_exists: Checks if a user exists in the password file
 *
 * Arguments: user	The username to check for
 *	      err	The location where errors should be stored
 * Returns: TRUE if the user was found
 *	    FALSE if the user wasn't found or an error occurred (check *err)
 */
gboolean pw_user_exists (const gchar *user, GError **err)
{
	gboolean found = FALSE;
	gchar *sep_pos, *buf, **arr, *conv;
	gint i;
	guint len;
	gsize bytes;
	
	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	if (!user || strlen (user) < 1)
	{
		/* FIXME: This is a programming error and shouldn't use GError... */
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_FAILED,
			     "Invalid username");
		return FALSE;
	}
	if (!g_file_get_contents (cfg.pwfile, &buf, &bytes, err))
		return FALSE;
	conv = string_to_vuser_locale (user);
	arr = g_strsplit (buf, "\n", -1);
	g_free (buf);
	i = -1;
	while (arr[++i])
	{
		buf = arr[i];
		if ((sep_pos = strchr (arr[i], PW_LINE_SEP_CH)) == NULL)
			continue;
		len = gpointer_diff (sep_pos,buf);
		if (strlen (conv) == len && strncmp (conv, buf, len) == 0)
		{
			found = TRUE;
			break;
		}
	}
	g_strfreev (arr);
	g_free (conv);
	return found;

}
/* pw_get_available_users: Get a list of available users in the puredb
 *
 * Returns: a list of available users, all strings valid UTF-8
 */
GList *pw_get_available_users (GError **err)
{
	GList *retval = NULL;
	PAvailableUser *user;
	FILE *pw;
	gchar buf[200], **arr;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	if ((pw = fopen (cfg.pwfile, "r")) == NULL)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Couldn't open %s: %s",
			     cfg.pwfile, g_strerror (errno));
		return NULL;
	}
	while (fgets (buf, 200, pw))
	{
		g_strchomp (buf);
		arr = g_strsplit (buf, PW_LINE_SEP, -1);
		if (arr_count (arr) < 4) 
			continue;
		user = g_new0 (PAvailableUser, 1);
		user->login = string_from_vuser_locale (arr[0]);
		user->realname = string_to_utf8 (arr[4]);
      
		retval = g_list_prepend (retval, user);
		g_strfreev (arr);
	}
	fclose (pw);
	return g_list_first (retval);
}

/* pw_get_user_information: Fetches information for the specified user.
 *
 * Arguments: user
 * Returns: A struct w/ all info or NULL upon failure and err is set appropiately
 */
PWInfo *pw_get_user_information (gchar *user, GError **err)
{
	PWInfo *info;
	FILE *pw_fl;
	gchar *buf, *conv;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	if ((pw_fl = fopen (cfg.pwfile, "r")) == NULL)
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_PERMISSION_DENIED,
			     "Unable to open password file %s: %s",
			     cfg.pwfile, g_strerror (errno));
		return NULL;
	}
	buf = g_new0 (gchar, LINE_MAX);
	info = g_new0 (PWInfo, 1);
	conv = string_to_vuser_locale (user);
	while (fgets (buf, LINE_MAX, pw_fl))
	{
		g_strstrip (buf);
		if (*buf == 0)
			continue;
		if (pw_parse_line (buf, info, conv)) {
			g_free (conv);
			return info;
		}
	}
	g_set_error (err,
		     PUREADMIN_USERMANAGER_ERROR,
		     PA_USR_ERROR_USER_NOT_FOUND,
		     "User %s not found", user);
	g_free (info);
	g_free (conv);
	return NULL;
}


/* pw_set_user_information: Sets the information for the user in i->login.
 *
 * Arguments: i		The information, including username. In UTF-8 format
 *	      err	Return location for error, or NULL to ignore errors.
 *
 * Returns: TRUE if user information was updated.
 *	    FALSE if something went wrong. Check err for details
 */
gboolean pw_set_user_information (PWInfo * const i, GError **err)
{
	GString *line;
	gchar *tmp, *user_vlocale;

	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);
	
	user_vlocale = string_to_vuser_locale (i->login);
	line = g_string_new (user_vlocale);
	g_string_append_printf (line, ":%s:", i->pwd);
	/* UID and GID */
	g_string_append_printf (line, "%d:%d:", i->uid, i->gid);

	/* Real name */
	g_string_append_printf (line, "%s:", i->gecos ? i->gecos : "");
	/* Home */
	if (MISC_VALID_STRING (i->home))
	{
		tmp = g_filename_from_utf8 (i->home, -1, NULL, NULL, NULL);
		g_string_append_printf (line, "%s:", tmp);
		g_free (tmp);
	}
	else
	{
		g_set_error (err,
			     PUREADMIN_USERMANAGER_ERROR,
			     PA_USR_ERROR_INVALID_FIELD,
			     "Home directory field invalid [%s]", i->home);
		g_string_free (line, TRUE);
		g_free (user_vlocale);
		return FALSE;
	}
	/* Bandwidth- and size-restrictions */
	if (i->bw_ul)
		g_string_append_printf (line, "%li:", i->bw_ul * 1024UL);
	else
		g_string_append (line, ":");
	if (i->bw_dl)
		g_string_append_printf (line, "%li:", i->bw_dl * 1024UL);
	else
		g_string_append (line, ":");
	if (i->ul_ratio)
		g_string_append_printf (line, "%d:", i->ul_ratio);
	else
		g_string_append (line, ":");
	if (i->dl_ratio)
		g_string_append_printf (line, "%d:", i->dl_ratio);
	else
		g_string_append (line, ":");
   
	if (i->per_user_max)
		g_string_append_printf (line, "%d:", i->per_user_max);
	else
		g_string_append (line, ":");
   
	if (i->quota_files)
		g_string_append_printf (line, "%d:", i->quota_files);
	else
		g_string_append (line, ":");
	if (i->quota_size)
		g_string_append_printf (line, "%lld:", i->quota_size * 1024ULL * 1024ULL);
	else
		g_string_append (line, ":");
   
	/* IP-restrictions */
	if (MISC_VALID_STRING (i->allow_local_ip))
		g_string_append_printf (line, "%s:", i->allow_local_ip);
	else
		g_string_append (line, ":");
   
	if (MISC_VALID_STRING (i->deny_local_ip))
		g_string_append_printf (line, "%s:", i->deny_local_ip);
	else
		g_string_append (line, ":");
   
	if (MISC_VALID_STRING (i->allow_client_ip))
		g_string_append_printf (line, "%s:", i->allow_client_ip);
	else
		g_string_append (line, ":");
   
	if (MISC_VALID_STRING (i->deny_client_ip))
		g_string_append_printf (line, "%s:", i->deny_client_ip);
	else
		g_string_append (line, ":");
   
	if (i->time_begin == 0 && i->time_end == 0) {
		/* Nada */
	}
	else if (i->time_begin < 2360 && (i->time_begin % 100) < 60 &&
		 i->time_end < 2360 && (i->time_end % 100) < 60)
		g_string_append_printf (line, "%04d-%04d", i->time_begin, i->time_end);

	if (!pw_add_line (line->str, user_vlocale, err))
	{
		g_string_free (line, TRUE);
		g_free (user_vlocale);
		return FALSE;
	}
	g_string_free (line, TRUE);
	g_free (user_vlocale);
	return rebuild_puredb (err);
}


syntax highlighted by Code2HTML, v. 0.9.1