/*
 *   Copyright (c) 1998 David Parsons. All rights reserved.
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *  3. All advertising materials mentioning features or use of this
 *     software must display the following acknowledgement:
 *
 *   This product includes software developed by David Parsons
 *   (orc@pell.chi.il.us)
 *
 *  4. My name may not be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY DAVID PARSONS ``AS IS'' AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 *  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DAVID
 *  PARSONS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 *  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 *  IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 *  THE POSSIBILITY OF SUCH DAMAGE.
 */

static const char rcsid[] = "$Id: s.user.cc 1.8 99/12/12 09:53:30-00:00 orc $";

/*
 * user:  user administration via the existing PW manipulation functions
 *
 * main screen:
 *                               
 *                  +-------------------------(-)+
 *                  |login  name         (locked)|
 *                  |login  name                 |
 *                  +-------------------------(+)+
 *                  [add] [modify] [delete] [done]
 */

extern "C" {

#include <ndialog.h>
#include <dialog.h>

#include <unistd.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <limits.h>

extern char* PasswordCheck(char*, char*);
}

#include "util.h"

#define CC_OK		0	/* committed changes */
#define CC_FAIL		1	/* didn't commit changes */
#define CC_RETRY	2	/* didn't commit changes; retry entry */

static LIA users;	/* a list of all the users, id=login, item=gecos */
static ndObject o_list;	/* the list object that displays users */

/* the user we're currently looking at in ModifyOrMakeUserMenu() */
static struct passwd* user;
static int user_nr;		/* in users[] list */

/* user modification variables for ModifyOrMakeUserMenu() */
static char name[80], login[17], passwd[17], shell[32], home[80], locked, uid[20], gid[20];
static char origlockstate;
static char createhome;


/* user modification objects for ModifyOrMakeUserMenu() */
static ndObject o_name, o_login, o_passwd, o_shell, o_home, o_locked, o_uid, o_gid;

/* password checking */
static char chkpw[17];
static ndObject o_chkpw;
static int needtochangepassword = 0;

/* valid shells */
static LIA shells;	/* a list of all the shells in getusershells() */


/* various callbacks */
static int adduser(ndObject obj, ndDisplay display);	/* add a user */
static int deleteuser(ndObject obj, ndDisplay display);	/* delete a user */
static int modifyuser(ndObject obj, ndDisplay display);	/* modify a user */
static int chk_pass(ndObject obj, ndDisplay display);	/* password change */
static int chk_login(ndObject obj, ndDisplay display);	/* check that a user name is not in use */
static int chk_lock(ndObject obj, ndDisplay display);	/* lock/unlock account */
static int chk_shell(ndObject obj, ndDisplay display);	/* check that a shell is valid */
static int chk_home(ndObject obj, ndDisplay display);	/* check that a home directory exists */
static int chk_uid(ndObject obj, ndDisplay display);	/* check that the UID is okay */
static int chk_gid(ndObject obj, ndDisplay display);	/* check that the GID is okay */
static int finalcheck(ndObject obj, ndDisplay display);	/* final check for adding/changing a user */

static int changeaccount(int);


#define PWfile		"/etc/passwd"
#define newPWfile	PWfile ".NEW"
#define oldPWfile	PWfile ".OLD"


/*
 * getUserList() builds a user list from the system login database
 *               (traditionally /etc/passwd)
 */
static void
getUserList()
{
    struct passwd *pwd;
    users = newLIA(0,0);
    char bfr[50];
    int pwsize;
    FILE *f;

    /* Ick:  getpwent() is better, but it magically does yellow pages
     * stuff which we don't want.
     */
    if ((f = fopen(PWfile, "r")) != 0) {
	while ((pwd = fgetpwent(f)) != 0) {
	    if (streq(pwd->pw_name, "+"))
		continue;
	    pwsize = strlen(pwd->pw_passwd);
	    sprintf(bfr, "%-30.30s %s",
		    pwd->pw_gecos,
		    (pwsize > 0 && pwsize < 2) ? "(locked)" : "        ");
	    addToLIA(users, pwd->pw_name, bfr, 0);
	}
	fclose(f);
    }
    endpwent();
} /* getUserList */


