# user-lib.pl
# Common functions for Unix user management

do '../web-lib.pl';
&init_config();
do "$gconfig{'os_type'}-lib.pl";

@random_password_chars = ( 'a' .. 'z', 'A' .. 'Z', '0' .. '9' );

# password_file(file)
# Returns true if some file looks like a valid Unix password file
sub password_file
{
if (!$_[0]) { return 0; }
elsif (open(SHTEST, $_[0])) {
	local($line);
	$line = <SHTEST>;
	close(SHTEST);
	return $line =~ /^\S+:\S*:/;
	}
else { return 0; }
}

# list_users()
# Returns an array of hashtable, each containing info about one user. Each hash
# will always contain the keys
#  user, pass, uid, gid, real, home, shell
# In addition, if the system supports shadow passwords it may also have:
#  change, min, max, warn, inactive, expire
# Or if it supports FreeBSD master.passwd info, it will also have
#  class, change, expire
sub list_users
{
return @list_users_cache if (defined(@list_users_cache));

# read the password file
local (@rv, $_, %idx, $lnum, @pw, $p, $i, $j);
local $pft = &passfiles_type();
if ($pft == 1) {
	# read the master.passwd file only
	$lnum = 0;
	open(PASSWD, $config{'master_file'});
	while(<PASSWD>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@pw = split(/:/, $_, -1);
			push(@rv, { 'user' => $pw[0],	'pass' => $pw[1],
				    'uid' => $pw[2],	'gid' => $pw[3],
				    'class' => $pw[4],	'change' => $pw[5],
				    'expire' => $pw[6],	'real' => $pw[7],
				    'home' => $pw[8],	'shell' => $pw[9],
				    'line' => $lnum,	'num' => scalar(@rv) });
			}
		$lnum++;
		}
	close(PASSWD);
	}
elsif ($pft == 6) {
	# Read netinfo dump
	open(PASSWD, "nidump passwd '$netinfo_domain' |");
	while(<PASSWD>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@pw = split(/:/, $_, -1);
			push(@rv, { 'user' => $pw[0],	'pass' => $pw[1],
				    'uid' => $pw[2],	'gid' => $pw[3],
				    'class' => $pw[4],	'change' => $pw[5],
				    'expire' => $pw[6],	'real' => $pw[7],
				    'home' => $pw[8],	'shell' => $pw[9],
				    'num' => scalar(@rv) });
			}
		}
	close(PASSWD);
	}
else {
	# start by reading /etc/passwd
	$lnum = 0;
	open(PASSWD, $config{'passwd_file'});
	while(<PASSWD>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@pw = split(/:/, $_, -1);
			push(@rv, { 'user' => $pw[0],	'pass' => $pw[1],
				    'uid' => $pw[2],	'gid' => $pw[3],
				    'real' => $pw[4],	'home' => $pw[5],
				    'shell' => $pw[6],	'line' => $lnum,
				    'num' => scalar(@rv) });
			$idx{$pw[0]} = $rv[$#rv];
			}
		$lnum++;
		}
	close(PASSWD);
	if ($pft == 2 || $pft == 5) {
		# read the shadow file data
		$lnum = 0;
		open(SHADOW, $config{'shadow_file'});
		while(<SHADOW>) {
			s/\r|\n//g;
			if (/\S/ && !/^[#\+\-]/) {
				@pw = split(/:/, $_, -1);
				$p = $idx{$pw[0]};
				$p->{'pass'} = $pw[1];
				$p->{'change'} = $pw[2] < 0 ? "" : $pw[2];
				$p->{'min'} = $pw[3] < 0 ? "" : $pw[3];
				$p->{'max'} = $pw[4] < 0 ? "" : $pw[4];
				$p->{'warn'} = $pw[5] < 0 ? "" : $pw[5];
				$p->{'inactive'} = $pw[6] < 0 ? "" : $pw[6];
				$p->{'expire'} = $pw[7] < 0 ? "" : $pw[7];
				$p->{'sline'} = $lnum;
				}
			$lnum++;
			}
		close(SHADOW);
		for($i=0; $i<@rv; $i++) {
			if (!defined($rv[$i]->{'sline'})) {
				# not in shadow!
				for($j=$i; $j<@rv; $j++) { $rv[$j]->{'num'}--; }
				splice(@rv, $i--, 1);
				}
			}
		}
	elsif ($pft == 4) {
		# read the AIX security passwd file
		local $lastuser;
		local $lnum = 0;
		open(SECURITY, $config{'shadow_file'});
		while(<SECURITY>) {
			s/\s*$//;
			if (/^\s*(\S+):/) {
				$lastuser = $idx{$1};
				$lastuser->{'sline'} = $lnum;
				}
			elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
				if ($1 eq 'password') {
					$lastuser->{'pass'} = $2;
					}
				elsif ($1 eq 'lastupdate') {
					$lastuser->{'change'} = $2;
					}
				elsif ($1 eq 'flags') {
					map { $lastuser->{lc($_)}++ }
					    split(/[,\s]+/, $2);
					}
				$lastuser->{'seline'} = $lnum;
				}
			$lnum++;
			}
		close(SECURITY);

		# read the AIX security user file
		open(USER, $config{'aix_user_file'});
		while(<USER>) {
			s/\s*$//;
			if (/^\s*(\S+):/) {
				$lastuser = $idx{$1};
				}
			elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
				if ($1 eq 'expires') {
					$lastuser->{'expire'} = $2;
					$lastuser->{'expire'} =~ s/^0$//;
					}
				elsif ($1 eq 'minage') {
					$lastuser->{'min'} = $2;
					$lastuser->{'min'} =~ s/^0$//;
					}
				elsif ($1 eq 'maxage') {
					$lastuser->{'max'} = $2;
					$lastuser->{'max'} =~ s/^0$//;
					}
				elsif ($1 eq 'pwdwarntime') {
					$lastuser->{'warn'} = $2;
					$lastuser->{'warn'} =~ s/^0$//; 
					}
				}
			}
		close(USER);
		}
	}
