#!/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> and Arturo Espinosa <arturo@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

# 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.


require "/usr/share/ximian-setup-tools/scripts/general.pl";
require "/usr/share/ximian-setup-tools/scripts/platform.pl";
require "/usr/share/ximian-setup-tools/scripts/util.pl";
require "/usr/share/ximian-setup-tools/scripts/file.pl";
require "/usr/share/ximian-setup-tools/scripts/xml.pl";


$DEBUG = 1; # 1 == command; no debug. 2 == no commands, just report. 3 == report and command.
$TEST_NIS = 0;

# --- Tool information --- #

$name = "users";
$version = "0.1.0";
@platforms = ("redhat-5.2", "redhat-6.0", "redhat-6.1", "redhat-6.2", "redhat-7.0");

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

$progress_max = 3;



# --- 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" );
@group_names = ( "/etc/group" );
@login_defs_names = ( "/etc/login.defs" );

# Where are the tools?

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


# --- Internal configuration variables --- #

# Configuration is parsed/read to, and printed/written from, these temporary global variables.

%users_hash = ();
@users = ();
%groups_hash = ();
@groups = ();
%login_defs = ();

$parse_tree = 0;

$passwd_last_modified = 0; # For /etc/passwd, since the epoch.
$group_last_modified = 0; # For /etc/group, since the epoch.

# --- 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" , "passwd_max_day_use",
  "PASS_MIN_DAYS" , "passwd_min_day_use",
  "PASS_MIN_LEN" , "passwd_min_length",
  "PASS_WARN_AGE" , "passwd_warning_advance_days",
  "UID_MIN" , "new_user_min_id",
  "UID_MAX" , "new_user_max_id",
  "GID_MIN" , "new_group_min_id",
  "GID_MAX" , "new_group_max_id",
  "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];
}


# --- Utility stuff --- #

sub my_system
{
  print STDERR "$_[0]\n";
  print "$_[0]\n" if ($DEBUG & 2);
  system ($_[0]) if ($DEBUG & 1);
}

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;
}


# --- Confignuration manipulation --- #

sub read_login_defs
{
  my $ifh;
  local *FILE;
  my @line;
	
  $ifh = &xst_open_read_from_names(@login_defs_names);
  if (not $ifh) { return; }  # We didn't find it.
  *FILE = $ifh;

  while (<FILE>)
  {
    next if &xst_ignore_line ($_);
    chomp;
    $_ = &xst_xml_unquote ($_);
    @line = split (/[ \t]+/, $_);
    $login_defs{$login_defs_prop_map{$line[0]}} = $line[1];
  }

  close (FILE);
}
		
sub read_passwd_shadow
{
  my $ifh;
  local *FILE;
  my (@line, $copy, %tmphash);
  my $login_pos = $users_prop_map{"login"};
  my $i = 0;

  # Find the passwd file.

  $ifh = &xst_open_read_from_names(@passwd_names);
  if (not $ifh) { return; }  # We didn't find it.
  *FILE = $ifh;
    $passwd_last_modified = (stat (FILE))[9]; # &get the mtime.

  # Parse the file.

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

  while (<FILE>)
  {
    chomp;
    $_ = &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 ++;
  }

  close (FILE);
	
  # Find the shadow file.

  *FILE = &xst_open_read_from_names(@shadow_names);
  if (*FILE) {
    my ($login, $passwd);
    my $passwd_pos = $users_prop_map{"password"};

    while (<FILE>)
    {
      chomp;
      @line = split ':', $_, -1;
      push @line, 1;
      $login = shift @line;
      $passwd = shift @line;
      push @{$tmphash{$login}}, @line;
      @{$tmphash{$login}}[$passwd_pos] = $passwd;
    }

    close (FILE);
  }	
}

sub read_group
{
  my $ifh;
  local *FILE;
  my (@line, $copy, @a);
  my $i = 0;

  # Find the file.

  $ifh = &xst_open_read_from_names(@group_names);
  if (not $ifh) { return; }  # We didn't find it.
  *FILE = $ifh;
  $group_last_modified = (stat (FILE))[9]; # &get the mtime.

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

  while (<FILE>)
  {
    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 ++;
  }

  close (FILE);
}