#define STARTUID	400
/*
 * getmaxuid() gets the highest user uid (>=STARTUID && < SHRT_MAX)
 */
uid_t
getmaxuid()
{
    struct passwd *pwd;
    uid_t maxuid = STARTUID-1;

    while ((pwd = getpwent()) != 0) {
	if (pwd->pw_uid < SHRT_MAX && pwd->pw_uid > maxuid)
	    maxuid = pwd->pw_uid;
    }
    endpwent();
    return maxuid;
} /* getmaxuid() */


/*
 * makeUserMenu() makes up the menu that presents a
 * list of users to manipulate.
 */
static ndObject
makeUserMenu()
{
    ndObject chain;

    getUserList();

    o_list = newRadioList(0,0,-1,12,LIAcount(users), LIAlist(users),
		    0, "", MENU_SELECTION|SHOW_IDS|ALWAYS_HIGHLIT,
		    modifyuser, "user.html#modify");

    chain = ObjChain(o_list, newButton(0, " ADD  ", adduser, "user.html#add"));
    chain = ObjChain(chain,  newButton(1, "MODIFY", modifyuser, "user.html#modify"));
    chain = ObjChain(chain,  newButton(2, "DELETE", deleteuser, "user.html#delete"));
    return ObjChain(chain, newOKButton(3, " DONE ", 0, 0));
} /* makeusermanu */


/*
 * ModifyOrMakeUserMenu() gives us a form that we can use either to create a new user or
 * modify the attributes for an existing user.
 *
 *           User name [                                   ]
 *           Login     [           ] Password  [           ]
 *           Shell     [           ] Home Dir  [           ]
 *                 UID [     ] GID [     ]   [x] Locked
 *
 *            [  OK  ]          [GROUPS]           [CANCEL]
 *
 *     If we're adding a new user, we have a UID field that's
 *     preloaded from the password database: max(uid: uid>400 && uid < MAXINT)
 *
 *     Groups brings up a menu containing
 *              +-------------------(-)+
 *              | [X]  group name      |
 *              | [X]  group name      |
 *              | [X]  group name      |
 *              +-------------------(+)+
 */
static ndObject
ModifyOrMakeUserMenu(struct passwd *user)
{
    ndObject chain ;
    int pwsize;
    struct group *gr;
    char *sh;

    needtochangepassword = 0;

    deleteLIA(shells);
    shells = newLIA(0, 0);
    while ((sh = getusershell()) != 0)
	addToLIA(shells, 0, sh, 0);
    endusershell();

    memset(passwd, 0, sizeof passwd);
    if (user) {
	strcpy(name, user->pw_gecos);
	strcpy(login, user->pw_name);
	strcpy(shell, user->pw_shell);
	strcpy(home, user->pw_dir);
	sprintf(uid, "%d", user->pw_uid);
	if ((gr = getgrgid(user->pw_gid)) != 0)
	    strcpy(gid, gr->gr_name);
	else
	    strcpy(gid, "nobody");
	pwsize = strlen(user->pw_passwd);
	origlockstate = locked = (pwsize > 0) && (pwsize < 2);
    }
    else {
	memset(name, 0, sizeof name);
	memset(login, 0, sizeof login);
	strcpy(shell, "/bin/sh");
	strcpy(home, "/home/");
	sprintf(uid, "%d", 1+getmaxuid());
	memset(gid, 0, sizeof gid);
	origlockstate = locked = 0;
    }

    o_name  = newString(0,0,42,sizeof name, name, 0, "User name", chk_login,
			(char*)(getuid() ? "useruser.html#user"
					 : "user.html#user"));
    o_login = newString(4,3,16,sizeof login, login, 0, "Login", 0, "user.html#login");
    o_passwd = newPWString(27, 3, 16, sizeof passwd, passwd,
			       0, "Password", chk_pass,
			       (char*)(getuid() ? "useruser.html#passwd"
						: "user.html#passwd"));
    if (locked)
	setReadonly(o_passwd);
    o_shell = newString(4, 6,30,sizeof shell, shell,
			0,  "Shell", chk_shell,
			(char*) (getuid() ? "useruser.html#shell"
					  : "user.html#shell"));
    o_home  = newString(1, 9,30,sizeof home, home, 0, "Home dir", 0, "user.html#home");
    o_uid   = newString(6,12,8,8,uid, 0, "UID", chk_uid, "user.html#uid");
    if (user)
	setReadonly(o_uid);
    o_gid   = newString(19,12,8,8,gid, 0, "Group", chk_gid, "user.html#groups");
    if (getuid() == 0)
	o_locked = newCheck(34,13,0, "|Locked", &locked, chk_lock, "user.html#locked");

    chain = ObjChain(o_name, o_login);
    chain = ObjChain(chain, o_passwd);
    chain = ObjChain(chain, o_shell);
    chain = ObjChain(chain, o_home);
    chain = ObjChain(chain, o_uid);
    chain = ObjChain(chain, o_gid);

    if (getuid() == 0)
	chain = ObjChain(chain, o_locked);
    else {
	setReadonly(o_login);
	setReadonly(o_home);
	setReadonly(o_gid);
	setReadonly(o_uid);
    }

    chain = ObjChain(chain, newOKButton(0, "APPLY", finalcheck,
			(char*)(getuid() ? "useruser.html" : "user.html")));
#if 0
    if (getuid() == 0)
	chain = ObjChain(chain, newButton(1, "GROUPS", 0, "user.html#groups"));
#endif
    chain = ObjChain(chain, newCancelButton(2, "CANCEL", 0, 
			(char*)(getuid() ? "useruser.html" : "user.html")));

    return chain;
} /* ModifyOrMakeUserMenu */