@list_users_cache = @rv;
return @rv;
}

# create_user(&details)
# Creates a new user with the given details
sub create_user
{
local(%u) = %{$_[0]};
local $lref;
local $pft = &passfiles_type();
if ($pft == 1) {
	# just need to add to master.passwd
	$lref = &read_file_lines($config{'master_file'});
	splice(@$lref, &nis_index($lref), 0,
	       "$u{'user'}:$u{'pass'}:$u{'uid'}:$u{'gid'}:$u{'class'}:".
	       "$u{'change'}:$u{'expire'}:$u{'real'}:$u{'home'}:$u{'shell'}");
	}
elsif ($pft == 3) {
	# Just invoke the useradd command
	&system_logged("useradd -u $u{'uid'} -g $u{'gid'} -c \"$u{'real'}\" -d $u{'home'} -s $u{'shell'} $u{'user'}");
	# And set the password
	&system_logged("echo $u{'pass'} | /usr/lib/scoadmin/account/password.tcl $u{'user'} >/dev/null 2>&1");
	}
elsif ($pft == 6) {
	# Use the niutil command
	&system_logged("niutil -create '$netinfo_domain' '/users/$u{'user'}'");
	&set_netinfo(\%u);
	}
else {
	# add to /etc/passwd
	$lref = &read_file_lines($config{'passwd_file'});
	splice(@$lref, &nis_index($lref), 0,
	       "$u{'user'}:".
	       ($pft == 2 || $pft == 5 ? "x" : $pft == 4 ? "!" : $u{'pass'}).
	       ":$u{'uid'}:$u{'gid'}:$u{'real'}:$u{'home'}:$u{'shell'}");
	if ($pft == 2) {
		# add to shadow as well..
		$lref = &read_file_lines($config{'shadow_file'});
		splice(@$lref, &nis_index($lref), 0,
		       "$u{'user'}:$u{'pass'}:$u{'change'}:$u{'min'}:".
		       "$u{'max'}:$u{'warn'}:$u{'inactive'}:$u{'expire'}:");
		}
	elsif ($pft == 5) {
		# add to SCO shadow file
		$lref = &read_file_lines($config{'shadow_file'});
		splice(@$lref, &nis_index($lref), 0,
	       	    "$u{'user'}:$u{'pass'}:$u{'change'}:$u{'min'}:$u{'max'}");
		}
	elsif ($pft == 4) {
		# add to AIX security passwd file as well..
		local @flags;
		push(@flags, 'ADMIN') if ($u{'admin'});
		push(@flags, 'ADMCHG') if ($u{'admchg'});
		push(@flags, 'NOCHECK') if ($u{'nocheck'});
		$lref = &read_file_lines($config{'shadow_file'});
		push(@$lref, "", "$u{'user'}:", "\tpassword = $u{'pass'}",
			     "\tlastupdate = $u{'change'}",
			     "\tflags = ".join(",", @flags));
		&flush_file_lines();
		
		# add to AIX security user file as well..
		$lref = &read_file_lines($config{'aix_user_file'});
		if ( $u{'expire'} || $u{'min'} || $u{'max'} || $u{'warn'} ) {
			push(@$lref, "$u{'user'}:");
			push(@$lref, "\texpires = $u{'expire'}") if ($u{'expire'});
			push(@$lref, "\tminage = $u{'min'}") if ($u{'min'});
			push(@$lref, "\tmaxage = $u{'max'}") if ($u{'max'});
			push(@$lref, "\tpwdwarntime = $u{'warn'}") if ($u{'warn'});
			push(@$lref, "");
			}
		}
	}
&flush_file_lines();
undef(@list_users_cache);
}

