#!/usr/bin/env perl
#-*- Mode: perl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-

# Users account mannager. Designed to be architecture and distribution independent.
#
# Copyright (C) 2000-2001 Ximian, Inc.
#
# Authors: Hans Petter Jansson <hpj@ximian.com>,
#          Arturo Espinosa <arturo@ximian.com>,
#          Tambet Ingo <tambet@ximian.com>.
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Library 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 Library General Public License for more details.
#
# You should have received a copy of the GNU Library 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.

# Best viewed with 100 columns of width.

# Configuration files affected:
#
# /etc/passwd
# /etc/group
# /etc/shadow
# /etc/login.defs
# /etc/shells
# /etc/skel/

# NIS support will come later.

# Running programs affected/used:
#
# adduser: creating users.
# usermod: modifying user data.
# passwd: assigning or changing passwords. (Un)locking users.
# chfn: modifying finger information - Name, Office, Office phone, Home phone.


BEGIN {
  $SCRIPTSDIR = "/usr/share/setup-tool-backends/scripts";
  if ($SCRIPTSDIR =~ /^___scriptsdir__[_]/)
  {
      $SCRIPTSDIR = ".";
      $DOTIN = ".in";
  }
  
  require "$SCRIPTSDIR/general.pl$DOTIN";
  require "$SCRIPTSDIR/platform.pl$DOTIN";
  require "$SCRIPTSDIR/util.pl$DOTIN";
  require "$SCRIPTSDIR/file.pl$DOTIN";
  require "$SCRIPTSDIR/xml.pl$DOTIN";
  require "$SCRIPTSDIR/replace.pl$DOTIN";
}


# --- Tool information --- #

$name = "users";
$version = "0.11.0";
@platforms = ("redhat-5.2", "redhat-6.0", "redhat-6.1", "redhat-6.2", "redhat-7.0", "redhat-7.1",
              "redhat-7.2",

              "mandrake-7.1", "mandrake-7.2",

              "debian-2.2", "debian-woody",

              "suse-7.0", "turbolinux-7.0");

#, "freebsd-4", "freebsd-5");

$description =<<"end_of_description;";
       Manages system users.
end_of_description;

# --- System config file locations --- #

# We list each config file type with as many alternate locations as possible.
# They are tried in array order. First found = used.

@passwd_names =     ( "/etc/passwd" );
@shadow_names =     ( "/etc/shadow", "/etc/master.passwd" );
@group_names =      ( "/etc/group" );
@login_defs_names = ( "/etc/login.defs", "/etc/adduser.conf" );
@shell_names =      ( "/etc/shells" );
@skel_dir =         ( "/usr/share/skel", "/etc/skel" );

$profile_file =     "profiles.xml";


# Where are the tools?

$cmd_usermod  = &xst_file_locate_tool ("usermod");
$cmd_userdel  = &xst_file_locate_tool ("userdel");
$cmd_useradd  = &xst_file_locate_tool ("useradd");	
$cmd_groupdel = &xst_file_locate_tool ("groupdel");
$cmd_groupadd = &xst_file_locate_tool ("groupadd");
$cmd_groupmod = &xst_file_locate_tool ("groupmod");
$cmd_gpasswd  = &xst_file_locate_tool ("gpasswd");	
$cmd_chfn     = &xst_file_locate_tool ("chfn");

# --- Mapping constants --- #

%users_prop_map = ();
@users_prop_array = (
  "key", 0,
  "login", 1,
  "password", 2,
  "uid", 3,
  "gid", 4,
  "comment", 5,
  "home", 6,
  "shell", 7,
  "last_mod", 8, # Read shadow (5) for these.
  "passwd_min_life", 9,
  "passwd_max_life", 10,
  "passwd_exp_warn", 11,
  "passwd_exp_disable", 12,
  "passwd_disable", 13,
  "reserved", 14,
  "is_shadow", 15,
  "", "");

for ($i = 0; $users_prop_array[$i] ne ""; $i += 2)
{
  $users_prop_map {$users_prop_array[$i]} = $users_prop_array[$i + 1];
  $users_prop_map {$users_prop_array[$i + 1]} = $users_prop_array[$i];
}

%groups_prop_map = ();
@groups_prop_array = (
  "key", 0,
  "name", 1,
	"password", 2,
	"gid", 3,
	"users", 4,
	"", "");

for ($i = 0; $groups_prop_array[$i] ne ""; $i += 2)
{
  $groups_prop_map {$groups_prop_array[$i]} = $groups_prop_array[$i + 1];
  $groups_prop_map {$groups_prop_array[$i + 1]} = $groups_prop_array[$i];
}