/*
 * adduser() will add a user to the login database
 */
static int
adduser(ndObject obj, ndDisplay display)
{
    int rc;
    ndObject chain = ModifyOrMakeUserMenu(0);

    user_nr = -1;
    needtochangepassword = 1;
    createhome = 1;
again:
    do {
	if (MENU(chain, -1, -1, "Add new user", 0, 0) != MENU_OK)
	    break;

	if ((rc = changeaccount(0)) == CC_OK) {
	    addToLIA(users, login, name, 0);
	    setObjData(o_list, 0, LIAlist(users), LIAcount(users));
	    setObjCursor(o_list, LIAcount(users)-1);
	    drawObj(o_list, display);
	    if (mkdir(home, 0755) == 0) {
		chown(home, user->pw_uid, user->pw_gid);
		/* copy /etc/skel... */
	    }
	    break;
	}
    } while (rc == CC_RETRY);

    deleteObjChain(chain);

    return 0;
} /* adduser */


/*
 * deleteuser() will delete a user from the login database
 */
static int
deleteuser(ndObject obj, ndDisplay display)
{
    int rc;
    char bfr[200];

    sprintf(bfr, "Are you sure you want to delete the %s account",
		LIAlist(users)[currentSelection(o_list)].id);
    rc = dialog_yesno("Confirm", bfr, -1, -1);

    if (rc == 0) {
	sprintf(bfr, "Are you REALLY sure you want to\n"
		     "delete the %s account",
		    LIAlist(users)[currentSelection(o_list)].id);
	rc = dialog_yesno("Confirm again", bfr, -1, -1);
    }

    if ( (rc == 0) && (changeaccount(1) == CC_OK) ) {
	delFromLIA(users, currentSelection(o_list));
	setObjData(o_list, 0, LIAlist(users), LIAcount(users));
	drawObj(o_list, display);
    }
    return 1;
} /* deleteuser */


/*
 * modifyuser() will change a user
 */
static int
modifyuser(ndObject obj, ndDisplay display)
{
    ndObject chain; 
    int cc;

    createhome = 0;

    user_nr = currentSelection(o_list);
    user = getpwnam(LIAlist(users)[user_nr].id);

    if (user == 0) {
	Error("%sser %s cannot be found",
		getpwnam(LIAlist(users)[user_nr].id) ? "Local u" : "U", user);
	delFromLIA(user, currentSelection(o_list));
	setObjData(o_list, 0, LIAlist(user), LIAcount(user));
	drawObj(o_list, display);
    }
    else {
	chain = ModifyOrMakeUserMenu(user);

	do {
	    if (MENU(chain, -1, -1, user->pw_name, 0, 0) != MENU_OK)
		break;

	    if ((cc = changeaccount(0)) == CC_OK)
		break;
	} while (cc != CC_RETRY);

	deleteObjChain(chain);
    }
    return 0;
} /* modifyuser */