# modify_user(&old, &details)
sub modify_user
{
local(%u) = %{$_[1]};
local(@passwd, @shadow);
local $pft = &passfiles_type();
if ($pft == 1) {
	# just need to update master.passwd
	&replace_file_line($config{'master_file'}, $_[0]->{'line'},
	      "$u{'user'}:$u{'pass'}:$u{'uid'}:$u{'gid'}:$u{'class'}:".
	      "$u{'change'}:$u{'expire'}:$u{'real'}:$u{'home'}:$u{'shell'}\n");
	}
elsif ($pft == 3) {
	# Just use the usermod command
	&system_logged("usermod -u $u{'uid'} -g $u{'gid'} -c \"$u{'real'}\" -d $u{'home'} -s $u{'shell'} $u{'user'}");
	&system_logged("echo $u{'pass'} | /usr/lib/scoadmin/account/password.tcl $u{'user'}");
	}
elsif ($pft == 6) {
	# Just use the niutil command to update
	if ($_[0]->{'user'} && $_[0]->{'user'} ne $_[1]->{'user'}) {
		# Need to delete and re-create!
		&system_logged("niutil -destroy '$netinfo_domain' '/users/$_[0]->{'user'}'");
		&system_logged("niutil -create '$netinfo_domain' '/users/$_[1]->{'user'}'");
		}
	&set_netinfo($_[1]);
	}
else {
	# update /etc/passwd
	&replace_file_line($config{'passwd_file'}, $_[0]->{'line'},
		"$u{'user'}:".
		($pft == 2 || $pft == 5 ? "x" : $pft == 4 ? "!" : $u{'pass'}).
		":$u{'uid'}:$u{'gid'}:$u{'real'}:$u{'home'}:$u{'shell'}\n");
	if ($pft == 2) {
		# update shadow file as well..
		&replace_file_line($config{'shadow_file'}, $_[0]->{'sline'},
			"$u{'user'}:$u{'pass'}:$u{'change'}:$u{'min'}:".
			"$u{'max'}:$u{'warn'}:$u{'inactive'}:$u{'expire'}:\n");
		}
	elsif ($pft == 5) {
		# update SCO shadow
		&replace_file_line($config{'shadow_file'}, $_[0]->{'sline'},
		   "$u{'user'}:$u{'pass'}:$u{'change'}:$u{'min'}:$u{'max'}\n");
		}
	elsif ($pft == 4) {
		# update AIX shadow passwd file as well..
		local @flags;
		push(@flags, 'ADMIN') if ($u{'admin'});
		push(@flags, 'ADMCHG') if ($u{'admchg'});
		push(@flags, 'NOCHECK') if ($u{'nocheck'});
		local $lref = &read_file_lines($config{'shadow_file'});
		splice(@$lref, $_[0]->{'sline'},
		       $_[0]->{'seline'} - $_[0]->{'sline'} + 1,
		       "$u{'user'}:", "\tpassword = $u{'pass'}",
		       "\tlastupdate = $u{'change'}",
		       "\tflags = ".join(",", @flags));
		&flush_file_lines();

		# update AIX security user file as well..
		# use chuser command because it's easier than working with the complexity issues of the file.
		$u{'expire'} = 0 if (! $u{'expire'}); 
		$u{'min'} = 0 if (! $u{'min'}); 
		$u{'max'} = 0 if (! $u{'max'}); 
		$u{'warn'} = 0 if (! $u{'warn'}); 
		&system_logged("chuser expires=$u{'expire'} minage=$u{'min'} maxage=$u{'max'} pwdwarntime=$u{'warn'} $u{'user'}");
		}
	}
undef(@list_users_cache);
}