%login_defs_prop_map = ();
@login_defs_prop_array =
(
 "QMAIL_DIR" ,     "qmail_dir",
 "MAIL_DIR" ,      "mailbox_dir",
 "MAIL_FILE" ,     "mailbox_file",
 "PASS_MAX_DAYS" , "pwd_maxdays",
 "PASS_MIN_DAYS" , "pwd_mindays",
 "PASS_MIN_LEN" ,  "pwd_min_length",
 "PASS_WARN_AGE" , "pwd_warndays",
 "UID_MIN" ,       "umin",
 "UID_MAX" ,       "umax",
 "GID_MIN" ,       "gmin",
 "GID_MAX" ,       "gmax",
 "USERDEL_CMD" ,   "del_user_additional_command",
 "CREATE_HOME" ,   "create_home",
 "", "");

for ($i = 0; $login_defs_prop_array[$i] ne ""; $i += 2)
{
  $login_defs_prop_map {$login_defs_prop_array[$i]} = $login_defs_prop_array[$i + 1];
  $login_defs_prop_map {$login_defs_prop_array[$i + 1]} = $login_defs_prop_array[$i];
}

%profiles_prop_map = ();
@profiles_prop_array =
(
 "NAME" ,          "name",
 "COMMENT",        "comment",
 "LOGINDEFS",      "login_defs",
 "HOME_PREFFIX",   "home_prefix",
 "SHELL",          "shell",
 "GROUP",          "group",
 "SKEL_DIR",       "skel_dir",
 "QMAIL_DIR" ,     "qmail_dir",
 "MAIL_DIR" ,      "mailbox_dir",
 "MAIL_FILE" ,     "mailbox_file",
 "PASS_RANDOM",    "pwd_random",
 "PASS_MAX_DAYS" , "pwd_maxdays",
 "PASS_MIN_DAYS" , "pwd_mindays",
 "PASS_MIN_LEN" ,  "pwd_min_length",
 "PASS_WARN_AGE" , "pwd_warndays",
 "UID_MIN" ,       "umin",
 "UID_MAX" ,       "umax",
 "GID_MIN" ,       "gmin",
 "GID_MAX" ,       "gmax",
 "USERDEL_CMD" ,   "del_user_additional_command",
 "CREATE_HOME" ,   "create_home",
 "", "");

for ($i = 0; $profiles_prop_array[$i] ne ""; $i += 2)
{
  $profiles_prop_map {$profiles_prop_array[$i]} = $profiles_prop_array[$i + 1];
  $profiles_prop_map {$profiles_prop_array[$i + 1]} = $profiles_prop_array[$i];
}


my $rh_logindefs_defaults = {
  'shell'       => '/bin/bash',
  'group'       => '$user',
  'skel_dir'    => '/etc/skel/',
};

my $logindefs_dist_map = {
  'redhat-5.2'     => $rh_logindefs_defaults,
  'redhat-6.0'     => $rh_logindefs_defaults,
  'redhat-6.1'     => $rh_logindefs_defaults,
  'redhat-6.2'     => $rh_logindefs_defaults,
  'redhat-7.0'     => $rh_logindefs_defaults,
  'redhat-7.1'     => $rh_logindefs_defaults,
  'redhat-7.2'     => $rh_logindefs_defaults,
  'mandrake-7.1'   => $rh_logindefs_defaults,
  'mandrake-7.2'   => $rh_logindefs_defaults,

  # FIXME: I don't know about those, so using RH values for now.
  'debian-2.2'     => $rh_logindefs_defaults,
  'debian-woody'   => $rh_logindefs_defaults,
  'suse-7.0'       => $rh_logindefs_defaults,
  'turbolinux-7.0' => $rh_logindefs_defaults,
};


# Add reporting table.

&xst_report_table ({
  'users_read_profiledb_success' => ['info', _('Profiles read successfully.')],
  'users_read_profiledb_fail'    => ['warn', _('Profiles read failed.')],
  'users_read_users_success'     => ['info', _('Users read successfully.')],
  'users_read_users_fail'        => ['warn', _('Users read failed.')],
  'users_read_groups_success'    => ['info', _('Groups read successfully.')],
  'users_read_groups_fail'       => ['warn', _('Groups read failed.')],
  'users_read_shells_success'    => ['info', _('Shells read successfully.')],
  'users_read_shells_fail'       => ['warn', _('Reading shells failed.')],

  'users_write_profiledb_success' => ['info', _('Profiles written successfully.')],
  'users_write_profiledb_fail'    => ['warn', _('Writing profiles failed.')],
  'users_write_users_success'     => ['info', _('Users written successfully.')],
  'users_write_users_fail'        => ['warn', _('Writing users failed.')],
  'users_write_groups_success'    => ['info', _('Groups written successfully.')],
  'users_write_groups_fail'       => ['warn', _('Writing groups failed.')],
});


