/* edituser.c -- An example of how to edit the user database. */

#include <config.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/file.h>
#include <pwd.h>
#include <ndbm.h>
#include <general.h>
#include <fingerpaths.h>
#include "recedit.h"

/* Field name of the field which contains the info about the last
   person to modify this record. */
#define MODFIELD "Last Modified By"

/* Name of this program. */
char *progname;

/* Non-zero means just print the user to stdout. */
int just_display = 0;

/* Editor to use when it is time to edit a person.  This can be
   overridden with either the EDITOR or VISUAL environment variable. */
char *editor = "emacs";

/* Name of the user editing the current data record. */
char *current_user;

/* Global variable contains the record format. */
FIELD_DESC **Fields;

/* Non-zero means we are deleting the mentioned users. */
int deleting = 0;

main (argc, argv)
     int argc;
     char **argv;
{
  char *temp;
  int open_flags;
  DBM *database;

  progname = argv[0];
  temp = (char *)rindex (progname, '/');
  if (temp)
    progname = temp + 1;

  if (strcmp (progname, "whois") == 0)
    {
      just_display = 1;
      open_flags = O_RDONLY;
    }
  else
    open_flags = O_RDWR | O_CREAT;

  temp = (char *)getenv ("EDITOR");

  if (!temp)
    temp = (char *)getenv ("VISUAL");

  if (temp)
    editor = temp;

  database = dbm_open (DBMINFO, open_flags, 0666);

  if (!database)
    {
      fprintf (stderr, "Cannot access database `%s'.\n", DBMINFO);
      exit (1);
    }

  /* Get the format of the data. */
  Fields = re_read_description_file (FORMATFILE);

  if (!Fields)
    {
      fprintf (stderr, "%s: Cannot read description file %s.\n",
	       progname, FORMATFILE);
      exit (1);
    }

  /* Get the name of the modifiying user. */
  {
    struct passwd *entry;
    int uid = getuid ();

    entry = getpwuid (uid);

    if (!entry)
      current_user = savestring ("Unknown User!");
    else
      current_user = savestring (entry->pw_name);
  }

  while (--argc)
    {
      char *arg = *++argv;

      if (!just_display && strcmp (arg, "-delete") == 0)
	{
	  if (geteuid () != 0)
	    {
	      fprintf (stderr, "%s: You must be root to delete users.\n",
		       progname);
	      exit (2);
	    }

	  deleting = 1;
	  continue;
	}

      if (deleting)
	delete_user (arg, database);
      else if (just_display)
	display_user (arg, database);
      else
	create_or_edit (arg, database);
    }

  dbm_close (database);
  exit (0);
}

/* Just do the "whois" part of the program. */
display_user (user, database)
     char *user;
     DBM *database;
{
  datum key, info, extract_key (), extract_record ();
  FIELD_DESC **fields;

  key.dsize = 1 + strlen (user);
  key.dptr = user;

  info = dbm_fetch (database, key);

  if (!info.dptr)
    {
      fprintf (stdout, "%s: Cannot find information about %s.\n",
	       progname, user);
      return;
    }

  fields = re_copy_fields (Fields);
  re_extract_fields (info.dptr, fields);
  re_print_fields (fields, stdout);
  fprintf (stdout, "End.\n\n");
  re_free_fields (fields);
}

/* Delete USER from DATABASE. */
delete_user (user, database)
     char *user;
     DBM *database;
{
  int result;
  datum key, info;

  key.dsize = 1 + strlen (user);
  key.dptr = user;

  info = dbm_fetch (database, key);

  if (!info.dptr)
    {
      fprintf (stdout, "%s: Cannot find information about %s.\n",
	       progname, user);
      return;
    }

  result = dbm_delete (database, key);
}

typedef struct {
  int gid;
  int count;
} GROUP_ID;