# delete_user(&details)
sub delete_user
{
local $pft = &passfiles_type();
if ($pft == 1) {
	&replace_file_line($config{'master_file'}, $_[0]->{'line'});
	}
elsif ($pft == 3) {
	# Just invoke the userdel command
	&system_logged("userdel -n0 $_[0]->{'user'}");
	}
elsif ($pft == 4) {
	# Just invoke the rmuser command
	&system_logged("rmuser -p $_[0]->{'user'}");
	}
elsif ($pft == 6) {
	# Just delete with the niutil command
	&system_logged("niutil -destroy '$netinfo_domain' '/users/$_[0]->{'user'}'");
	}
else {
	&replace_file_line($config{'passwd_file'}, $_[0]->{'line'});
	if ($pft == 2 || $pft == 5) {
		&replace_file_line($config{'shadow_file'}, $_[0]->{'sline'});
		}
	}
undef(@list_users_cache);
}

# list_groups()
# Returns a list of all the local groups as an array of hashtables. Each
# will contain group, pass, gid, members
sub list_groups
{
return @list_groups_cache if (defined(@list_groups_cache));

local(@rv, $lnum, $_, %idx, $g, $i, $j, @gr);
$lnum = 0;
local $gft = &groupfiles_type();
if ($gft == 5) {
	# Get groups from netinfo
	open(GROUP, "nidump group '$netinfo_domain' |");
	while(<GROUP>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@gr = split(/:/, $_, -1);
			push(@rv, { 'group' => $gr[0],	'pass' => $gr[1],
				    'gid' => $gr[2],
				    'members' => join(",", split(/\s+/,$gr[3])),
				    'num' => scalar(@rv) });
			}
		}
	close(GROUP);
	}
else {
	# Read the standard group file
	open(GROUP, $config{'group_file'});
	while(<GROUP>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@gr = split(/:/, $_, -1);
			push(@rv, { 'group' => $gr[0],	'pass' => $gr[1],
				    'gid' => $gr[2],	'members' => $gr[3],
				    'line' => $lnum,	'num' => scalar(@rv) });
			$idx{$gr[0]} = $rv[$#rv];
			}
		$lnum++;
		}
	close(GROUP);
	}
if ($gft == 2) {
	# read the gshadow file data
	$lnum = 0;
	open(SHADOW, $config{'gshadow_file'});
	while(<SHADOW>) {
		s/\r|\n//g;
		if (/\S/ && !/^[#\+\-]/) {
			@gr = split(/:/, $_, -1);
			$g = $idx{$gr[0]};
			$g->{'pass'} = $gr[1];
			$g->{'sline'} = $lnum;
			}
		$lnum++;
		}
	close(SHADOW);
	#for($i=0; $i<@rv; $i++) {
	#	if (!defined($rv[$i]->{'sline'})) {
	#		# not in shadow!
	#		for($j=$i; $j<@rv; $j++) { $rv[$j]->{'num'}--; }
	#		splice(@rv, $i--, 1);
	#		}
	#	}
	}
elsif ($gft == 4) {
	# read the AIX group data
	local $lastgroup;
	local $lnum = 0;
	open(SECURITY, $config{'gshadow_file'});
	while(<SECURITY>) {
		s/\s*$//;
		if (/^\s*(\S+):/) {
			$lastgroup = $idx{$1};
			$lastgroup->{'sline'} = $lnum;
			$lastgroup->{'seline'} = $lnum;
			}
		elsif (/^\s*([^=\s]+)\s*=\s*(.*)/) {
			$lastgroup->{'seline'} = $lnum;
			}
		$lnum++;
		}
	close(SECURITY);
	}
@list_groups_cache = @rv;
return @rv;
}

# create_group(&details)
sub create_group
{
local(%g) = %{$_[0]};
local $gft = &groupfiles_type();
if ($gft == 5) {
	# Use niutil command
	&system_logged("niutil -create '$netinfo_domain' '/groups/$g{'group'}'");
	&set_group_netinfo(\%g);
	}
else {
	# Update group file(s)
	local $lref;
	$lref = &read_file_lines($config{'group_file'});
	splice(@$lref, &nis_index($lref), 0,
	       "$g{'group'}:".(&groupfiles_type() == 2 ? "x" : $g{'pass'}).
	       ":$g{'gid'}:$g{'members'}");
	close(GROUP);
	if ($gft == 2) {
		$lref = &read_file_lines($config{'gshadow_file'});
		splice(@$lref, &nis_index($lref), 0,
		       "$g{'group'}:$g{'pass'}::$g{'members'}");
		}
	elsif ($gft == 4) {
		$lref = &read_file_lines($config{'gshadow_file'});
		push(@$lref, "", "$g{'group'}:", "\tadmin = false");
		}
	&flush_file_lines();
	}
undef(@list_groups_cache);
}