sub write_group_passwd
{
  my ($i, $j, $k);
  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_hash;
  @parse_users = @users;
  $parse_passwd_last_modified = $passwd_last_modified;
  &read_passwd_shadow;
  &xst_progress(10);

  %parse_groups_hash = %groups_hash;
  @parse_groups = @groups;
  $parse_group_last_modified = $group_last_modified;
  &read_group;
  &xst_progress(20);
	
#	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;
	}
	
	&xst_progress(25);
	
	foreach $i (sort (keys (%users_all)))
	{
	  &del_user ($users_hash{$i}) if ($users_all{$i} == 1);
	}
	
	&xst_progress(35);
	
	foreach $i (sort (keys (%groups_all)))
	{
	  &del_group ($groups_hash{$i}) if ($groups_all{$i} == 1);
	}
	
	&xst_progress(45);
	
	foreach $i (sort (keys (%groups_all)))
	{
	  &add_group ($parse_groups_hash{$i}) if ($groups_all{$i} == 2);
	}
	
	&xst_progress(55);
	
	foreach $i (sort (keys (%users_all)))
	{
	  &add_user ($parse_users_hash{$i}) if ($users_all{$i} == 2);
	}

	&xst_progress(70);
	
	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});
		}
	}
	
	&xst_progress(85);
	
	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_progress(95);
}

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

sub add_user
{
	my $data = $_[0];
	
	$command = "$cmd_useradd -c \'" . $$data[$users_prop_map{"comment"}] . 
	  "\' -d \'" . $$data[$users_prop_map{"home"}] .
	  "\' -g \'" . $$data[$users_prop_map{"gid"}] .
	  "\' -p \'" . $$data[$users_prop_map{"password"}] .
	  "\' -s \'" . $$data[$users_prop_map{"shell"}] .
	  "\' -u \'" . $$data[$users_prop_map{"uid"}] .
	  "\' \'" . $$data[$users_prop_map{"login"}] . "\'";
	&my_system ($command);
}

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

sub del_group
{
	my $data = $_[0];

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

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

sub change_group
{
	my $old_data = $_[0];
	my $new_data = $_[1];
	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"}] . "\'";
	&my_system ($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 ++;
				
				&my_system ($command);
			} elsif ($r > 0) { # delete the user from the group.
			  $command = "$cmd_gpasswd -d \'" . $$o[$j] . "\' \'" . 
				  $$new_data[$groups_prop_map{"name"}] . "\'";
				$j ++;
				
				&my_system ($command);
			} else { # The information is the same. Go to next tuple.
			  $i ++; $j ++;
			}	
		}	
	}	
}

# --- XML parsing --- #


# Scan XML from standard input to an internal tree.

sub xml_parse
{
  my $tree;
	
  # Scan XML to tree.

  $parse_tree = $tree = &xst_xml_scan;

  # 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]); }

    shift @$tree;
    shift @$tree;
  }

  return($tree);
}