/* Create or edit the existing definition of USER in DATABASE. */
create_or_edit (user, database)
     char *user;
     DBM *database;
{
  datum key, info, extract_key (), extract_record ();
  FIELD_DESC **fields = re_copy_fields (Fields);
  int result, dbm_flags, record_modified = 0;
  register int i;

  key.dsize = 1 + strlen (user);
  key.dptr = user;

  info = dbm_fetch (database, key);

  /* If this user doesn't exist, then create it. */
  if (!info.dptr)
    {
      PHONE phone;
      DATE birthdate;
      struct passwd *entry;

      dbm_flags = DBM_INSERT;

      /* See if this user has an account on this machine. */
      entry = getpwnam (user);

      if (entry)
	{
	  char *tem = entry->pw_gecos;
	  while (*tem && *tem != ';' && *tem != ',')
	    tem++;
	  *tem = '\0';

	  re_set_field (fields, "Real Name", savestring (entry->pw_gecos));
	  re_set_field (fields, "Home Dir", savestring (entry->pw_dir));
	  re_set_field (fields, "User ID", entry->pw_uid);
	  re_set_field (fields, "Group ID", entry->pw_gid);
	  re_set_field (fields, "Login Shell", savestring (entry->pw_shell));
	}
      else
	{
	  int *intp, highest = 0;
	  FIELD_DESC **tfields = re_copy_fields (Fields);
	  datum lkey, linfo;
	  int ids_size;
	  GROUP_ID **ids, *best_bet;

	  /* Here is the interesting part.  If this user doesn't already have
	     a password entry, try to default the user id field.  Also,
	     default the group id to the most common group in the database.
	     DBM has much faster access time than setpwent ()... */

	  ids_size = 20;
	  ids = (GROUP_ID **)xmalloc (ids_size * sizeof (GROUP_ID *));

	  bzero (ids, ids_size * sizeof (GROUP_ID));

	  for (lkey = dbm_firstkey (database); lkey.dptr;
	       lkey = dbm_nextkey (database))
	    {
	      linfo = dbm_fetch (database, lkey);
	      re_extract_fields (linfo.dptr, tfields);

	      /* Find the highest user number. */
	      intp = (int *)re_get_field_value (tfields, "User ID");
	      if (intp && highest < *intp)
		highest = *intp;

	      /* Keep track of the group ids. */
	      intp = (int *)re_get_field_value (tfields, "Group ID");
	      if (intp)
		{
		  int group = *intp;

		  for (i = 0; ids[i]; i++)
		    {
		      if (group == ids[i]->gid)
			{
			  ids[i]->count++;
			  break;
			}
		    }

		  if (!ids[i])
		    {
		      ids[i] = (GROUP_ID *)xmalloc (sizeof (GROUP_ID));
		      ids[i]->gid = group;
		      ids[i]->count = 1;

		      if (i + 1 >= ids_size)
			{
			  ids_size += 20;
			  ids = (GROUP_ID **)
			    xrealloc (ids, ids_size * sizeof (GROUP_ID *));
			}
		      ids[++i] = (GROUP_ID *)NULL;
		    }
		}
	    }

	  re_free_fields (tfields);

	  /* We know the highest user number, and we have a histogram
	     of the group ids.  Use the most common group id as the
	     default. */
	    
	  best_bet = ids[0];

	  for (i = 0; ids[i]; i++)
	    {
	      if (ids[i]->count > best_bet->count)
		best_bet = ids[i];
	    }

	  if (best_bet)
	    re_set_field (fields, "Group ID", best_bet->gid);

	  for (i = 0; ids[i]; i++)
	    free (ids[i]);

	  free (ids);

	  re_set_field (fields, "User ID", highest + 1);
	  re_set_field (fields, "Login Shell",
			savestring ("/usr/local/bin/bash"));
	}

      re_set_field (fields, "User Name", savestring (user));
      re_set_field (fields, "Comment", savestring ("Newly created user."));

      phone.area_code = 818;
      phone.prefix = 356;
      phone.number = 5000;
      re_set_field (fields, "Phone", &phone);

      birthdate.month = 12;
      birthdate.day = 11;
      birthdate.year = 59;
      set_birthdate_field (fields, &birthdate);

      /* Maybe we can default the email field. */
      {
	char hostname[256];

	if (gethostname (hostname, 256) != -1)
	  {
	    char *email =
	      (char *)xmalloc (3 + strlen (user) + strlen (hostname));
	    sprintf (email, "%s@%s", user, hostname);
	    re_set_field (fields, "E-Mail Address", email);
	  }
	else
	  re_set_field (fields, "E-Mail Address", savestring (user));
      }
      info = extract_record (fields);
    }
  else
    {
      dbm_flags = DBM_REPLACE;
      re_extract_fields (info.dptr, fields);
    }

  /* If the user doing the editing is the same as the user being edited,
     then the only fields not changeable are User ID and User Name. */
  {
    char **value, *username;

    value  = (char **)re_get_field_value (fields, "User Name");
    username = *value;

    if (username && strcmp (username, current_user) == 0)
      {
	register int i;
	FIELD_DESC *field;

	for (i = 0; field = fields[i]; i++)
	  if ((field->attributes & FieldKey) != FieldKey &&
	      xstricmp (field->name, "User ID"))
	    field->attributes = field->attributes & ~FieldPrivileged;
      }
  }

  /* Call the editor on this person. */
  edit_user (user, fields);

  /* If the record has been modified, then set the Last Modification field. */
  {
    datum newinfo;

    newinfo = extract_record (fields);

    if (info.dsize != newinfo.dsize ||
	bcmp (info.dptr, newinfo.dptr, info.dsize) != 0)
      {
	FIELD_DESC *field;

	field = re_find_field (fields, MODFIELD);

	if (field)
	  {
	    char *time_string, *ctime (), mod_value[256];
	    long the_time, time ();

	    the_time = time ((long *)0);
	    time_string = ctime (&the_time);
	    time_string[strlen (time_string) - 1] = '\0';
	    
	    field->attributes = field->attributes & ~FieldPrivileged;
	    sprintf (mod_value, "%s on %s", current_user, time_string);
	    re_set_field (fields, MODFIELD, savestring (mod_value));
	  }
      }

    free (newinfo.dptr);
    if (dbm_flags == DBM_INSERT)
      free (info.dptr);
  }

  /* Put the new info in the database. */
  key = extract_key (fields);
  info = extract_record (fields);
  
  if (*key.dptr)
    result = dbm_store (database, key, info, dbm_flags);
  free (info.dptr);
  free (key.dptr);
  re_free_fields (fields);
}