# modify_group(&old, &details)
sub modify_group
{
local(%g) = %{$_[1]};
local $gft = &groupfiles_type();
if ($gft == 5) {
	# Call niutil to update the group
	if ($_[0]->{'group'} && $_[0]->{'group'} ne $_[1]->{'group'}) {
		# Need to delete and re-create!
		&system_logged("niutil -destroy '$netinfo_domain' '/groups/$_[0]->{'group'}'");
		&system_logged("niutil -create '$netinfo_domain' '/groups/$_[1]->{'group'}'");
		}
	&set_group_netinfo($_[1]);
	}
else {
	# Update in files
	local $gs = (&groupfiles_type() == 2 && $_[0]->{'sline'} ne '');
	&replace_file_line($config{'group_file'}, $_[0]->{'line'},
		   "$g{'group'}:".($gs ? "x" : $g{'pass'}).
		   ":$g{'gid'}:$g{'members'}\n");
	if ($gs) {
		&replace_file_line($config{'gshadow_file'}, $_[0]->{'sline'},
				   "$g{'group'}:$g{'pass'}::$g{'members'}\n");
		}
	elsif (&groupfiles_type() == 4) {
		$lref = &read_file_lines($config{'gshadow_file'});
		splice(@$lref, $_[0]->{'sline'}, 1, "$g{'group'}:");
		&flush_file_lines();
		}
	}
undef(@list_groups_cache);
}

# delete_group(&details)
sub delete_group
{
local $gft = &groupfiles_type();
if ($gft == 5) {
	# Call niutil to delete
	&system_logged("niutil -destroy '$netinfo_domain' '/groups/$_[0]->{'group'}'");
	}
else {
	# Remove from group file(s)
	&replace_file_line($config{'group_file'}, $_[0]->{'line'});
	if ($gft == 2 && $_[0]->{'sline'} ne '') {
		&replace_file_line($config{'gshadow_file'}, $_[0]->{'sline'});
		}
	elsif ($gft == 4) {
		local $lref = &read_file_lines($config{'gshadow_file'});
		splice(@$lref, $_[0]->{'sline'},
		       $_[0]->{'seline'} - $_[0]->{'sline'} + 1);
		&flush_file_lines();
		}
	}
undef(@list_groups_cache);
}


############################################################################
# Misc functions
############################################################################
# recursive_change(dir, olduid, oldgid, newuid, newgid)
# Change the UID or GID of a directory and all files in it, if they match the
# given UID/GID
sub recursive_change
{
local(@list, $f, @stbuf);
(@stbuf = stat($_[0])) || return;
(-l $_[0]) && return;
if (($_[1] < 0 || $_[1] == $stbuf[4]) &&
    ($_[2] < 0 || $_[2] == $stbuf[5])) {
	# Found match..
	chown($_[3] < 0 ? $stbuf[4] : $_[3],
	      $_[4] < 0 ? $stbuf[5] : $_[4], $_[0]);
	}
if (-d $_[0]) {
	opendir(DIR, $_[0]);
	@list = readdir(DIR);
	closedir(DIR);
	foreach $f (@list) {
		if ($f eq "." || $f eq "..") { next; }
		&recursive_change("$_[0]/$f", $_[1], $_[2], $_[3], $_[4]);
		}
	}
}

# making_changes()
# Called before changes are made to the password or group file
sub making_changes
{
if ($config{'pre_command'} =~ /\S/) {
	local $out = &backquote_logged("($config{'pre_command'}) 2>&1 </dev/null");
	return $? ? $out : undef;
	}
return undef;
}

# made_changes()
# Called after the password or group file has been changed, to run the
# post-changes command.
sub made_changes
{
if ($config{'post_command'} =~ /\S/) {
	local $out = &backquote_logged("($config{'post_command'}) 2>&1 </dev/null");
	return $? ? $out : undef;
	}
return undef;
}