sub xml_parse_users
{
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.

  while (@$tree)
	{
	  if ($$tree[0] eq "logindefs") { &xml_parse_login_defs ($$tree[1]); }
		elsif ($$tree[0] eq "passwd_last_modified") { &xml_parse_passwd_last_modified ($$tree[1]); }
		elsif ($$tree[0] eq "group_last_modified") { &xml_parse_group_last_modified ($$tree[1]); }
		elsif ($$tree[0] eq "userdb") { &xml_parse_userdb ($$tree[1]); }
		elsif ($$tree[0] eq "groupdb") { &xml_parse_groupdb ($$tree[1]); }
		else
		{
		  &xst_report_warning(1, "Unexpected tag " . $$tree[0] . " while parsing users");
		}

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

sub xml_parse_login_defs
{
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.
	
	while (@$tree)
	{
		if ($login_defs_prop_map{$$tree[0]} ne undef)
		{
		  $login_defs{$$tree[0]} = $$tree[1][2];
		}
		else
		{
		  &xst_report_warning(2, "Unexpected tag " . $$tree[0] . " while parsing logindefs");
		}
		
		shift @$tree;
		shift @$tree;
	}
}	

sub xml_parse_passwd_last_modified
{
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.
	
	&xst_report_warning(3, "passwd_last_modified tag contents is not PCDATA only") if ($$tree[0] ne "0");
	$passwd_last_modified = $$tree[1];
}	

sub xml_parse_group_last_modified
{
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.
	
	&xst_report_warning(4, "group_last_modified tag contents is not PCDATA only") if ($$tree[0] ne "0");
	$group_last_modified = $$tree[1];
}	

sub xml_parse_userdb
{
  my $tree = $_[0];
	
  shift @$tree;  # Skip attributes.

  while (@$tree)
	{
	  if ($$tree[0] eq "user") { &xml_parse_user ($$tree[1]); }
		else
		{
		  &xst_report_warning(5, "Unexpected tag " . $$tree[0] . " while parsing userdb");
		}

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

sub xml_parse_user
{
  my $tree = $_[0];
  my @line = ();
	
  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_warning(6, "Unexpected tag " . $$tree[0] . " while parsing user");
		}
		
		shift @$tree;
		shift @$tree;
	}

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

  while (@$tree)
  {
    if ($$tree[0] eq "group") { &xml_parse_group ($$tree[1]); }
    else
    {
      &xst_report_warning(7, "Unexpected tag " . $$tree[0] . " while parsing groupdb");
    }

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

sub xml_parse_group
{
  my $tree = $_[0];
	my (@line, $copy, $a, @u);
	
  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_warning(9, "Unexpected tag " . $$tree[0] . " while parsing group");
		}
		
		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_warning(8, "Unexpected tag " . $$a[0] . " while parsing users");
		}
		shift @$a;
		shift @$a;
	}
	
	push @line, [@u];
	$copy = [@line];
	$groups_hash{sprintf ("%06d", $line[0])} = $copy;
	push (@groups, $copy);
}	
	
  
# --- XML printing --- #

sub xml_print_nis
{
  my ($key, $i, $j, $k);
  
  &xst_xml_print ("<nis_userdb>\n");
	&xst_xml_enter ();
	foreach $i (@users)
	{
          &xst_xml_vspace ();
	  &xst_xml_print ("<user>\n");
		&xst_xml_enter ();
		for ($j = 0; $j < ($#users_prop_array - 1) / 2; $j++) {
		  &xst_xml_print ("<" . $users_prop_map{"$j"} . ">" . $$i[$j] . "</" . $users_prop_map {$j} . ">\n");
		}
		&xst_xml_leave ();
		&xst_xml_print ("</user>\n");
	}
	&xst_xml_leave ();
  
  &xst_xml_vspace ();
  &xst_xml_print ("</nis_userdb>\n");
	
  &xst_xml_print ("<!-- Now the groups -->\n");
  &xst_xml_vspace ();
	
	&xst_xml_print ("<nis_groupdb>\n");
	&xst_xml_enter ();
	foreach $i (@groups)
	{
    &xst_xml_vspace ();
	  &xst_xml_print ("<group>\n");
		&xst_xml_enter ();
		for ($j = 0; $j < ($#groups_prop_array - 1) / 2 - 1; $j++) {
			&xst_xml_print ("<" . $groups_prop_map{$j} . ">" . $$i[$j] . "</" . $groups_prop_map {$j} . ">\n");
		}
		&xst_xml_print ("<users>\n");
		&xst_xml_enter ();
		$k = $$i[$groups_prop_map{"users"}];
		foreach $j (@$k)
		{
			&xst_xml_print ("<user>$j</user>\n");
		}
		&xst_xml_leave ();
		&xst_xml_print ("</users>\n");

		&xst_xml_leave ();
		&xst_xml_print ("</group>\n");
	}
	&xst_xml_leave ();
  &xst_xml_vspace ();
  &xst_xml_print ("</nis_groupdb>\n");
}

sub xml_print
{
  my ($key, $i, $j, $k);

  print "<?xml version='1.0' encoding='ISO-8859-1' standalone='yes'?>\n";
  print "<!DOCTYPE users []>\n\n";
  print "<users>\n";
  &xst_xml_enter ();

  &xst_xml_vspace ();
  &xst_xml_print ("<!-- login.defs configuration starts here -->\n");
  &xst_xml_vspace ();

  &xst_xml_print ("<logindefs>\n");
	&xst_xml_enter ();
	foreach $key (keys %login_defs)
	{
	  if ($key ne "") {
	    &xst_xml_print ("<$key>" . $login_defs{$key} . "</$key>\n");
	  }
	}
	&xst_xml_leave ();
	&xst_xml_print ("</logindefs>\n");
	&xst_xml_vspace ();
  &xst_xml_print ("<!-- Now the users -->\n");
  &xst_xml_vspace ();
	
  &xst_xml_print ("<!-- When was the passwd file last modified (since the epoch)? -->\n");
  &xst_xml_vspace ();
  &xst_xml_print ("<passwd_last_modified>$passwd_last_modified</passwd_last_modified>\n");
  &xst_xml_vspace ();
	
	&xst_xml_print ("<userdb>\n");
	&xst_xml_enter ();
	foreach $i (@users)
	{
          &xst_xml_vspace ();
	  &xst_xml_print ("<user>\n");
		&xst_xml_enter ();
		for ($j = 0; $j < ($#users_prop_array - 1) / 2; $j++) {
		  &xst_xml_print ("<" . $users_prop_map{"$j"} . ">" . xst_xml_quote ($$i[$j]) . "</" . $users_prop_map {$j} . ">\n");
		}
		&xst_xml_leave ();
		&xst_xml_print ("</user>\n");
	}
	&xst_xml_leave ();
  &xst_xml_vspace ();
  &xst_xml_print ("</userdb>\n");
	
  &xst_xml_print ("<!-- Now the groups -->\n");
  &xst_xml_vspace ();
	
  &xst_xml_print ("<!-- When was the group file last modified (since the epoch)? -->\n");
  &xst_xml_vspace ();
  &xst_xml_print ("<group_last_modified>$group_last_modified</group_last_modified>\n");
  &xst_xml_vspace ();
	
	&xst_xml_print ("<groupdb>\n");
	&xst_xml_enter ();
	foreach $i (@groups)
	{
    &xst_xml_vspace ();
	  &xst_xml_print ("<group>\n");
		&xst_xml_enter ();
		for ($j = 0; $j < ($#groups_prop_array - 1) / 2 - 1; $j++) {
			&xst_xml_print ("<" . $groups_prop_map{$j} . ">" . xst_xml_quote ($$i[$j]) . "</" . $groups_prop_map {$j} . ">\n");
		}
		&xst_xml_print ("<users>\n");
		&xst_xml_enter ();
		$k = $$i[$groups_prop_map{"users"}];
		foreach $j (@$k)
		{
			&xst_xml_print ("<user>$j</user>\n");
		}
		&xst_xml_leave ();
		&xst_xml_print ("</users>\n");

		&xst_xml_leave ();
		&xst_xml_print ("</group>\n");
	}
	&xst_xml_leave ();
  &xst_xml_vspace ();
  &xst_xml_print ("</groupdb>\n");

  # This for test purposes only
  &xml_print_nis () if $TEST_NIS;

  &xst_xml_vspace ();
	&xst_xml_leave ();
  print "</users>\n";
}


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

sub get
{
  &xst_report_info(1, "Getting user database");

  &read_login_defs (); &xst_print_progress ();
  &read_group (); &xst_print_progress ();
  &read_passwd_shadow (); &xst_print_progress ();

  &xst_end();
  &xml_print ();
}

sub set
{
  &xml_parse ();

  if ($xst_do_immediate)
  {
    &write_group_passwd ();
  }

  &xst_end();
}


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


sub filter
{
  &xml_parse ();
  &xst_end();
  &xml_print ();
}


# --- Main --- #

&xst_init ($name, $version, $description, @ARGV);
&xst_platform_ensure_supported (@platforms);

# Do our thing.

if    ($xst_operation eq "get")    { &get; }
elsif ($xst_operation eq "set")    { &set; }
elsif ($xst_operation eq "filter") { &filter; }