/*
 * pw_paranoid() checks to make sure a confirmation password is the
 * same as the original password
 */
static int
pw_paranoid(ndObject obj, ndDisplay display)
{
    if (!streq(chkpw, passwd)) {
	Error("Passwords do not match");
	memset(chkpw, 0, sizeof chkpw);
	drawObj(o_chkpw, display);
	return 0;
    }
    return 1;
} /* pw_paranoid */


/*
 * chk_login() makes certain that a new username does not match one that's in
 * the user database
 */
static int
chk_login(ndObject obj, ndDisplay display)
{
    int x;
    char *name = (char*)objDataPtr(obj);

    if (!writable(obj))
	return 1;

    for (x=0; x < LIAcount(users); x++) {
	if (x == user_nr)
	    continue;
	if (streq(LIAlist(users)[x].id, name)) {
	    Error("The name %s is already being used\n"
		  "Please choose a different one.");
	    memset(login, 0, sizeof login);
	    setObjData(obj, login, sizeof login);
	    drawObj(obj, display);
	    return 0;
	}
    }
    return 1;
} /* chk_login */


/* 
 * chk_pass() asks a user to confirm that they really want to change a password
 */
static int
chk_pass(ndObject obj, ndDisplay display)
{
    ndObject chain;
    int rc;
    char* msg;
    static char lastpwd[20];
    static int retry = 0;

    if ((msg = PasswordCheck(login, passwd)) != 0) {
	Error(msg);
	if (~streq(lastpwd, passwd)) {
	    strcpy(lastpwd, passwd);
	    retry = 0;
	}
	else if (++retry < getuid() ? 10 : 3)
	    goto fail_pass;
    }

    o_chkpw = newPWString(0,0,16,15,chkpw, 0, "Confirm password", 
				    pw_paranoid, "user.html#passwd");
    chain = ObjChain(o_chkpw, newOKButton(0, "  OK  ", pw_paranoid, 0));
    chain = ObjChain(chain, newCancelButton(1, "CANCEL", 0, 0));

    rc = MENU(chain, -1, -1, "Confirm password change",
			    "Please re-enter the password to make sure\n"
			    "it's correct", 0);
    deleteObjChain(chain);

    if (rc == MENU_OK) {
	needtochangepassword = 1;
	return 1;
    }
fail_pass:
    memset(passwd, 0, sizeof passwd);
    setObjData(o_passwd, passwd, sizeof passwd);
    drawObj(o_passwd, display);
    return 0;
} /* chk_pass */


/*
 * chk_lock() toggles writability of the passwd field depending on whether the account is
 * locked or not.
 */
static int
chk_lock(ndObject obj, ndDisplay display)
{
    if (locked)
	setReadonly(o_passwd);
    else
	setWritable(o_passwd);
    drawObj(o_passwd, display);

    if (origlockstate && !locked)
	needtochangepassword = 1;

    return 1;
} /* chk_lock */


/*
 * exitmenu() is a dummy callback that simply tells MENU() to exit
 */
static int
exitmenu(ndObject obj, ndDisplay display)
{
    return -1;	/* Flee! All is discovered! */
} /* exitmenu */


/*
 * chk_shell() checks to see if a shell is legitimate.
 */