# other_modules(function, arg, ...)
# Call some function in the useradmin_update.pl file in other modules
sub other_modules
{
local($m, %minfo);
local $func = shift(@_);
foreach $m (&get_all_module_infos()) {
	if (&check_os_support($m) &&
	    -r "$root_directory/$m->{'dir'}/useradmin_update.pl") {
		&foreign_require($m->{'dir'}, "useradmin_update.pl");
		&foreign_call($m->{'dir'}, $func, @_);
		}
	}
}

# can_edit_user(&acl, &user)
sub can_edit_user
{
local $m = $_[0]->{'uedit_mode'};
local %u;
if ($m == 0) { return 1; }
elsif ($m == 1) { return 0; }
elsif ($m == 2 || $m == 3 || $m == 5) {
	map { $u{$_}++ } split(/\s+/, $_[0]->{'uedit'});
	if ($m == 5 && $_[0]->{'uedit_sec'}) {
		# Check secondary groups too
		return 1 if ($u{$_[1]->{'gid'}});
		foreach $g (&list_groups()) {
			local @m = split(/,/, $g->{'members'});
			return 1 if ($u{$g->{'gid'}} &&
				     &indexof($_[1]->{'user'}, @m) >= 0);
			}
		return 0;
		}
	else {
		return $m == 2 ? $u{$_[1]->{'user'}} :
		       $m == 3 ? !$u{$_[1]->{'user'}} :
				 $u{$_[1]->{'gid'}};
		}
	}
elsif ($m == 4) {
	return (!$_[0]->{'uedit'} || $_[1]->{'uid'} >= $_[0]->{'uedit'}) &&
	       (!$_[0]->{'uedit2'} || $_[1]->{'uid'} <= $_[0]->{'uedit2'});
	}
elsif ($m == 6) {
	return $_[1]->{'user'} eq $remote_user;
	}
return 0;
}

# can_edit_group(&acl, &group)
sub can_edit_group
{
local $m = $_[0]->{'gedit_mode'};
local %g;
if ($m == 0) { return 1; }
elsif ($m == 1) { return 0; }
elsif ($m == 2 || $m == 3) {
	map { $g{$_}++ } split(/\s+/, $_[0]->{'gedit'});
	return $m == 2 ? $g{$_[1]->{'group'}}
		       : !$g{$_[1]->{'group'}};
	}
else { return (!$_[0]->{'gedit'} || $_[1]->{'gid'} >= $_[0]->{'gedit'}) &&
	      (!$_[0]->{'gedit2'} || $_[1]->{'gid'} <= $_[0]->{'gedit2'}); }
}

# nis_index(&lines)
sub nis_index
{
local $i;
for($i=0; $i<@{$_[0]}; $i++) {
	last if ($_[0]->[$i] =~ /^[\+\-]/);
	}
return $i;
}

# copy_skel_files(source, dest, uid, gid)
sub copy_skel_files
{
local ($f, $df);
foreach $f (split(/\s+/, $_[0])) {
	if (-d $f) {
		# copy all files in a directory
		opendir(DIR, $f);
		foreach $df (readdir(DIR)) {
			if ($df eq "." || $df eq "..") { next; }
			&copy_file("$f/$df", $_[1], $_[2], $_[3]);
			}
		closedir(DIR);
		}
	elsif (-r $f) {
		# copy just one file
		&copy_file($f, $_[1], $_[2], $_[3]);
		}
	}
}

# copy_file(file, destdir, uid, gid)
# Copy a file or directory and chown it
sub copy_file
{
local($base, $subs);
$_[0] =~ /\/([^\/]+)$/; $base = $1;
if ($config{"files_remap_$base"}) {
	$base = $config{"files_remap_$base"};
	}
$subs = $config{'files_remove'};
$base =~ s/$subs//g if ($subs);
local ($opts, $nochown);
if (-b $_[0] || -c $_[0]) {
	# Looks like a device file .. re-create it
	local @st = stat($_[0]);
	local $maj = int($st[6] / 256);
	local $min = $st[6] % 256;
	local $typ = ($st[2] & 00170000) == 0020000 ? 'c' : 'b';
	&system_logged("mknod \"$_[1]/$base\" $typ $maj $min");
	chown($_[2], $_[3], "$_[1]/$base");
	$nochown++;
	}
elsif (-l $_[0] && !$config{'copy_symlinks'}) {
	# A symlink .. re-create it
	local $l = readlink($_[0]);
	&system_logged("ln -s \"$l\" \"$_[1]/$base\" >/dev/null 2>/dev/null");
	$opts = "-h";
	}
elsif (-d $_[0]) {
	# A directory .. copy it recursively
	&system_logged("cp -R \"$_[0]\" \"$_[1]/$base\" >/dev/null 2>/dev/null");
	}
else {
	# Just a normal file .. copy it
	&system_logged("cp \"$_[0]\" \"$_[1]/$base\" >/dev/null 2>/dev/null");
	chown($_[2], $_[3], "$_[1]/$base");
	$nochown++;
	}
&system_logged("chown $opts -R $_[2]:$_[3] \"$_[1]/$base\" >/dev/null 2>/dev/null") if (!$nochown);
}

