5df118b3e3bef19af93967cc6a6816bdef8daa04
[webmin.git] / ldap-useradmin / save_user.cgi
1 #!/usr/local/bin/perl
2 # save_user.cgi
3 # Create, update or delete an LDAP user
4
5 require './ldap-useradmin-lib.pl';
6 use Time::Local;
7 &ReadParse();
8 $ldap = &ldap_connect();
9 $schema = $ldap->schema();
10 &lock_user_files();
11 if (!$in{'new'}) {
12         # Get existing user
13         $rv = $ldap->search(base => $in{'dn'},
14                             scope => 'base',
15                             filter => &user_filter());
16         ($uinfo) = $rv->all_entries;
17         $uinfo || &error($text{'usave_egone'});
18         %ouser = &dn_to_hash($uinfo);
19         &can_edit_user(\%ouser) || &error($text{'usave_eedit'});
20         }
21 else {
22         $access{'ucreate'} || &error($text{'usave_ecreate'});
23         }
24
25 if ($in{'mailboxes'}) {
26         # Just re-direct to mailboxes page
27         &redirect("../mailboxes/list_mail.cgi?user=$ouser{'user'}");
28         exit;
29         }
30 elsif ($in{'switch'}) {
31         # Just re-direct to Usermin switch user program
32         &redirect("../usermin/switch.cgi?user=$ouser{'user'}");
33         exit;
34         }
35 elsif ($in{'delete'}) {
36         # Delete the user .. but ask first!
37         &ui_print_header(undef, $text{'udel_title'}, "");
38         $home = $uinfo->get_value("homeDirectory");
39         $user = $uinfo->get_value("uid");
40         if ($in{'confirm'}) {
41                 # Run the before command
42                 %uhash = &dn_to_hash($uinfo);
43                 &set_user_envs(\%uhash, 'DELETE_USER', undef, undef);
44                 $merr = &making_changes();
45                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
46                         if (defined($merr));
47
48                 # Work out old classes
49                 @classes = $uinfo->get_value("objectClass");
50                 @cyrus_class_2 = split(' ',$cyrus_class);
51                 $wascyrus = &indexof($cyrus_class_2[0], @classes) >= 0;
52
53                 # Delete from other modules
54                 %user = &dn_to_hash($uinfo);
55                 if ($in{'others'}) {
56                         print "$text{'udel_other'}<br>\n";
57                         &useradmin::other_modules("useradmin_delete_user",
58                                                   \%user);
59                         print "$text{'udel_done'}<p>\n";
60                         }
61
62                 # Delete from any groups
63                 print "$text{'udel_groups'}<br>\n";
64                 $base = &get_group_base();
65                 $rv = $ldap->search(base => $base,
66                                     filter => &group_filter());
67                 foreach $g ($rv->all_entries) {
68                         local @mems = $g->get_value("memberUid");
69                         local $idx = &indexof($user, @mems);
70                         if ($idx >= 0) {
71                                 # Take out of this group
72                                 splice(@mems, $idx, 1);
73                                 $rv = $ldap->modify($g->dn(), replace =>
74                                         { 'memberUid' => \@mems });
75                                 if ($rv->code) {
76                                         &error(&text('usave_emodgroup',
77                                                      $g->get_value('cn'),
78                                                      $rv->error));
79                                         }
80                                 }
81                         }
82                 print "$text{'udel_done'}<p>\n";
83
84                 # Delete from the LDAP db
85                 print "$text{'udel_pass'}<br>\n";
86                 $rv = $ldap->delete($in{'dn'});
87                 if ($rv->code) {
88                         &error(&text('usave_edelete', $rv->error));
89                         }
90                 print "$text{'udel_done'}<p>\n";
91
92                 # Delete his addressbook entry
93                 if ($config{'addressbook'} && $wascyrus) {
94                         print "$text{'udel_book'}<br>\n";
95                         $err = &delete_addressbook();
96                         if ($err) {
97                                 print &text('udel_failed', $err),"<p>\n";
98                                 }
99                         else {
100                                 print "$text{'udel_done'}<p>\n";
101                                 }
102                         }
103
104                 # Delete his home directory
105                 if ($in{'delhome'}) {
106                         print "$text{'udel_home'}<br>\n";
107                         $home = $uinfo->get_value("homeDirectory");
108                         if (-d $home && $home ne "/") {
109                                 local $realhome = &resolve_links($home);
110                                 local $qhome = quotemeta($realhome);
111                                 system("rm -rf $qhome >/dev/null 2>&1");
112                                 unlink($home);  # in case of links
113                                 }
114                         print "$text{'udel_done'}<p>\n";
115
116                   # Delete his IMAP mailbox only if home gets deleted, too
117                   if ($config{'imap_host'}) {
118                         print "$text{'udel_imap'}<br>\n";
119                         $imap = &imap_connect();
120                         $rv = $imap->delete("user".$config{'imap_foldersep'}.
121                                             $uinfo->get_value("uid"));
122                         $imap->logout();
123                         print "$text{'udel_done'}<p>\n";
124                         }
125                 }
126
127                 &made_changes();
128
129                 %p = ( %in, %user );
130                 &webmin_log("delete", "user", $user{'user'}, \%p);
131                 }
132         else {
133                 # Show confirmation page
134                 if ($home ne "/" && -d $home) {
135                         # With option to delete home
136                         $size = &nice_size(&disk_usage_kb($home)*1024);
137                         $msg = &text('udel_sure', $user, $home, $size);
138                         @buts = ( [ undef, $text{'udel_del1'} ],
139                                   [ "delhome", $text{'udel_del2'} ] );
140                         }
141                 else {
142                         # Without home
143                         $msg = &text('udel_sure2', $user);
144                         @buts = ( [ undef, $text{'udel_del1'} ] );
145                         }
146                 print &ui_confirmation_form(
147                         "save_user.cgi",
148                         $msg,
149                         [ [ "dn", $in{'dn'} ],
150                           [ "confirm", 1 ],
151                           [ "delete", 1 ] ],
152                         \@buts,
153                         &ui_checkbox("others", 1, $text{'udel_dothers'},
154                                      $mconfig{'default_other'}),
155                         $user eq 'root' ?
156                           "<font color=#ff0000>$text{'udel_root'}</font>" : ""
157                         );
158                 }
159
160         $ldap->unbind();
161         &ui_print_footer("", $text{'index_return'});
162         exit;
163         }
164 elsif ($in{'raw'}) {
165         # Show all LDAP attributes for user
166         &redirect("raw.cgi?user=1&dn=".&urlize($in{'dn'}));
167         exit;
168         }
169 else {
170         # Validate inputs
171         &error_setup($text{'usave_err'});
172         $in{'user'} =~ /^[^:\t]+$/ ||
173                 &error(&text('usave_ebadname', $in{'user'}));
174         $in{'user'} =~ s/\r//g;
175         $in{'real'} || &error($text{'usave_ereal'});
176         @users = split(/\n/, $in{'user'});
177         $user = $users[0];
178         $in{'uid'} =~ /^\-?[0-9]+$/ || &error(&text('usave_euid', $in{'uid'}));
179         $uid = $in{'uid'};
180         $in{'real'} =~ /^[^:]*$/ || &error(&text('usave_ereal', $in{'real'}));
181         $firstname = $in{'firstname'};
182         $lastname = $in{'lastname'};
183         $real = $in{'real'};
184         $shell = $in{'shell'} eq '*' ? $in{'othersh'} : $in{'shell'};
185         if ($in{'new'}) {
186                 &check_user_used($ldap, $user) &&
187                         &error(&text('usave_einuse', $user));
188                 }
189
190         # Check for UID clash
191         if ($in{'new'} && !$access{'umultiple'}) {
192                 &check_uid_used($ldap, $uid) &&
193                         &error($text{'usave_euidused2'});
194                 }
195
196         # Validate IMAP quota
197         $quota = undef;
198         if ($config{'quota_support'} && !$in{'quota_def'} &&
199             defined($in{'quota'})) {
200                 $in{'quota'} =~ /^\d+$/ || &error($text{'usave_equota'});
201                 $quota = $in{'quota'};
202                 }
203
204         # Compute and validate home directory
205         if ($access{'autohome'}) {
206                 if ($in{'new'} || $ouser{'user'} ne $user) {
207                         $home = &auto_home_dir($access{'home'}, $user, $grp);
208                         }
209                 else {
210                         $home = $ouser{'home'};
211                         }
212                 }
213         elsif ($mconfig{'home_base'} && $in{'home_base'}) {
214                 $home = &auto_home_dir($mconfig{'home_base'}, $user);
215                 }
216         else {
217                 $home = $in{'home'};
218                 $home =~ /^\// || &error(&text('usave_ehome', $home));
219                 }
220         if (!$access{'autohome'}) {
221                 $home =~ /^\// || &error(&text('usave_ehome', $home));
222                 $al = length($access{'home'});
223                 if (length($home) < $al ||
224                     substr($home, 0, $al) ne $access{'home'}) {
225                         &error(&text('usave_ehomepath', $home));
226                         }
227                 }
228
229         local $pfx = $config{'md5'} == 1 || $config{'md5'} == 3 ? "{md5}" :
230                      $config{'md5'} == 4 ? "{ssha}" :
231                      $config{'md5'} == 0 ? "{crypt}" : "";
232         if ($in{'passmode'} == 0) {
233                 # Password is blank
234                 if (!$mconfig{'empty_mode'}) {
235                         local $err = &useradmin::check_password_restrictions(
236                                         "", $user);
237                         &error($err) if ($err);
238                         }
239                 $pass = "";
240                 }
241         elsif ($in{'passmode'} == 1) {
242                 # Password is locked
243                 $pass = $mconfig{'lock_string'};
244                 }
245         elsif ($in{'passmode'} == 2) {
246                 # Specific encrypted password entered, or possibly no change
247                 $pass = $in{'encpass'};
248                 $pass = $pfx.$pass if ($pass !~ /^\{[a-z0-9]+\}/i && $pfx);
249                 }
250         elsif ($in{'passmode'} == 3) {
251                 # Normal password entered - check restrictions
252                 local $err = &useradmin::check_password_restrictions(
253                                 $in{'pass'}, $user);
254                 &error($err) if ($err);
255                 $pass = $pfx.&encrypt_password($in{'pass'});
256                 $plainpass = $in{'pass'};
257                 }
258         if ($in{'disable'} && ($in{'passmode'} == 2 || $in{'passmode'} == 3)) {
259                 $pass = $useradmin::disable_string.$pass;
260                 }
261         if ($in{'gid'} =~ /^\d+$/) {
262                 $gid = $in{'gid'};
263                 }
264         else {
265                 $gid = &all_getgrnam($in{'gid'});
266                 defined($gid) || &error(&text('usave_egid', $in{'gid'}));
267                 }
268         $grp = &all_getgrgid($gid);
269
270         # Build useradmin-style hash of user details
271         local %uhash = ( 'user' => $user,
272                          'uid' => $uid,
273                          'gid' => $gid,
274                          'group' => $in{'group'},
275                          'real' => $real,
276                          'shell' => $shell,
277                          'pass' => $pass,
278                          'plainpass' => $plainpass,
279                          'home' => $home,
280                          'firstname' => $firstname,
281                          'lastname' => $lastname );
282
283         if ($in{'new'}) {
284                 defined(&all_getpwnam($user)) &&
285                         &error(&text('usave_einuse', $user));
286                 if ($in{'passmode'} == 1 || $in{'passmode'} == 2) {
287                         if ($in{'cyrus'}) {
288                                 &error($text{'usave_ecyruspass'});
289                                 }
290                         }
291
292                 # Run the pre-change command
293                 &set_user_envs(\%uhash, 'CREATE_USER',
294                                $in{'passmode'} == 3 ? $in{'pass'} : "",
295                                undef);
296                 $merr = &making_changes();
297                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
298                         if (defined($merr));
299
300                 # Create home dir
301                 if (!-e $home && $in{'makehome'}) {
302                         &lock_file($home);
303                         mkdir($home, oct($mconfig{'homedir_perms'})) ||
304                                 &error(&text('usave_emkdir', $!));
305                         chmod(oct($mconfig{'homedir_perms'}), $home) ||
306                                 &error(&text('usave_echmod', $!));
307                         chown($uid, $gid, $home) ||
308                                 &error(&text('usave_echown', $!));
309                         &unlock_file($home);
310                         }
311
312                 # Get configured properties for new users
313                 local @props = &split_props($config{'props'}, \%uhash);
314                 if ($in{'cyrus'}) {
315                         push(@props, &split_props($config{'imap_props'},
316                                                   \%uhash));
317                         }
318
319                 # Build Samba-related properties
320                 if ($in{'samba'}) {
321                         &samba_properties(1, \%uhash, $in{'passmode'},
322                                           $in{'pass'}, $schema, \@props, $ldap);
323                         }
324
325                 if ($in{'cyrus'}) {
326                         # Build mail-related properties
327                         &mail_props();
328                         }
329
330                 # Add any extra LDAP fields
331                 &parse_extra_fields($config{'fields'}, \@props, \@rprops,
332                                     $ldap);
333
334                 # Add shadow LDAP fields
335                 $shadow = &shadow_fields();
336
337                 # Add to the ldap database
338                 @classes = ( "posixAccount", "shadowAccount" );
339                 if ($schema && $schema->objectclass("person") && $config{'person'}) {
340                         push(@classes, "person");
341                         }
342
343                 push(@classes, split(/\s+/, $config{'other_class'}));
344                 push(@classes, $samba_class) if ($in{'samba'});
345                 push(@classes, split(' ',$cyrus_class)) if ($in{'cyrus'});
346                 @classes = grep { /\S/ } @classes;      # Remove empty
347                 &name_fields();
348                 @classes = &unique(@classes);
349                 $base = &get_user_base();
350                 $newdn = "uid=$user,$base";
351                 @allprops = ( "cn" => $real,
352                               "uid" => \@users,
353                               "uidNumber" => $uid,
354                               "loginShell" => $shell,
355                               "homeDirectory" => $home,
356                               "gidNumber" => $gid,
357                               "userPassword" => $pass,
358                               "objectClass" => \@classes,
359                               @props );
360                 if (&indexoflc("person", @classes) >= 0 &&
361                     !&in_props(\@allprops, "sn")) {
362                         # Person needs an 'sn' too
363                         push(@allprops, "sn", $real);
364                         }
365                 $rv = $ldap->add($newdn, attr => \@allprops);
366                 if ($rv->code) {
367                         &error(&text('usave_eadd', $rv->error));
368                         }
369
370                 if ($in{'cyrus'}) {
371                         if ($config{'addressbook'}) {
372                                 # Create addressbook entry
373                                 &setup_addressbook(\%uhash);
374                                 }
375
376                         # Disconnect to save the changes
377                         $ldap->unbind();
378                         undef($ldap);
379
380                         # Create imap account
381                         &setup_imap(\%uhash, $quota);
382
383                         # Re-connect for later LDAP operations
384                         $ldap = &ldap_connect();
385                         }
386
387                 # Copy files into user's directory
388                 if ($in{'makehome'} && $mconfig{'user_files'}) {
389                         local $uf = $mconfig{'user_files'};
390                         local $shell = $user{'shell'}; $shell =~ s/^(.*)\///g;
391                         $uf =~ s/\$group/$in{'gid'}/g;
392                         $uf =~ s/\$gid/$user{'gid'}/g;
393                         $uf =~ s/\$shell/$shell/g;
394                         &useradmin::copy_skel_files($uf, $home, $uid, $gid);
395                         }
396                 }
397         else {
398                 # Modifying a user
399                 $olduser = $uinfo->get_value('uid');
400                 if ($olduser ne $user) {
401                         defined(&all_getpwnam($user)) &&
402                                 &error(&text('usave_einuse', $user));
403                         }
404
405                 # Work out old settings
406                 @classes = $uinfo->get_value("objectClass");
407                 $wassamba = &indexof($samba_class, @classes) >= 0;
408                 @cyrus_class_2 = split(' ',$cyrus_class);
409                 $wascyrus = &indexof($cyrus_class_2[0], @classes) >= 0;
410                 if ($in{'passmode'} == 1 || $in{'passmode'} == 2) {
411                         if (!$wascyrus && $in{'cyrus'}) {
412                                 &error($text{'usave_ecyruspass'});
413                                 }
414                         }
415
416                 # Run the pre-change command
417                 &set_user_envs(\%uhash, 'MODIFY_USER',
418                                $in{'passmode'} == 3 ? $in{'pass'} : "",
419                                undef);
420                 $merr = &making_changes();
421                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
422                         if (defined($merr));
423
424                 # Rename home dir, if needed
425                 $oldhome = $uinfo->get_value("homeDirectory");
426                 if ($home ne $oldhome && -d $oldhome && !-e $home &&
427                     $in{'movehome'}) {
428                         $out = `mv '$oldhome' '$home' 2>&1`;
429                         if ($?) { &error(&text('usave_emove', $out)); }
430                         }
431
432                 # Change GID on files if needed
433                 $oldgid = $uinfo->get_value("gidNumber");
434                 $olduid = $uinfo->get_value("uidNumber");
435                 if ($oldgid != $gid && $in{'chgid'}) {
436                         if ($in{'chgid'} == 1) {
437                                 &useradmin::recursive_change($home, $olduid,
438                                                              $oldgid, -1, $gid);
439                                 }
440                         else {
441                                 &useradmin::recursive_change("/", $olduid,
442                                                              $oldgid, -1, $gid);
443                                 }
444                         }
445
446                 # Change UID on files if needed
447                 if ($olduid != $uid && $in{'chuid'}) {
448                         if ($in{'chuid'} == 1) {
449                                 &useradmin::recursive_change($home, $olduid,
450                                                              -1, $uid, -1);
451                                 }
452                         else {
453                                 &useradmin::recursive_change("/", $olduid,
454                                                              -1, $uid, -1);
455                                 }
456                         }
457
458                 # Get properties for modified users
459                 local @props = &split_props($config{'mod_props'}, \%uhash);
460
461                 # Work out samba-related property changes
462                 $oldpass = $uinfo->get_value('userPassword');
463                 if ($in{'samba'}) {
464                         # Is a samba user .. add or update props
465                         $passmode = $in{'passmode'};
466                         if ($passmode == 2 && $wassamba &&
467                             $in{'encpass'} eq $oldpass) {
468                                 # Not being changed
469                                 $passmode = 4;
470                                 }
471                         &samba_properties(!$wassamba, \%uhash, $passmode,
472                                           $in{'pass'}, $schema, \@props, $ldap);
473                         }
474                 elsif ($wassamba) {
475                         # Is no longer a samba user .. take away standard
476                         # samba properties
477                         &samba_removes(\%uhash, $schema, \@rprops);
478                         }
479
480                 # Work out imap-related property changes
481                 if ($in{'cyrus'}) {
482                         &mail_props();
483                         }
484                 if ($in{'cyrus'} && !$wascyrus) {
485                         # Add any extra properties for IMAP users
486                         push(@props, &split_props($config{'imap_props'}));
487                         }
488                 elsif (!$in{'cyrus'} && $wascyrus) {
489                         # Take away properties for IMAP users
490                         push(@rprops, &split_first($config{'imap_props'}));
491                         &delete_mail_props();
492                         }
493
494                 # Add or update any extra LDAP fields
495                 &parse_extra_fields($config{'fields'}, \@props, \@rprops,
496                                     $ldap, $in{'dn'});
497
498                 # Add or update shadow LDAP fields
499                 $shadow = &shadow_fields();
500
501                 # Update the ldap database
502                 if ($in{'samba'}) {
503                         push(@classes, $samba_class);
504                         }
505                 else {
506                         @classes = grep { $_ ne $samba_class } @classes;
507                         }
508                 if ($in{'cyrus'}) {
509                         push(@classes, split(' ',$cyrus_class));
510                         }
511                 else {
512                        @cyrus_class_4 = split(' ',$cyrus_class);
513                        foreach $one_cyrus_class (@cyrus_class_4) {     
514                                @classes = grep { $_ ne $one_cyrus_class }
515                                                @classes;
516                                }
517                         }
518                 push(@classes, "shadowAccount") if ($shadow);
519                 &name_fields();
520                 @classes = &unique(@classes);
521                 @classes = grep { /\S/ } @classes;      # Remove empty
522                 @rprops = grep { defined($uinfo->get_value($_)) } @rprops;
523
524                 if ($olduser ne $user) {
525                         # Need to rename the LDAP dn itself, first
526                         $renaming = 1;
527                         $base = &get_user_base();
528                         $newdn = "uid=$user,$base";
529                         $rv = $ldap->moddn($in{'dn'}, newrdn => "uid=$user");
530                         if ($rv->code) {
531                                 &error(&text('usave_emoddn', $rv->error));
532                                 }
533                         }
534                 else {
535                         $newdn = $in{'dn'};
536                         }
537
538                 # Change the user's properties
539                 %allprops = ( "cn" => $real,
540                               "uid" => \@users,
541                               "uidNumber" => $uid,
542                               "loginShell" => $shell,
543                               "homeDirectory" => $home,
544                               "gidNumber" => $gid,
545                               "userPassword" => $pass,
546                               "objectClass" => \@classes,
547                               @props );
548                 if (&indexoflc("person", @classes) >= 0 &&
549                     !$allprops{'sn'}) {
550                         # Person needs 'sn'
551                         $allprops{'sn'} = $real;
552                         }
553                 $rv = $ldap->modify($newdn, 'replace' => \%allprops,
554                                             'delete' => \@rprops);
555                 if ($rv->code) {
556                         &error(&text('usave_emod', $rv->error));
557                         }
558
559                 if ($olduser ne $user) {
560                         # Check if an addressbook dn exists
561                         local $olda =
562                                 "ou=$olduser, $config{'addressbook'}";
563                         $rv = $ldap->search(base => $olda,
564                                             scope => 'base',
565                                             filter => '(&(objectClass=organizationalUnit))');
566                         ($oldbook) = $rv->all_entries;
567
568                         if ($oldbook) {
569                                 # Need to rename the addressbook dn
570                                 $rv = $ldap->modify($olda, replace =>
571                                         { "ou" => $user });
572                                 if ($rv->code) {
573                                         &error(&text('usave_emodbook',
574                                                      $rv->error));
575                                         }
576
577                                 $rv = $ldap->moddn($olda, newrdn =>
578                                         "ou=$user");
579                                 if ($rv->code) {
580                                         &error(&text('usave_emodbookdn',
581                                                      $rv->error));
582                                         }
583                                 }
584                         }
585
586                 if ($in{'cyrus'} && !$wascyrus) {
587                         # Adding IMAP support
588                         if ($config{'addressbook'}) {
589                                 # Create addressbook entry
590                                 &setup_addressbook();
591                                 }
592
593                         # Setup the imap account as well
594                         &setup_imap(\%uhash, $quota);
595                         }
596                 elsif (!$in{'cyrus'} && $wascyrus) {
597                         # Removing IMAP support
598                         if ($config{'addressbook'}) {
599                                 # Delete addressbook entry
600                                 &delete_addressbook();
601                                 }
602                         }
603                 elsif ($in{'cyrus'} && $wascyrus) {
604                         # Changing IMAP support
605                         if (!$in{'quota_def'} && $config{'quota_support'}) {
606                                 &set_imap_quota(\%uhash, $in{'quota'});
607                                 }
608                         }
609                 }
610
611         if ($config{'secmode'} != 1) {
612                 # Update any groups that the user has been added to/removed from
613                 @sgnames = $config{'secmode'} == 2 ? split(/\s+/, $in{'sgid'})
614                                                    : split(/\r?\n/, $in{'sgid'});
615                 foreach $gname (@sgnames) {
616                         $ingroup{$gname}++;
617                         }
618                 $base = &get_group_base();
619                 $rv = $ldap->search(base => $base,
620                                     filter => &group_filter());
621                 foreach $g ($rv->all_entries) {
622                         local @mems = $g->get_value("memberUid");
623                         local $gname = $g->get_value("cn");
624                         local $ldap_group_id = $g->get_value("gidNumber");
625                         if ($renaming) {
626                                 local $idx = &indexof($olduser, @mems);
627                                 if ($ingroup{$gname} && $idx<0) {
628                                         # Need to add to the group
629                                         push(@mems, $user);
630                                         push(@sgids, $ldap_group_id);
631                                         }
632                                 elsif (!$ingroup{$gname} && $idx>=0) {
633                                         # Need to remove from the group
634                                         splice(@mems, $idx, 1);
635                                         }
636                                 elsif ($idx >= 0) {
637                                         # Need to rename in group
638                                         $mems[$idx] = $user;
639                                         push(@sgids, $ldap_group_id);
640                                         }
641                                 else { next; }
642                                 }
643                         else {
644                                 local $idx = &indexof($user, @mems);
645                                 if ($ingroup{$gname} && $idx<0) {
646                                         # Need to add to the group
647                                         push(@mems, $user);
648                                         push(@sgids, $ldap_group_id);
649                                         }
650                                 elsif (!$ingroup{$gname} && $idx>=0) {
651                                         # Need to remove from the group
652                                         splice(@mems, $idx, 1);
653                                         }
654                                 elsif ($ingroup{$gname} && $idx >=0) {
655                                         # already in this group
656                                         push(@sgids, $ldap_group_id);
657                                         next;
658                                         }
659                                 else { next; }
660                                 }
661
662                         # Actually change the group
663                         $rv = $ldap->modify($g->dn(), replace =>
664                                 { 'memberUid' => \@mems });
665                         if ($rv->code) {
666                                 &error(&text('usave_emodgroup', $g->get_value('cn'),
667                                              $rv->error));
668                                 }
669                         }
670                 }
671
672         # Get the updated user object
673         $rv = $ldap->search(base => $newdn,
674                             scope => 'base',
675                             filter => &user_filter());
676         ($uinfo) = $rv->all_entries;
677         %user = &dn_to_hash($uinfo);
678
679         # Run post-change script
680         &set_user_envs(\%user, $in{'new'} ? 'CREATE_USER' : 'MODIFY_USER',
681                        $in{'passmode'} == 3 ? $in{'pass'} : "", \@sgids);
682         &made_changes();
683
684         # Run other modules' scripts
685         if ($in{'others'}) {
686                 $user{'passmode'} = $in{'passmode'};
687                 if ($in{'passmode'} == 2 && $user{'pass'} eq $ouser{'pass'}) {
688                         # not changing password
689                         $user{'passmode'} = 4;
690                         }
691                 $user{'plainpass'} = $in{'pass'} if ($in{'passmode'} == 3);
692                 $ldap->unbind();        # force commit?
693                 if (!$in{'new'}) {
694                         $user{'olduser'} = $ouser{'user'};
695                         &useradmin::other_modules("useradmin_modify_user",
696                                                   \%user, \%ouser);
697                         }
698                 else {
699                         &useradmin::other_modules("useradmin_create_user",
700                                                   \%user);
701                         }
702                 $ldap = &ldap_connect();
703                 }
704         }
705 $ldap->unbind();
706 delete($in{'pass'});
707 delete($in{'passmode'});
708 &unlock_user_files();
709 &webmin_log(!$in{'new'} ? 'modify' : 'create', 'user', $user, \%in);
710 &redirect($in{'return'} || "");
711
712 # mail_props()
713 # Add properties for mail and aliases
714 sub mail_props
715 {
716 # Do nothing if no domain is set
717 return if (!$config{'domain'});
718
719 # Add surname and first name details
720 local ($autofirstname, $autolastname);
721 if ($firstname && $lastname) {
722         $autofirstname = $firstname;
723         $autolastname = $lastname;
724         }
725 elsif ($in{'real'} =~ /(\S+)\s+(\S+)$/) {
726         $autofirstname = lc($1);
727         $autolastname = lc($2);
728         }
729 elsif ($in{'real'} =~ /(\S+)/) {
730         $autofirstname = lc($1);
731         }
732 else {
733         $autofirstname = lc($in{'user'});
734         }
735 if ($autolastname) {
736         if (&in_schema($schema, "mail")) {
737                 if ($config{'mailfmt'} == 0) {
738                         push(@props, "mail",
739                                      "$autofirstname.$autolastname\@$config{'domain'}")
740                         }
741                 else {
742                         push(@props, "mail",
743                                      "$user\@$config{'domain'}")
744                         }
745                 }
746         }
747 else {
748         push(@props, "mail", "$autofirstname\@$config{'domain'}")
749                 if (&in_schema($schema, "mail"));
750         }
751
752 # Add extra aliases
753 local $aattr = $config{'maillocaladdress'} || "alias";
754 if (&in_schema($schema, $aattr)) {
755         local @alias = split(/\s+/, $in{'alias'});
756         if ($in{'alias'}) {
757                 if (!$config{'alias_same'}) {
758                         ($dup, $dupwhat) = &check_duplicates($ldap, $aattr, \@alias, $in{'dn'});
759                         $dup && &error(&text('save_ealiasdup', $dupwhat, $dup->dn()));
760                         }
761                 push(@props, $aattr, \@alias);
762                 }
763         else {
764                 push(@rprops, $aattr);
765                 }
766         }
767 local $battr = $config{'mailroutingaddress'};
768 push(@props, $battr, lc($in{'user'})."\@$config{'imap_host'}")
769         if ($battr ne "") && (&in_schema($schema, $battr));
770 }
771
772 # delete_mail_props()
773 # Take away any extra properties added by mail_props
774 sub delete_mail_props
775 {
776 local $aattr = $config{'maillocaladdress'} || "alias";
777 if (&in_schema($schema, $aattr)) {
778         push(@rprops, $aattr);
779         }
780 local $battr = $config{'mailroutingaddress'};
781 if (($battr ne "") && &in_schema($schema, $battr)) {
782         push(@rprops, $battr);
783         }
784 push(@rprops, "mail")
785         if (&in_schema($schema, "mail"));
786 }
787
788 sub delete_addressbook
789 {
790 return &delete_ldap_subtree($ldap, "ou=$user, $config{'addressbook'}");
791 }
792
793 sub name_fields
794 {
795 if ($config{'given'}) {
796         if ($firstname) {
797                 if (&in_schema($schema, "gn")) {
798                         push(@props, "gn", $firstname);
799                         }
800                 elsif (&in_schema($schema, "givenName")) {
801                         push(@props, "givenName", $firstname)
802                         }
803                 }
804         if ($lastname && &in_schema($schema, "sn")) {
805                 push(@props, "sn", $lastname);
806                 }
807         if ($firstname || $lastname) {
808                 push(@classes, $config{'given_class'});
809                 }
810         }
811 if (&in_schema($schema, "gecos")) {
812         push(@props, "gecos", &remove_accents($in{'real'}));
813         }
814 }
815
816 sub shadow_fields
817 {
818 if (&in_schema($schema, "shadowLastChange")) {
819         # Validate shadow-password inputs
820         $in{'min'} =~ /^\-?[0-9]*$/ ||
821                 &error(&text('usave_emin', $in{'min'}));
822         if ($in{'min'} ne '') {
823                 push(@props, "shadowMin", $in{'min'});
824                 }
825         else {
826                 push(@rprops, "shadowMin");
827                 }
828         $in{'max'} =~ /^\-?[0-9]*$/ ||
829                 &error(&text('usave_emax', $in{'max'}));
830         if ($in{'max'} ne '') {
831                 push(@props, "shadowMax", $in{'max'});
832                 }
833         else {
834                 push(@rprops, "shadowMax");
835                 }
836         if ($in{'expired'} ne "" && $in{'expirem'} ne ""
837             && $in{'expirey'} ne "") {
838                 eval { $expire = timelocal(0, 0, 12,
839                                         $in{'expired'},
840                                         $in{'expirem'}-1,
841                                         $in{'expirey'}-1900); };
842                 if ($@) { &error($text{'usave_eexpire'}); }
843                 push(@props, "shadowExpire", int($expire / (60*60*24)));
844                 }
845         else {
846                 push(@rprops, "shadowExpire");
847                 }
848         $in{'warn'} =~ /^\-?[0-9]*$/ ||
849             &error(&text('usave_ewarn', $in{'warn'}));
850         if ($in{'warn'} ne '') {
851                 push(@props, "shadowWarning", $in{'warn'});
852                 }
853         else {
854                 push(@rprops, "shadowWarning");
855                 }
856         $in{'inactive'} =~ /^\-?[0-9]*$/ ||
857             &error(&text('usave_einactive', $in{'inactive'}));
858         if ($in{'inactive'} ne '') {
859                 push(@props, "shadowInactive", $in{'inactive'});
860                 }
861         else {
862                 push(@rprops, "shadowInactive");
863                 }
864         if ($in{'passmode'} == 3 ||
865             $in{'passmode'} == 2 && $pass ne $oldpass) {
866                 $daynow = int(time() / (60*60*24));
867                 push(@props, "shadowLastChange", $daynow);
868                 }
869         return 1;
870         }
871 else {
872         return 0;
873         }
874 }
875