static int
chk_shell(ndObject obj, ndDisplay display)
{
    int x;
    ndObject list, chain;
    int rc;

    for (x=LIAcount(shells)-1; x >= 0; x--)
	if (streq(LIAlist(shells)[x].item, shell))
	    break;
    if (x >= 0)
	return 1;

    /* else we didn't find the shell via getusershell() */
    if (getuid() != 0) {
	/* Users may only choose shells that are in getusershell()
	 * (/etc/shells) -- we'll spit up a little list and give
	 * them the chance to pick one of the approved ones.
	 */
	list = newList(0,0,-1,5,LIAcount(shells), LIAlist(shells),
			0, "", MENU_SELECTION, exitmenu, "useruser.html#shell");
	chain = ObjChain(list, newCancelButton(1, "CANCEL", 0, "useruser.html#shell"));

	rc = MENU(chain, -1, -1, (char*)objDataPtr(obj),
		    "This shell is not allowed.  Please\n"
		    "choose one of the listed shells.", 0);

	if (rc == MENU_OK)
	    strcpy(shell, LIAlist(shells)[currentSelection(list)].item);
	else if (user)
	    strcpy(shell, user->pw_shell);
	else
	    memset(shell, 0, sizeof shell);

	setObjData(o_shell, shell);
	drawObj(o_shell, display);

	deleteObjChain(chain);
	return (rc == MENU_OK) ? 1 : 0;
    }

    /* Root can set any shell, with the caveat that it must exist.  If the shell is
     * blank, though, it is okay, because it defaults to /bin/sh
     */
    if (shell[0] && access(shell, X_OK) != 0) {
	Error("%s does not exist", shell);
	if (user)
	    strcpy(shell, user->pw_shell);
	else
	    memset(shell, 0, sizeof shell);
	setObjData(o_shell, shell);
	drawObj(o_shell, display);
	return 0;
    }
    return 1;
} /* chk_shell */


/*
 * chk_uid() makes certain the uid isn't stomping on someone else's toes and if it's
 * a forbidden uid.
 */
static int
chk_uid(ndObject obj, ndDisplay display)
{
    char *ptr = (char*)objDataPtr(obj);
    int thisuid = atoi(ptr);
    struct passwd *uidpwd;

    if (*ptr == 0) {
	sprintf(uid, "%d", getmaxuid()+1);
	setObjData(obj, uid);
	drawObj(obj, display);
	return 1;
    }
    while (isdigit(*ptr))
	++ptr;

    if (*ptr)
	Error("UIDs must be numeric");
    else if (user && (thisuid != user->pw_uid))
	Error("You cannot change this UID");
    else if ((user == 0) && (uidpwd = getpwuid(thisuid)) != 0)
	Error("This UID is already in use");
    else
	return 1;

    memset(uid, 0, sizeof uid);
    setObjData(obj, uid);
    drawObj(obj,display);
    return 0;
} /* chk_uid */


/*
 * getgroupbymenu() spits up a menu of the existing groups and lets the user pick one
 * for the new account.  It returns 1 if a group was successfully selected, 0 otherwise.
 */
static int
getgroupbymenu(int flag, struct group** ptr)
{
    LIA groups;
    int rc;
    struct group* grp;
    ndObject list, chain;

    groups = newLIA(0,0);

    while ((grp = getgrent()) != 0)
	addToLIA(groups, 0, grp->gr_name, 0);
    endgrent();

    list = newList(0,0,-1,5,LIAcount(groups), LIAlist(groups),
		    0, "", MENU_SELECTION, exitmenu, "user.html#groups");
    chain = ObjChain(list, newCancelButton(1, "CANCEL", 0, 0));

    rc = MENU(chain, -1, -1, gid,
	    (char*)(flag ?  "This group does not exist.  Please choose\n"
			     "one of the groups that exists on the system."
			 :   "Please choose one of the following groups"), 0);

    if (rc == MENU_OK) {
	strcpy(gid, LIAlist(groups)[currentSelection(list)].item);
	*ptr = getgrnam(gid);

	if (*ptr == 0) {
	    Error("The group %s has disappeared from the system?", gid);
	    rc = MENU_ERROR;
	}

    }

    deleteObjChain(chain);
    deleteLIA(groups);

    return (rc == MENU_OK) ? 1 : 0;
} /* getgroupbymenu */


/*
 * chk_gid() makes sure the group id exists
 */