# lock_user_files()
# Lock all password, shadow and group files
sub lock_user_files
{
&lock_file($config{'passwd_file'});
&lock_file($config{'group_file'});
&lock_file($config{'shadow_file'});
&lock_file($config{'gshadow_file'});
&lock_file($config{'master_file'});
}

# unlock_user_files()
# Unlock all password, shadow and group files
sub unlock_user_files
{
&unlock_file($config{'passwd_file'});
&unlock_file($config{'group_file'});
&unlock_file($config{'shadow_file'});
&unlock_file($config{'gshadow_file'});
&unlock_file($config{'master_file'});
}

# Functions similar to the standard password file ones, but which may
# use webmin's reading of the user/group files instead.

sub my_setpwent
{
if ($config{'from_files'}) {
	@setpwent_cache = &list_users();
	$setpwent_pos = 0;
	}
else { return setpwent(); }
}

sub my_getpwent
{
if ($config{'from_files'}) {
	my_setpwent() if (!@setpwent_cache);
	if ($setpwent_pos >= @setpwent_cache) {
		return wantarray ? () : undef;
		}
	else {
		return &pw_user_rv($setpwent_cache[$setpwent_pos++],
				   wantarray, 'user');
		}
	}
else { return getpwent(); }
}

sub my_endpwent
{
if ($config{'from_files'}) {
	undef(@setpwent_cache);
	}
elsif ($gconfig{'os_type'} eq 'hpux') {
	# On hpux, endpwent() can crash perl!
	return 0;
	}
else { return endpwent(); }
}

sub my_getpwnam
{
if ($config{'from_files'}) {
	foreach $u (&list_users()) {
		return &pw_user_rv($u, wantarray, 'uid')
			if ($u->{'user'} eq $_[0]);
		}
	return wantarray ? () : undef;
	}
else { return getpwnam($_[0]); }
}

sub my_getpwuid
{
if ($config{'from_files'}) {
	foreach $u (&list_users()) {
		return &pw_user_rv($u, wantarray, 'user')
			if ($u->{'uid'} eq $_[0]);
		}
	return wantarray ? () : undef;
	}
else { return getpwuid($_[0]); }
}

sub pw_user_rv
{
return $_[0] ? ( $_[0]->{'user'}, $_[0]->{'pass'}, $_[0]->{'uid'},
		 $_[0]->{'gid'}, undef, undef, $_[0]->{'real'},
		 $_[0]->{'home'}, $_[0]->{'shell'}, undef ) : $_[0]->{$_[2]};
}

sub my_setgrent
{
if ($config{'from_files'}) {
	@setgrent_cache = &list_groups();
	$setgrent_pos = 0;
	}
else { return setgrent(); }
}

sub my_getgrent
{
if ($config{'from_files'}) {
	my_setgrent() if (!@setgrent_cache);
	if ($setgrent_pos >= @setgrent_cache) {
		return ();
		}
	else {
		return &gr_group_rv($setgrent_cache[$setgrent_pos++],
				    wantarray, 'group');
		}
	}
else { return getgrent(); }
}

sub my_endgrent
{
if ($config{'from_files'}) {
	undef(@setgrent_cache);
	}
elsif ($gconfig{'os_type'} eq 'hpux') {
	# On hpux, endpwent() can crash perl!
	return 0;
	}
else { return endgrent(); }
}

sub my_getgrnam
{
if ($config{'from_files'}) {
	foreach $g (&list_groups()) {
		return &gr_group_rv($g, wantarray, 'gid')
			if ($g->{'group'} eq $_[0]);
		}
	return wantarray ? () : undef;
	}
else { return getgrnam($_[0]); }
}

sub my_getgrgid
{
if ($config{'from_files'}) {
	foreach $g (&list_groups()) {
		return &gr_group_rv($g, wantarray, 'group')
			if ($g->{'gid'} eq $_[0]);
		}
	return wantarray ? () : undef;
	}
else { return getgrgid($_[0]); }
}