datum
extract_key (fields)
     FIELD_DESC **fields;
{
  datum key;

  key.dsize = re_data_length (fields, FieldKey);
  key.dptr = re_extract_data (fields, FieldKey);

  return (key);
}

datum
extract_record (fields)
     FIELD_DESC **fields;
{
  datum info;

  info.dsize = re_data_length (fields, 0);
  info.dptr = re_extract_data (fields, 0);

  return (info);
}

/* Edit FIELDS using the users favorite editor. */
int
edit_user (user, fields)
     FIELD_DESC **fields;
{
  char tempfile[256];
  char command[1024];
  FILE *file;

  sprintf (tempfile, "/tmp/eu-%d", getpid ());

  file = fopen (tempfile, "w");

  if (!file)
    return (1);

  fprintf (file, "# You are editing the definition of `%s', a Unix user.\n",
	   user);
  fprintf (file, "# Fields beginning with a `*' may not be modified.\n");
  fprintf (file, "# Use the normal editing commands, and save the file\n");
  fprintf (file, "# when you are done.  If you do not save the file, then\n");
  fprintf (file, "# your changes will be ineffective.\n\n");

  re_print_fields (fields, file);
  fclose (file);

  sprintf (command, "%s %s", editor, tempfile);
  system (command);

  file = fopen (tempfile, "r");
  unlink (tempfile);

  if (!file)
    return (1);

  re_eval_fields (fields, file);

  fclose (file);
  return (0);
}