# --- Utility stuff --- #

sub max
{
  return $_[0] > $_[1]? $_[0]: $_[1];
}

sub arr_cmp_recurse
{
	my ($a1, $a2) = @_;
	my $i;
	
	return -1 if ($#$a1 != $#$a2);
	
	for ($i = 0; $i <= $#$a1; $i++) {
	  if (ref ($$a1[$i]) eq "ARRAY") { # see if this is a reference.
		  return -1 if &arr_cmp_recurse ($$a1[$i], $$a2[$i]); # we assume it is a ref to an array.
		} elsif ($$a1[$i] ne $$a2[$i]) {
		  return -1;
		}
	}
	
	return 0;
}

sub get_logindefs
{
  my $profiledb = shift;
  return unless $profiledb;

  foreach my $profile (@$profiledb)
  {
    return $profile if (exists ($profile->{'login_defs'}));
  }
}

# --- Configuration manipulation --- #

sub read
{
  my (%hash);

  &read_group         (\%hash);
  &read_passwd_shadow (\%hash);
  &read_profiledb     (\%hash);
  &read_shells        (\%hash);

  return \%hash;
}

sub logindefs_add_defaults
{
  # Common for all distros
  my $logindefs = {
    'name'        => 'Default',
    'comment'     => 'Default profile',
    'login_defs'  => 1,
    'home_prefix' => '/home/$user',
  };

  # Distro specific
  my $dist_specific = $logindefs_dist_map->{$xst_dist};

  # Just to be 100% sure SOMETHING gets filled:
  unless ($dist_specific)
  {
    $dist_specific = $rh_logindefs_defaults;
    &xst_debug_print_line ("logindefs_add_defaults: Couldn't find distro specific parameters.");
  }

  foreach my $key (keys %$dist_specific)
  {
    # Make sure there's no crappy entries
    if (exists ($profiles_prop_map{$key}))
    {
      $logindefs->{$key} = $dist_specific->{$key};
    }
  }
  return $logindefs;
}

sub read_logindefs
{
  my $profiledb = shift;
  my $logindefs =  &get_logindefs ($profiledb);

  unless ($logindefs)
  {
    $logindefs = &logindefs_add_defaults ();
    push @$profiledb, $logindefs;
  }

  # Get new data in case someone has changed login_defs manually.
  my $fh = &xst_file_open_read_from_names (@login_defs_names);
  return unless $fh;

  while (<$fh>)
  {
    next if &xst_ignore_line ($_);
    chomp;
    my @line = split /[ \t]+/;
    if (exists $login_defs_prop_map{$line[0]})
    {
      $logindefs->{$login_defs_prop_map{$line[0]}} = $line[1];
    }
  }
  close $fh;
}


sub read_profiledb
{
  my ($hash) = @_;
  my $path;
  my $profiles = [];

  $$hash{'profiledb'} = $profiles;

  $path = &xst_file_get_data_path () . "/" . $main::tool->{'name'} . "/" . $profile_file;
  my $tree = &xst_xml_scan ($path, $tool);

  if ($tree && scalar @$tree)
  {
    if ($$tree[0] eq 'profiledb')
    {
      &xml_parse_profiledb ($$tree[1], $hash);
    }
    else
    {
      &xst_report ('xml_unexp_tag', $$tree[0]);
    }
  }

  &read_logindefs ($profiles);

  if (scalar @$profiles)
  {
    &xst_report ('users_read_profiledb_success');
  }
  else
  {
    &xst_report ('users_read_profiledb_fail');
  }
}

sub read_passwd_shadow
{
  my ($hash) = @_;
  my ($ifh, @users, %users_hash, $passwd_last_modified);
  my (@line, $copy, %tmphash);
  my $login_pos = $users_prop_map{"login"};
  my $i = 0;

  # Find the passwd file.

  $ifh = &xst_file_open_read_from_names(@passwd_names);
  unless ($ifh)
  {
    &xst_report ('users_read_users_fail');
    return;
  }
  $passwd_last_modified = (stat ($ifh))[9]; # &get the mtime.

  # Parse the file.

  @users = ();
  %users_hash = ();

  while (<$ifh>)
  {
    chomp;
    # FreeBSD allows comments in the passwd file. */
    next if ($_ =~ /\#.*/);
    $_ = &xst_xml_unquote ($_);
    
    @line = split ':', $_, -1;
    unshift @line, sprintf ("%06d", $i);
    $copy = [@line];
    $users_hash{sprintf ("%06d", $i)} = $copy;
    $tmphash{$line[$login_pos]} = $copy;
    push (@users, $copy);
    $i ++;
  }
  &xst_file_close ($ifh);
	
  # Find the shadow file.

  $ifh = &xst_file_open_read_from_names(@shadow_names);
  if ($ifh) {
    my ($login, $passwd);
    my $passwd_pos = $users_prop_map{"password"};

    while (<$ifh>)
    {
      chomp;
      # FreeBSD allows comments in the shadow passwd file. */
      next if ($_ =~ /\#.*/);
      @line = split ':', $_, -1;
      push @line, 1;
      $login = shift @line;
      $passwd = shift @line;
      push @{$tmphash{$login}}, @line;
      @{$tmphash{$login}}[$passwd_pos] = $passwd;
    }

    &xst_file_close ($ifh);
  }

  $$hash{"users"}      = \@users;
  $$hash{"users_hash"} = \%users_hash;
  $$hash{"passwd_last_modified"} = $passwd_last_modified;

  if (scalar @users)
  {
    &xst_report ('users_read_users_success');
  }
  else
  {
    &xst_report ('users_read_users_fail');
  }
}

sub read_group
{
  my ($hash) = @_;
  my ($ifh, @groups, %groups_hash, $group_last_modified);
  my (@line, $copy, @a);
  my $i = 0;

  # Find the file.

  $ifh = &xst_file_open_read_from_names(@group_names);
  unless ($ifh)
  {
    &xst_report ('users_read_groups_fail');
    return;
  }
  $group_last_modified = (stat ($ifh))[9]; # &get the mtime.

  # Parse the file.
	
  @groups = ();
  %groups_hash = ();

  while (<$ifh>)
  {
    chomp;
    $_ = &xst_xml_unquote ($_);
    @line = split ':', $_, -1;
    unshift @line, sprintf ("%06d", $i);
    @a = split ',', pop @line;
    push @line, [@a];
    $copy = [@line];
    $groups_hash{sprintf ("%06d", $i)} = $copy;
    push (@groups, $copy);
    $i ++;
  }
  &xst_file_close ($ifh);

  $$hash{"groups"}      = \@groups;
  $$hash{"groups_hash"} = \%groups_hash;
  $$hash{"group_last_modified"} = $group_last_modified;

  if (scalar @groups)
  {
    &xst_report ('users_read_groups_success');
  }
  else
  {
    &xst_report ('users_read_groups_fail');
  }
}

sub read_shells
{
  my ($hash) = @_;
  my ($ifh, @shells);

  # Init @shells, I think every *nix has /bin/false.
  push (@shells, "/bin/false") if (stat ("/bin/false"));
  
  $ifh = &xst_file_open_read_from_names(@shell_names);
  return unless $ifh;

  while (<$ifh>)
  {
    next if &xst_ignore_line ($_);
    chomp;
    push (@shells, $_) if (stat ($_) ne "");
  }
  &xst_file_close ($ifh);

  $$hash{"shelldb"} = \@shells;
  &xst_report ('users_read_shells_success');
}


sub write_group_passwd
{
  my ($hash) = @_;
  my ($users, $users_hash, $groups, $groups_hash);
  my ($passwd_last_modified, $group_last_modified);
  my ($i, $j, $k);
  my (%old_hash);
  my (%users_all, $parse_users_hash, $parse_users, $parse_passwd_last_modified);
  my (%groups_all, $parse_groups_hash, $parse_groups, $parse_group_last_modified);

  $parse_users = $$hash{"users"};
  $parse_users_hash = $$hash{"users_hash"};
  $parse_passwd_last_modified = $$hash{"passwd_last_modified"};
  $parse_groups = $$hash{"groups"};
  $parse_groups_hash = $$hash{"groups_hash"};
  $parse_group_last_modified = $$hash{"group_last_modified"};

  &read_passwd_shadow (\%old_hash);
  &read_group (\%old_hash);

  $users = $old_hash{"users"};
  $users_hash = $old_hash{"users_hash"};
  $passwd_last_modified = $old_hash{"passwd_last_modified"};
  $groups = $old_hash{"groups"};
  $groups_hash = $old_hash{"groups_hash"};
  $group_last_modified = $old_hash{"group_last_modified"};

#	if ($passwd_last_modified > $parse_passwd_last_modified) 
#	{
#	  print STDERR "Password file may be inconsistent! No changes made.\n";
#		return;
#	}

  foreach $i (keys (%$users_hash)) 
	{
		$users_all{$i} |= 1;
	}	
	
	foreach $i (keys (%$parse_users_hash))
	{
	  $users_all{$i} |= 2;
	}
	
  foreach $i (keys (%$groups_hash)) 
	{
		$groups_all{$i} |= 1;
	}	
	
	foreach $i (keys (%$parse_groups_hash))
	{
	  $groups_all{$i} |= 2;
	}
	
	foreach $i (sort (keys (%users_all)))
	{
	  &del_user ($$users_hash{$i}) if ($users_all{$i} == 1);
	}

  foreach $i (sort (keys (%groups_all)))
	{
	  &del_group ($$groups_hash{$i}) if ($groups_all{$i} == 1);
	}
	
	foreach $i (sort (keys (%groups_all)))
	{
	  &add_group ($$parse_groups_hash{$i}) if ($groups_all{$i} == 2);
	}
	
	foreach $i (sort (keys (%users_all)))
	{
	  &add_user ($$parse_users_hash{$i}) if ($users_all{$i} == 2);
	}

	foreach $i (sort (keys (%groups_all)))
	{
	  if ($groups_all{$i} == 3 && &arr_cmp_recurse ($$groups_hash{$i}, $$parse_groups_hash{$i}))
		{
		  &change_group ($$groups_hash{$i}, $$parse_groups_hash{$i});
		}
	}
	
	foreach $i (sort (keys (%users_all)))
	{
	  if ($users_all{$i} == 3 && &arr_cmp_recurse ($$users_hash{$i}, $$parse_users_hash{$i}))
		{
		  &change_user ($$users_hash{$i}, $$parse_users_hash{$i});
		}
	}

  &xst_report ('users_write_users_success');
  &xst_report ('users_write_groups_success');
}

sub del_user
{
	my ($data) = @_;
  my ($command);
	
	$command = "$cmd_userdel -r \'" . $$data[$users_prop_map{"login"}] . "\'";
	&xst_file_run ($command);
}

sub change_user_chfn
{
  my ($comment, $username) = @_;
  my ($fname, $office, $office_phone, $home_phone);
  my ($command, @line);

  return if !$username;
  
  @line = split /\,/, $comment;
  ($fname, $office, $office_phone, $home_phone) = @line;

  $fname = "-f \'" . $fname . "\'";  
  $home_phone = "-h \'" . $home_phone . "\'";

  if ($xst_dist =~ /^debian/)
  {
    $office = "-r \'" . $office . "\'";
    $office_phone = "-w \'" . $office_phone . "\'";
  }
  else
  {
    $office = "-o \'" . $office . "\'";
    $office_phone = "-p \'" . $office_phone . "\'";
  }  
  
  $command = "$cmd_chfn $fname $office $office_phone $home_phone $username";
  &xst_file_run ($command);
}

sub add_user
{
	my ($data) = @_;
	my ($home_parents, $tool_mkdir);
  
  $home_parents = $$data[$users_prop_map{"home"}];
  $home_parents =~ s/\/+[^\/]+\/*$//;

  $tool_mkdir = &xst_file_locate_tool ("mkdir");
  &xst_file_run ("$tool_mkdir -p $home_parents");
  
  $command = "$cmd_useradd" . " -d \'" . $$data[$users_prop_map{"home"}] .
	  "\' -g \'" . $$data[$users_prop_map{"gid"}] .
	  "\' -m -p \'" . $$data[$users_prop_map{"password"}] .
	  "\' -s \'" . $$data[$users_prop_map{"shell"}] .
	  "\' -u \'" . $$data[$users_prop_map{"uid"}] .
	  "\' \'" . $$data[$users_prop_map{"login"}] . "\'";
	&xst_file_run ($command);

  &change_user_chfn ($$data[$users_prop_map{"comment"}], $$data[$users_prop_map{"login"}]);
}

sub change_user
{
	my ($old_data, $new_data) = @_;
	
  $command = "$cmd_usermod" . " -d \'" . $$new_data[$users_prop_map{"home"}] .
	  "\' -g \'" . $$new_data[$users_prop_map{"gid"}] .
	  "\' -l \'" . $$new_data[$users_prop_map{"login"}] .
	  "\' -p \'" . $$new_data[$users_prop_map{"password"}] .
	  "\' -s \'" . $$new_data[$users_prop_map{"shell"}] .
	  "\' -u \'" . $$new_data[$users_prop_map{"uid"}] .
	  "\' \'" . $$old_data[$users_prop_map{"login"}] . "\'";
	&xst_file_run ($command);

  &change_user_chfn ($$new_data[$users_prop_map{"comment"}], $$new_data[$users_prop_map{"login"}]);
}

sub del_group
{
	my ($data) = @_;

	$command = "$cmd_groupdel \'" . $$data[$groups_prop_map{"name"}] . "\'";
	&xst_file_run ($command);
}

sub add_group
{
	my ($data) = @_;
	
	$command = "$cmd_groupadd -g \'" . $$data[$groups_prop_map{"gid"}] .
	  "\' " . $$data[$groups_prop_map{"name"}];
	&xst_file_run ($command);
}

sub change_group
{
	my ($old_data, $new_data) = @_;
	my ($n, $o, $i, $j, $max_n, $max_o, $r, @tmp); # for iterations

	$command = "$cmd_groupmod -g \'" . $$new_data[$groups_prop_map{"gid"}] .
	  "\' -n \'" . $$new_data[$groups_prop_map{"name"}] . "\'" 
		. " \'" . $$old_data[$groups_prop_map{"name"}] . "\'";
	&xst_file_run ($command);
	
	# Let's see if the users that compose the group have changed.
	if (&arr_cmp_recurse ($$new_data[$groups_prop_map{"users"}],
	                      $$old_data[$groups_prop_map{"users"}])) {

		$n = [ @{$$new_data[$groups_prop_map{"users"}]} ]; sort @$n;
		$o = [ @{$$old_data[$groups_prop_map{"users"}]} ]; sort @$o;
		
		$max_n = $#$n;
		$max_o = $#$o;
		for ($i = 0, $j = 0; $i <= &max ($max_n, $max_o); ) {
		  $r = $$n[$i] cmp $$o[$j];
			$r *= -1 if (($$o[$j] eq "") || ($$n[$i] eq ""));
		  if ($r < 0) { # add this user to the group.
			  $command = "$cmd_gpasswd -a \'" . $$n[$i] . "\' \'" . 
				  $$new_data[$groups_prop_map{"name"}] . "\'";
				$i ++;
				
				&xst_file_run ($command);
			} elsif ($r > 0) { # delete the user from the group.
			  $command = "$cmd_gpasswd -d \'" . $$o[$j] . "\' \'" . 
				  $$new_data[$groups_prop_map{"name"}] . "\'";
				$j ++;
				
				&xst_file_run ($command);
			} else { # The information is the same. Go to next tuple.
			  $i ++; $j ++;
			}	
		}	
	}	
}


sub write_logindefs
{
  my ($login_defs) = @_;
  my ($key);
  my $file;

  return unless $login_defs;

  foreach $key (@login_defs_names)
  {
    if (-e $key)
    {
      &xst_debug_print_line ("write_logindefs:$key");
      $file = $key;
      last;
    }
  }

  unless ($file) 
  {
    &xst_report ("file_open_read_failed", join (", ", @login_defs_names));
    return;
  }

  foreach $key (keys (%$login_defs))
  {
    # Write ONLY login.defs values.
    if (exists ($login_defs_prop_map{$key}))
    {
      &xst_replace_split ($file, $login_defs_prop_map{$key}, "[ \t]+", $$login_defs{$key});
    }
  }
}


sub write_profiledb
{
  my ($hash) = @_;
  my $profiledb = $hash->{'profiledb'};

  unless ($profiledb)
  {
    &xst_report ('users_write_profiledb_fail');
    return;
  }

  # Update login.defs file.  
  &write_logindefs (&get_logindefs ($profiledb));

  # Write our profiles.
  my $path = &xst_file_get_data_path () . "/" . $main::tool->{'name'} . "/" . $profile_file;
  my $fh = &xst_file_open_write_from_names ($path);
  if ($fh)
  {
    local *STDOUT = $fh;
    &xml_print_profiledb ($hash);
    close ($fh);
    &xst_report ('users_write_profiledb_success');
  }
  else
  {
    &xst_report ('users_write_profiledb_fail');
  }
}


# --- XML parsing --- #

# Scan XML from standard input to an internal tree.

sub xml_parse
{
  my ($tool) = @_;
  my ($tree, %hash);
	
  # Scan XML to tree.

  $tree = &xst_xml_scan (undef, $tool);

  $hash{"users"} = [];
  $hash{"users_hash"} = {};
  $hash{"groups"} = [];
  $hash{"groups_hash"} = {};
  $hash{"profiledb"} = [];

  # Walk the tree recursively and extract configuration parameters.
  # This is the top level - find and enter the "users" tag.

  while (@$tree)
  {
    if ($$tree[0] eq "users") { &xml_parse_users($$tree[1], \%hash); }

    shift @$tree;
    shift @$tree;
  }

  return (\%hash);
}

sub xml_parse_users
{
  my ($tree, $hash) = @_;
	
  shift @$tree;  # Skip attributes.

  while (@$tree)
	{
	  if ($$tree[0] eq "logindefs") { &xml_parse_login_defs ($$tree[1], $hash); }
		elsif ($$tree[0] eq "passwd_last_modified") { &xml_parse_passwd_last_modified ($$tree[1], $hash); }
		elsif ($$tree[0] eq "group_last_modified") { &xml_parse_group_last_modified ($$tree[1], $hash); }
		elsif ($$tree[0] eq "userdb") { &xml_parse_userdb ($$tree[1], $hash); }
		elsif ($$tree[0] eq "groupdb") { &xml_parse_groupdb ($$tree[1], $hash); }
		elsif ($$tree[0] eq "shelldb")  { }
		elsif ($$tree[0] eq "profiledb")  { &xml_parse_profiledb ($$tree[1], $hash); }
		else
		{
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}

    shift @$tree;
    shift @$tree;
  }
}

sub xml_parse_passwd_last_modified
{
  my ($tree, $hash) = @_;
	
  shift @$tree;  # Skip attributes.
	
	&xst_report ("xml_unexp_arg", "", "passwd_last_modified") if ($$tree[0] ne "0");
	$$hash{"passwd_last_modified"} = $$tree[1];
}

sub xml_parse_group_last_modified
{
  my ($tree, $hash) = @_;
	
  shift @$tree;  # Skip attributes.
	
	&xst_report ("xml_unexp_arg", "", "group_last_modified") if ($$tree[0] ne "0");
	$$hash{"group_last_modified"} = $$tree[1];
}	

sub xml_parse_userdb
{
  my ($tree, $hash) = @_;
	
  shift @$tree;  # Skip attributes.

  while (@$tree)
	{
	  if ($$tree[0] eq "user") { &xml_parse_user ($$tree[1], $hash); }
		else
		{
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}

    shift @$tree;
    shift @$tree;
  }
}

sub xml_parse_user
{
  my ($tree, $hash) = @_;
  my ($users, $users_hash);
  my @line = ();

  $users = $$hash{"users"};
  $users_hash = $$hash{"users_hash"};
	
  shift @$tree;  # Skip attributes.

	while (@$tree)
	{
		if ($users_prop_map{$$tree[0]} ne undef)
		{
		  $line[$users_prop_map{$$tree[0]}] = &xst_xml_unquote($$tree[1][2]);
		}
		else
		{
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}
		
		shift @$tree;
		shift @$tree;
	}

  $$users_hash{sprintf ("%06d", $line[0])} = [@line];
  push (@$users, [@line]);
}	
	
sub xml_parse_groupdb
{
  my ($tree, $hash) = @_;
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.

  while (@$tree)
  {
    if ($$tree[0] eq "group") { &xml_parse_group ($$tree[1], $hash); }
    else
    {
		  &xst_report ("xml_unexp_tag", $$tree[0]);
    }

    shift @$tree;
    shift @$tree;
  }
}

sub xml_parse_group
{
  my ($tree, $hash) = @_;
	my (@line, $copy, $a, @u);
  my ($groups, $users_hash);
	
  $groups = $$hash{"groups"};
  $groups_hash = $$hash{"groups_hash"};
	
  shift @$tree;  # Skip attributes.

	while (@$tree)
	{
		if ($groups_prop_map{$$tree[0]} ne undef)
		{
		  if ($$tree[0] eq "users") { $line[$groups_prop_map{$$tree[0]}] = $$tree[1]; }
			else { $line[$groups_prop_map{$$tree[0]}] = $$tree[1][2]; }
		}
		else
		{
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}
		
		shift @$tree;
		shift @$tree;
	}
	
	# @$a should be a parse tree of the array of users.
	$a = pop @line;
	shift @$a;
	while (@$a) {
	  if ($$a[0] eq "user") {
		  push @u, $$a[1][2];
		}
		else
		{
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}
		shift @$a;
		shift @$a;
	}
	
	push @line, [@u];
	$copy = [@line];
	$$groups_hash{sprintf ("%06d", $line[0])} = $copy;
	push (@$groups, $copy);
}	

sub xml_parse_profile
{
  my ($tree, $hash) = @_;
  my (%profile);

  shift @$tree;  # Skip attributes.

  while (@$tree)
	{
		if ($profiles_prop_map{$$tree[0]})
		{
		  $profile{$$tree[0]} = $$tree[1][2];
		}
		elsif ($$tree[0] ne "files") # files tag is ignored for parsing. # FIXME!
    {
		  &xst_report ("xml_unexp_tag", $$tree[0]);
		}

    shift @$tree;
		shift @$tree;
	}

  push @{$hash->{'profiledb'}}, \%profile;
}

sub xml_parse_profiledb
{
  my ($tree, $hash) = @_;

  shift @$tree; # Skip attributes.

  while (@$tree)
  {
    if ($$tree[0] eq "profile") { &xml_parse_profile ($$tree[1], $hash); }
    else
    {
		  &xst_report ("xml_unexp_tag", $$tree[0]);
    }

    shift @$tree;
    shift @$tree;
  }
}


# --- XML printing --- #

sub xml_print_profiledb
{
  my ($hash) = @_;

  my $profiledb = $$hash{"profiledb"};

  return unless scalar @$profiledb;

  &xst_xml_container_enter ('profiledb');

  foreach my $profile (@$profiledb)
  {
    my $key;
    &xst_xml_container_enter ('profile');
    foreach $key (keys %$profile)
    {
      &xst_xml_print_pcdata ($key, $profile->{$key});
    }
    &xst_xml_container_leave ();
  }

  &xst_xml_container_leave ();
	&xst_xml_print_vspace ();
}

sub xml_print_shells
{
  my ($hash) = @_;
  my ($i, $shells);

  $shells = $$hash{"shelldb"};
  return unless scalar @$shells;

  &xst_xml_container_enter ('shelldb');

  foreach $i (@$shells) {
    &xst_xml_print_pcdata ('shell', $i);
  }

  &xst_xml_container_leave ();
	&xst_xml_print_vspace ();
}

sub xml_print
{
  my ($hash) = @_;
  my ($key, $value, $i, $j, $k);
  my ($passwd_last_modified, $users);

  $passwd_last_modified = $$hash{"passwd_last_modified"};
  $users = $$hash{"users"};
  $group_last_modified = $$hash{"group_last_modified"};
  $groups = $$hash{"groups"};

  &xst_xml_print_begin ();

  &xst_xml_print_vspace ();
  &xst_xml_print_comment ('Profiles configuration starts here');
  &xst_xml_print_vspace ();

  &xml_print_profiledb ($hash);
  &xml_print_shells ($hash);

  &xst_xml_print_comment ('Now the users');
  &xst_xml_print_vspace ();

  &xst_xml_print_comment ('When was the passwd file last modified (since the epoch)?');
  &xst_xml_print_vspace ();
  &xst_xml_print_pcdata ('passwd_last_modified', $passwd_last_modified);
  &xst_xml_print_vspace ();

  &xst_xml_container_enter ('userdb');
	foreach $i (@$users)
	{
    &xst_xml_print_vspace ();
	  &xst_xml_container_enter ('user');
		for ($j = 0; $j < ($#users_prop_array - 1) / 2; $j++)
    {
      &xst_xml_print_pcdata ($users_prop_map{$j}, $$i[$j]);
		}
		&xst_xml_container_leave ();
	}
	&xst_xml_container_leave ();
  &xst_xml_print_vspace ();
	
  &xst_xml_print_comment ('Now the groups');
  &xst_xml_print_vspace ();
	
  &xst_xml_print_comment ('When was the group file last modified (since the epoch)?');
  &xst_xml_print_vspace ();
  &xst_xml_print_pcdata ('group_last_modified', $group_last_modified);
  &xst_xml_print_vspace ();
	
	&xst_xml_container_enter ('groupdb');
	foreach $i (@$groups)
	{
    &xst_xml_print_vspace ();
	  &xst_xml_container_enter ('group');
		for ($j = 0; $j < ($#groups_prop_array - 1) / 2 - 1; $j++)
    {
      &xst_xml_print_pcdata ($groups_prop_map{$j}, $$i[$j]);
		}
		&xst_xml_container_enter ('users');
		$k = $$i[$groups_prop_map{"users"}];
		foreach $j (@$k)
		{
			&xst_xml_print_pcdata ('user', $j);
		}
		&xst_xml_container_leave ();

		&xst_xml_container_leave ();
	}
	&xst_xml_container_leave ();
  &xst_xml_print_vspace ();

  &xst_xml_print_end ();
}


# --- Get (read) config --- #

sub get
{
  my ($tool) = @_;
  my ($hash);
  
  $hash = &read ();
  &xst_report_end ();
  &xml_print ($hash);
}

sub set
{
  my ($tool) = @_;
  my ($hash);

  $hash = &xml_parse ($tool);

  if ($hash)
  {
    # Make backup manually, otherwise they don't get backed up.
    &xst_file_backup ($_) foreach (@passwd_names);
    &xst_file_backup ($_) foreach (@shadow_names);
    &xst_file_backup ($_) foreach (@group_names);

    &write_profiledb ($hash);
    &write_group_passwd ($hash);
  }

  &xst_report_end ();
}


# --- Filter config: XML in, XML out --- #


sub filter
{
  my ($tool) = @_;
  my ($hash);
  
  $hash = &xml_parse ($tool);
  &xst_report_end ();
  &xml_print ($hash);
}


# --- Main --- #

# get, set and filter are special cases that don't need more parameters than a ref to their function.
# Read general.pl.in:xst_run_directive to know about the format of this hash.

$directives = {
  "get"    => [ \&get,    [], "" ],
  "set"    => [ \&set,    [], "" ],
  "filter" => [ \&filter, [], "" ]
    };

$tool = &xst_init ($name, $version, $description, $directives, @ARGV);
&xst_platform_ensure_supported ($tool, @platforms);
&xst_run ($tool);