static int
chk_gid(ndObject obj, ndDisplay display)
{
    char *ptr = (char*)objDataPtr(obj);
    int thisgid = atoi(ptr);
    struct group* grp;
    int rc;

    if (*ptr == 0) {
	if (getgroupbymenu(0, &grp) == 0)
	    return 0;
	else
	    ptr = gid;
    }
    else {
	while (isdigit(*ptr))
	    ++ptr;
	if (*ptr)
	    grp = getgrnam((char*)objDataPtr(obj));
	else
	    grp = getgrgid(thisgid);
    }

    if (grp != 0 || getgroupbymenu(1, &grp)) {
	if (grp->gr_gid == 0)
	    rc = dialog_yesno("CONFIRM",
			"This is the administrator group.  Do you want\n"
			"to give this user administrator privileges?", -1, -1);
	else
	    rc = 0;

	if (rc == 0) {
	    strcpy(gid, grp->gr_name);
	    setObjData(obj, gid);
	    drawObj(obj, display);
	    return 1;
	}
    }
    memset(gid, 0, sizeof gid);
    setObjData(obj, gid);
    drawObj(obj, display);
    return 0;
} /* chk_gid */


/*
 * finalcheck() is the finishing check for adding or modifying a user
 */
static int
finalcheck(ndObject obj, ndDisplay display)
{
    if (login[0] == 0) {
	Error("Please enter a login name for this account");
	return 0;
    }
    if (chk_shell(o_shell, display) && chk_login(o_login, display)
				    && chk_uid(o_uid, display)
				    && chk_gid(o_gid, display)) {
	if (needtochangepassword) {
	    if (passwd[0] == 0) {
		int rc = dialog_yesno(login,
			"Do you want this account to have\n"
			"no password?", -1, -1);
		if (rc != 0)
		    return 0;
	    }
	    else if (!streq(passwd, chkpw))
		return chk_pass(o_passwd, display);
	}
	return 1;
    }
    return 0;
} /* finalcheck */


/*
 * database manipulation routines.  Today they only do /etc/passwd, gleefully ignoring /etc/shadow
 * and yellowpages.
 *
 */


/*
 * PWfail() pops up a dialog box describing a login database error and asks the
 * user if they want to retry
 */
static int
PWfail(char *whence)
{
    char msg[200];
    int rc;

    sprintf(msg, "User database update error:\n"
		 "%s\n"
		 "\n"
		 "Do you want to RETRY or CANCEL", strerror(errno));

    rc = ndialog_yesno("Error!", msg, -1, -1, "0:RETRY", "1:CANCEL");

    return (rc == 0) ? CC_RETRY : CC_FAIL;
} /* PWfail */


/* 
 * changeaccount() changes the account specified in user
 */