sub gr_group_rv
{
return $_[1] ? ( $_[0]->{'group'}, $_[0]->{'pass'}, $_[0]->{'gid'},
		 $_[0]->{'members'} ) : $_[0]->{$_[2]};
}

# auto_home_dir(base, username)
# Returns an automatically generated home directory, and creates needed
# parent dirs
sub auto_home_dir
{
if ($config{'home_style'} == 0) {
	return $_[0]."/".$_[1];
	}
elsif ($config{'home_style'} == 1) {
	&mkdir_if_needed($_[0]."/".substr($_[1], 0, 1));
	return $_[0]."/".substr($_[1], 0, 1)."/".$_[1];
	}
elsif ($config{'home_style'} == 2) {
	&mkdir_if_needed($_[0]."/".substr($_[1], 0, 1));
	&mkdir_if_needed($_[0]."/".substr($_[1], 0, 1)."/".
			 substr($_[1], 0, 2));
	return $_[0]."/".substr($_[1], 0, 1)."/".
	       substr($_[1], 0, 2)."/".$_[1];
	}
elsif ($config{'home_style'} == 3) {
	&mkdir_if_needed($_[0]."/".substr($_[1], 0, 1));
	&mkdir_if_needed($_[0]."/".substr($_[1], 0, 1)."/".
			 substr($_[1], 1, 1));
	return $_[0]."/".substr($_[1], 0, 1)."/".
	       substr($_[1], 1, 1)."/".$_[1];
	}
}

sub mkdir_if_needed
{
-d $_[0] || mkdir($_[0], 0755);
}

sub set_netinfo
{
local %u = %{$_[0]};
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' passwd '$u{'pass'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' uid '$u{'uid'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' gid '$u{'gid'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' class '$u{'class'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' change '$u{'change'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' expire '$u{'expire'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' realname '$u{'real'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' home '$u{'home'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/users/$u{'user'}' shell '$u{'shell'}'");
}

sub set_group_netinfo
{
local %g = %{$_[0]};
local $mems = join(" ", map { "'$_'" } split(/,/, $g{'members'}));
&system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' gid '$g{'gid'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' passwd '$g{'pass'}'");
&system_logged("niutil -createprop '$netinfo_domain' '/groups/$g{'group'}' users $mems");
}

# check_password_restrictions(pass, username)
# Returns an error message if the given password fails length and other
# checks, or undef if it is OK
sub check_password_restrictions
{
return &text('usave_epasswd_min', $config{'passwd_min'})
	if (length($_[0]) < $config{'passwd_min'});
local $re = $config{'passwd_re'};
return &text('usave_epasswd_re', $re)
	if ($re && !eval { $_[0] =~ /$re/ });
if ($config{'passwd_same'}) {
	return &text('usave_epasswd_same') if ($_[0] =~ /\Q$_[1]\E/i);
	}
if ($config{'passwd_dict'} && $_[0] =~ /^[A-Za-z\'\-]+$/ &&
    (&has_command("ispell") || &has_command("spell"))) {
	local $temp = &tempname();
	open(TEMP, ">$temp");
	print TEMP $_[0],"\n";
	close(TEMP);
	if (&has_command("ispell")) {
		open(SPELL, "ispell -a <$temp |");
		while(<SPELL>) {
			if (/^(#|\&|\?)/) {
				$unknown++;
				}
			}
		close(SPELL);
		}
	else {
		open(SPELL, "spell <$temp |");
		local $line = <SPELL>;
		$unknown++ if ($line);
		close(SPELL);
		}
	unlink($temp);
	return &text('usave_epasswd_dict') if (!$unknown);
	}
return undef;
}

# can_use_group(&acl, group)
# Returns 1 if some group can be used as a primary secondary, 0 if not
sub can_use_group
{
return 1 if ($_[0]->{'ugroups'} eq '*');
local @sp = split(/\s+/, $_[0]->{'ugroups'});
if ($_[0]->{'uedit_gmode'} == 3) {
	return &indexof($_[1], @sp) < 0;
	}
elsif ($_[0]->{'uedit_gmode'} == 4) {
	local @ginfo = &my_getgrnam($_[1]);
	return (!$_[0]->{'ugroups'} || $ginfo[2] >= $_[0]->{'ugroups'}) &&
	       (!$_[0]->{'ugroups2'} || $ginfo[2] <= $_[0]->{'ugroups2'});
	}
else {
	return &indexof($_[1], @sp) >= 0;
	}
}

1;