static int
changeaccount(int actually_delete)
{
    FILE *pw = 0;		/* FILE pointing at the new password file */
    FILE *oldpw = 0;		/* FILE pointing at the old password file */
    int pwd;			/* file descriptor pointing at same */
    int didput=0;		/* flag to mark when we wrote any new entry */
    struct passwd *ent;		/* scratch password entry */
    static struct passwd newpwent;	/* new password entry */
    char salt[2];
    struct group* gr;
    time_t now;
    struct stat info;
    int retry = 0;
    int rc;
    int hasyp = 0;		/* found a + password entry */
    struct passwd ypent;	/* set if a + entry is found */


    /* try to open a new password file */
    while ((pwd = open(newPWfile, O_WRONLY|O_CREAT|O_APPEND|O_EXCL, 0644)) < 0) {
	/* the newPWfile exists.  So we'll take a look at it and kill it
	 * dead if it's more than 60 seconds old
	 */
	if (retry++ > 10)	/* no infinite loops, please */
	    return PWfail("one");
	time(&now);
	if (stat(newPWfile, &info) < 0)
	    if (errno != ENOENT)
		return PWfail("two");
	    /* otherwise it's gone away
	     * in the interim */

	if (now-info.st_atime > 60L)
	    unlink(newPWfile);
	else
	    return PWfail("three");
    }

    if ((pw = fdopen(pwd, "w")) == 0) {
	rc = PWfail("four");
die_a_horrible_death:
	unlink(newPWfile);
	unlink(oldPWfile);
	if (pw)
	    fclose(pw);
	close(pwd);
	return rc;
    }

    /* open the old password file */

    if ((oldpw = fopen(PWfile, "r")) != 0) {
	/* walk through the old password file, copying entries
	 * over to the new password file
	 */
	errno = hasyp = 0;
	while ((ent = fgetpwent(oldpw)) != 0)
	    if (streq(ent->pw_name, "+")) {
		hasyp = 1;
		ypent = (*ent);
	    }
	    else if (user == 0 || ent->pw_uid != user->pw_uid)
		if (putpwent(ent, pw) < 0) {
		    rc = PWfail("five");
		    goto die_a_horrible_death;
		}
	fclose(oldpw);
    }
    else {
	rc = PWfail("six");
	goto die_a_horrible_death;
    }

    if ( !actually_delete ) {
	time(&now);
	salt[0] = (now & 0x0f) +	'A';
	salt[1] = ((now & 0xf0) >> 4) + 'a';

	newpwent.pw_name = login;

	if (needtochangepassword || user == 0 || locked)
	    newpwent.pw_passwd = (char*)(locked ? "*" : crypt(passwd, salt));
	else
	    newpwent.pw_passwd = user->pw_passwd;

	newpwent.pw_uid = atoi(uid);
	if ((gr = getgrnam(gid)) != 0)
	    newpwent.pw_gid = gr->gr_gid;
	else
	    newpwent.pw_gid = 0;
	newpwent.pw_gecos = name;
	newpwent.pw_dir = home;
	newpwent.pw_shell = shell;

	user = &newpwent;
	if (putpwent(user, pw) < 0) {
	    rc = PWfail("seven");
	    goto die_a_horrible_death;
	}
    }
    if (hasyp) {
	fprintf(pw, "+:%s", ypent.pw_passwd);
	fprintf(pw, (ypent.pw_uid != 0) ? ":%d" : ":", ypent.pw_uid);
	fprintf(pw, (ypent.pw_gid != 0) ? ":%d" : ":", ypent.pw_gid);
	fprintf(pw, ":%s:%s:%s\n",  ypent.pw_gecos,
				    ypent.pw_dir,
				    ypent.pw_shell);
    }

    /* being paranoid about possible errors,
     * close the old and new password files
     */
    if (errno != 0 || fclose(pw) != 0) {
	rc = PWfail("eight");
	goto die_a_horrible_death;
    }

    pw = 0;
    close(pwd);
    endpwent();

    /* rename the files */
    unlink(oldPWfile);
    if (link(PWfile, oldPWfile) < 0) {		/* current -> old */
	goto die_a_horrible_death;
    }

    if (unlink(PWfile) < 0) {			/* !current */
	goto die_a_horrible_death;
    }

    if (link(newPWfile, PWfile) < 0) {		/* new -> current */
	Error("A horrible database error just happened!  We unlinked the old\n"
	      "user database, but were unable to commit the changes to it.\n"
	      "Press [ENTER] to start backing out the changes to the old file");
	if (link(oldPWfile, PWfile) < 0)
	    goto die_a_horrible_death;
    }
    unlink(newPWfile);				/* !new */
    return CC_OK;
} /* changeaccount */




/*
 * user, in mortal flesh
 */
extern "C" int
doit(int argc, char **argv)
{
    int rc;
    ndObject chain;

    shells = newLIA(0,0);

    if (getuid() != 0) {
	if ((user = getpwuid(getuid())) != 0)
	    goto editoneaccount;
	else if (getpwuid(getuid()) != 0) {
	    Error("Your account doesn't exit in the local user database");
	    rc = MENU_ERROR;
	}
	else {
	    Error("You don't seem to exist in the user database?");
	    rc = MENU_ERROR;
	}
    }
    else if (argc <= 1) {
	chain = makeUserMenu();
	rc = MENU(chain, -1, -1, "User administration", 0, 0);
	deleteObjChain(chain);
    }
    else if ((user = getpwnam(argv[1])) != 0) {
editoneaccount:
	chain = ModifyOrMakeUserMenu(user);
	do {
	    if (MENU(chain, -1, -1, user->pw_gecos, 0, 0) != MENU_OK)
		break;
	} while (changeaccount(0) == CC_RETRY);
	deleteObjChain(chain);
    }
    else {
	Error((char*)(getpwnam(argv[1]) ? "No local user %s" : "No user %s"), argv[1]);
	rc = MENU_ERROR;
    }

    return (rc == MENU_OK) ? 0 : 1;
} /* doit */
