Handle hostnames with upper-case letters
[webmin.git] / useradmin / batch_exec.cgi
1 #!/usr/local/bin/perl
2 # batch_exec.cgi
3 # Execute create/modify/delete commands in a batch file
4
5 require './user-lib.pl';
6 $access{'batch'} || &error($text{'batch_ecannot'});
7 if ($ENV{'REQUEST_METHOD'} eq 'GET') {
8         &ReadParse();
9         }
10 else {
11         &ReadParseMime();
12         }
13 if ($in{'source'} == 0) {
14         $data = $in{'file'};
15         $data =~ /\S/ || &error($text{'batch_efile'});
16         }
17 elsif ($in{'source'} == 1) {
18         open(LOCAL, $in{'local'}) || &error($text{'batch_elocal'});
19         while(<LOCAL>) {
20                 $data .= $_;
21                 }
22         close(LOCAL);
23         }
24 elsif ($in{'source'} == 2) {
25         $data = $in{'text'};
26         $data =~ /\S/ || &error($text{'batch_etext'});
27         }
28
29 &ui_print_unbuffered_header(undef, $text{'batch_title'}, "");
30
31 # Force defaults for save options
32 $in{'makehome'} = 1 if (!$access{'makehome'});
33 $in{'copy'} = 1 if (!$access{'copy'} && $config{'user_files'} =~ /\S/);
34 $in{'movehome'} = 1 if (!$access{'movehome'});
35 $in{'chuid'} = 1 if (!$access{'chuid'});
36 $in{'chgid'} = 1 if (!$access{'chgid'});
37
38 # Work out a good base UID for new users
39 &build_user_used(\%used, undef, \%taken);
40 $newuid = int($config{'base_uid'} > $access{'lowuid'} ?
41               $config{'base_uid'} : $access{'lowuid'});
42
43 # Work out a good base GID for new groups
44 &build_group_used(\%gused, \%gtaken);
45 if ($config{'new_user_gid'}) {
46         %used = ( %used, %gused );
47         }
48 $newgid = int($config{'base_gid'} > $access{'lowgid'} ?
49               $config{'base_gid'} : $access{'lowgid'});
50 @glist = &list_groups();
51
52 # Process the file
53 &batch_start() if ($in{'batch'});
54 &lock_user_files();
55 $lnum = $created = $modified = $deleted = 0;
56 print "<pre>\n";
57 $pft = &passfiles_type();
58 foreach $line (split(/[\r\n]+/, $data)) {
59         $lnum++;
60         $line =~ s/^\s*#.*$//;
61         next if ($line !~ /\S/);
62         local @line = split(/:/, $line, -1);
63         local %user;
64         if ($line[0] eq 'create') {
65                 # Creating a new user
66                 if ($pft == 5) {
67                         # Openserver passwd and short shadow information
68                         if (@line != 10) {
69                                 print &text('batch_elen', $lnum, 10),"\n";
70                                 next;
71                                 }
72                         $user{'min'} = $line[8];
73                         $user{'max'} = $line[9];
74                         }
75                 elsif ($pft == 4) {
76                         # AIX passwd and security information
77                         if (@line != 12) {
78                                 print &text('batch_elen', $lnum, 12),"\n";
79                                 next;
80                                 }
81                         $user{'min'} = $line[8];
82                         $user{'max'} = $line[9];
83                         $user{'expire'} = $line[10];
84                         map { $user{$_}++ } split(/\s+/, $line[11]);
85                         }
86                 elsif ($pft == 2) {
87                         # SYSV-style passwd and shadow information
88                         if (@line != 13) {
89                                 print &text('batch_elen', $lnum, 13),"\n";
90                                 next;
91                                 }
92                         $user{'min'} = $line[8];
93                         $user{'max'} = $line[9];
94                         $user{'warn'} = $line[10];
95                         $user{'inactive'} = $line[11];
96                         $user{'expire'} = $line[12];
97                         $user{'change'} = $line[2] eq '' ? '' :
98                                                 int(time() / (60*60*24));
99                         }
100                 elsif ($pft == 1 || $pft == 6) {
101                         # BSD master.passwd information
102                         if (@line != 11) {
103                                 print &text('batch_elen', $lnum, 11),"\n";
104                                 next;
105                                 }
106                         $user{'class'} = $line[8];
107                         $user{'change'} = $line[9];
108                         $user{'expire'} = $line[10];
109                         }
110                 else {
111                         # Classic passwd file information (type 0 and 3)
112                         if (@line != 8) {
113                                 print &text('batch_elen', $lnum, 8),"\n";
114                                 next;
115                                 }
116                         }
117
118                 # Make sure all min/max fields are numeric
119                 $err = &validate_batch_minmax(\%user, $lnum);
120                 if ($err) {
121                         print $err,"\n";
122                         next;
123                         }
124
125                 # Parse common fields
126                 if (!$line[1]) {
127                         print &text('batch_eline', $lnum),"\n";
128                         next;
129                         }
130                 $user{'user'} = $line[1];
131                 $err = &check_username_restrictions($user{'user'});
132                 if ($err) {
133                         print &text('batch_echeck', $lnum, $err),"\n";
134                         next;
135                         }
136                 if ($taken{$user{'user'}}) {
137                         print &text('batch_euser', $lnum, $user{'user'}),"\n";
138                         next;
139                         }
140                 if ($line[3] !~ /^\d+$/) {
141                         # make up a UID
142                         while($used{$newuid}) {
143                                 $newuid++;
144                                 }
145                         $user{'uid'} = $newuid;
146                         }
147                 else {
148                         # use the given UID
149                         if ($used{$line[3]} && !$access{'umultiple'}) {
150                                 print &text('batch_ecaccess', $lnum,
151                                             $text{'usave_euidused2'}),"\n";
152                                 next;
153                                 }
154                         $user{'uid'} = $line[3];
155                         }
156                 $used{$user{'uid'}}++;
157                 if ($line[7] !~ /^\//) {
158                         print &text('batch_eshell', $lnum, $line[7]),"\n";
159                         next;
160                         }
161                 $user{'shell'} = $line[7];
162                 $user{'real'} = $line[5];
163                 local @gids = split(/[ ,]+/, $line[4]);
164                 $user{'gid'} = $gids[0];
165                 local $grp = &my_getgrgid($gids[0]);
166
167                 $real_home = undef;
168                 if ($access{'autohome'}) {
169                         # Assign home dir automatically based on ACL
170                         $user{'home'} = &auto_home_dir($access{'home'},
171                                                        $user{'user'},
172                                                        $grp);
173                         if ($config{'real_base'}) {
174                                 $real_home = &auto_home_dir(
175                                     $config{'real_base'}, $user{'user'}, $grp);
176                                 }
177                         }
178                 else {
179                         if ($line[6] eq '' && $config{'home_base'}) {
180                                 # Choose home dir automatically based on
181                                 # module config
182                                 $user{'home'} = &auto_home_dir(
183                                         $config{'home_base'}, $user{'user'},
184                                         $user{'gid'});
185                                 if ($config{'real_base'}) {
186                                         $real_home = &auto_home_dir(
187                                             $config{'real_base'},
188                                             $user{'user'}, $grp);
189                                         }
190                                 }
191                         elsif ($line[6] !~ /^\//) {
192                                 print &text('batch_ehome', $lnum,$line[6]),"\n";
193                                 next;
194                                 }
195                         else {
196                                 # Use given home dir
197                                 $user{'home'} = $line[6];
198                                 }
199                         }
200                 $real_home ||= $user{'home'};
201
202                 # Check access control restrictions
203                 if (!$access{'ucreate'}) {
204                         print &text('batch_ecaccess', $lnum,
205                                     $text{'usave_ecreate'});
206                         next;
207                         }
208                 local $ch = &check_user(\%user);
209                 if ($ch) {
210                         print &text('batch_ecaccess', $lnum, $ch),"\n";
211                         next;
212                         }
213
214                 # Work out secondary group membership
215                 local @secs;
216                 if (@gids > 1) {
217                         local $i;
218                         for($i=1; $i<@gids; $i++) {
219                                 local ($group) =
220                                     grep { $_->{'gid'} eq $gids[$i] } @glist;
221                                 push(@secs, $group) if ($group);
222                                 }
223                         }
224
225                 # Work out the password
226                 if ($in{'crypt'}) {
227                         $user{'pass'} = $line[2];
228                         $user{'passmode'} = 2;
229                         }
230                 elsif ($line[2] eq 'x') {
231                         # No login allowed
232                         $user{'pass'} = $config{'lock_string'};
233                         $user{'passmode'} = 1;
234                         }
235                 elsif ($line[2] eq '') {
236                         # No password needed
237                         $user{'pass'} = '';
238                         $user{'passmode'} = 0;
239                         }
240                 else {
241                         # Normal password
242                         $user{'pass'} = &encrypt_password($line[2]);
243                         $user{'passmode'} = 3;
244                         $user{'plainpass'} = $line[2];
245                         }
246
247                 # Run the before command
248                 &set_user_envs(\%user, 'CREATE_USER', $user{'plainpass'},
249                                [ map { $_->{'gid'} } @secs ]);
250                 $merr = &making_changes();
251                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
252                         if (defined($merr));
253
254                 if ($user{'gid'} !~ /^\d+$/) {
255                         # Need to create a new group for the user
256                         if (!$access{'gcreate'}) {
257                                 print &text('batch_ecaccess', $lnum,
258                                             $text{'usave_egcreate'}),"\n";
259                                 next;
260                                 }
261                         if ($gtaken{$user{'user'}}) {
262                                 print &text('batch_egtaken', $lnum,
263                                             $user{'user'}),"\n";
264                                 next;
265                                 }
266
267                         if ($config{'new_user_gid'}) {
268                                 $newgid = $user{'uid'};
269                                 }
270                         else {
271                                 while($gused{$newgid}) {
272                                         $newgid++;
273                                         }
274                                 }
275                         local %group;
276                         $group{'group'} = $user{'user'};
277                         $user{'gid'} = $group{'gid'} = $newgid;
278                         &create_group(\%group);
279                         $gused{$group{'gid'}}++;
280                         }
281
282                 # Create the user!
283                 if ($in{'makehome'} && !-d $user{'home'}) {
284                         &create_home_directory(\%user, $real_home);
285                         }
286                 &create_user(\%user);
287
288                 # Add user to some secondary groups
289                 local $group;
290                 foreach $group (@secs) {
291                         local @mems = split(/,/ , $group->{'members'});
292                         push(@mems, $user{'user'});
293                         $group->{'members'} = join(",", @mems);
294                         &modify_group($group, $group);
295                         }
296
297                 # All done
298                 &made_changes();
299
300                 # Call other modules, ignoring any failures
301                 $error_must_die = 1;
302                 eval {
303                         &other_modules("useradmin_create_user", \%user)
304                                 if ($access{'cothers'} == 1 && $in{'others'} ||
305                                     $access{'cothers'} == 0);
306                         };
307                 $other_err = $@;
308                 $error_must_die = 0;
309
310                 if ($in{'copy'} && $in{'makehome'}) {
311                         # Copy files to user's home directory
312                         local $groupname = &my_getgrgid($user{'gid'});
313                         local $uf = &get_skel_directory(\%user, $groupname);
314                         &copy_skel_files($uf, $user{'home'},
315                                          $user{'uid'}, $user{'gid'});
316                         }
317
318                 print "<b>",&text('batch_created',$user{'user'}),"</b>\n";
319                 print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
320                         if ($other_err);
321                 $created++;
322                 }
323         elsif ($line[0] eq 'delete') {
324                 # Deleting an existing user
325                 if (@line != 2) {
326                         print &text('batch_elen', $lnum, 2),"\n";
327                         next;
328                         }
329                 local @ulist = &list_users();
330                 local ($user) = grep { $_->{'user'} eq $line[1] } @ulist;
331                 if (!$user) {
332                         print &text('batch_enouser', $lnum, $line[1]),"\n";
333                         next;
334                         }
335                 if (!&can_edit_user(\%access, $user)) {
336                         print &text('batch_edaccess', $lnum,
337                                     $text{'udel_euser'}),"\n";
338                         next;
339                         }
340                 if (!$config{'delete_root'} && $user->{'uid'} <= 10) {
341                         print &text('batch_edaccess', $lnum,
342                                     $text{'udel_eroot'}),"\n";
343                         next;
344                         }
345
346                 # Run the before command
347                 &set_user_envs($user, 'DELETE_USER', undef,
348                                [ &secondary_groups($user->{'user'}) ]);
349                 $merr = &making_changes();
350                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
351                         if (defined($merr));
352
353                 # Delete from other modules, ignoring errors
354                 $error_must_die = 1;
355                 eval {
356                         &other_modules("useradmin_delete_user", $user)
357                                 if ($access{'dothers'} == 1 && $in{'others'} ||
358                                     $access{'dothers'} == 0);
359                         };
360                 $other_err = $@;
361                 $error_must_die = 0;
362
363                 # Delete the user entry
364                 &delete_user($user);
365
366                 # Delete the user from groups
367                 foreach $g (&list_groups()) {
368                         @mems = split(/,/, $g->{'members'});
369                         $idx = &indexof($user->{'user'}, @mems);
370                         if ($idx >= 0) {
371                                 splice(@mems, $idx, 1);
372                                 %newg = %$g;
373                                 $newg{'members'} = join(',', @mems);
374                                 &modify_group($g, \%newg);
375                                 }
376                         $mygroup = $g if ($g->{'group'} eq $user->{'user'});
377                         }
378
379                 # Delete the user's group
380                 if ($mygroup && !$mygroup->{'members'}) {
381                         local $another;
382                         foreach $ou (&list_users()) {
383                                 $another++
384                                         if ($ou->{'gid'} == $mygroup->{'gid'});
385                                 }
386                         if (!$another) {
387                                 &delete_group($mygroup);
388                                 }
389                         }
390                 &made_changes();
391
392                 # Delete his home directory
393                 if ($in{'delhome'} &&
394                     $user->{'home'} &&
395                     $user->{'home'} !~ /^\/+$/) {
396                         &delete_home_directory($user);
397                         }
398
399                 print "<b>",&text('batch_deleted',$user->{'user'}),"</b>\n";
400                 print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
401                         if ($other_err);
402                 $deleted++;
403                 }
404         elsif ($line[0] eq 'modify') {
405                 # Modifying an existing user
406                 local $wlen = $pft == 5 ? 11 :
407                               $pft == 4 ? 13 :
408                               $pft == 2 ? 14 :
409                               $pft == 1 || $pft == 6 ? 12 : 9;
410                 if (@line != $wlen) {
411                         print &text('batch_elen', $lnum, $wlen),"\n";
412                         next;
413                         }
414                 local @ulist = &list_users();
415                 local ($user) = grep { $_->{'user'} eq $line[1] } @ulist;
416                 if (!$user) {
417                         print &text('batch_enouser', $lnum, $line[1]),"\n";
418                         next;
419                         }
420                 %olduser = %user = %$user;
421                 $user{'olduser'} = $user->{'user'};
422                 if (!&can_edit_user(\%access, \%user)) {
423                         print &text('batch_emaccess', $lnum,
424                                     $text{'usave_eedit'}),"\n";
425                         next;
426                         }
427
428                 # Update supplied fields
429                 if ($line[2] ne '') {
430                         if (!$access{'urename'}) {
431                                 print &text('batch_erename', $lnum, $line[1]),"\n";
432                                 }
433                         $user{'user'} = $line[2];
434                         }
435                 if ($in{'crypt'} && $line[3] ne '') {
436                         # Changing to pre-encrypted password
437                         $user{'pass'} = $line[3];
438                         $user{'passmode'} = 2;
439                         }
440                 elsif ($line[3] eq 'x') {
441                         # No login allowed
442                         $user{'pass'} = $config{'lock_string'};
443                         $user{'passmode'} = 1;
444                         }
445                 elsif ($line[3] ne '') {
446                         # Normal password
447                         $user{'pass'} = &encrypt_password($line[3]);
448                         $user{'passmode'} = 3;
449                         $user{'plainpass'} = $line[3];
450                         }
451                 else {
452                         # No change
453                         $user{'passmode'} = 4;
454                         }
455                 $user{'uid'} = $line[4] if ($line[4] ne '');
456                 $user{'gid'} = $line[5] if ($line[5] ne '');
457                 $user{'real'} = $line[6] if ($line[6] ne '');
458                 $user{'home'} = $line[7] if ($line[7] ne '');
459                 $user{'shell'} = $line[8] if ($line[8] ne '');
460                 if ($access{'peopt'}) {
461                         if ($pft == 5) {
462                                 # Openserver password and short shadow
463                                 $user{'min'}=$line[9] if ($line[9] ne '');
464                                 $user{'max'}=$line[10] if ($line[10] ne '');
465                                 $user{'change'}=int(time() / (60*60*24))
466                                         if ($line[3] ne '');
467                                 }
468                         elsif ($pft == 4) {
469                                 # AIX password and security information
470                                 $user{'min'}=$line[9] if ($line[9] ne '');
471                                 $user{'max'}=$line[10] if ($line[10] ne '');
472                                 $user{'expire'}=$line[11] if ($line[11] ne '');
473                                 if ($line[12] ne '') {
474                                         delete($user{'admin'});
475                                         delete($user{'admchg'});
476                                         delete($user{'nocheck'});
477                                         map { $user{$_}++ }
478                                             split(/\s+/, $line[12]);
479                                         }
480                                 $user{'change'}=time() if ($line[3] ne '');
481                                 }
482                         elsif ($pft == 2) {
483                                 # SYSV-style passwd and shadow information
484                                 $user{'min'}=$line[9] if ($line[9] ne '');
485                                 $user{'max'}=$line[10] if ($line[10] ne '');
486                                 $user{'warn'}=$line[11] if ($line[11] ne '');
487                                 $user{'inactive'}=$line[12]
488                                         if ($line[12] ne '');
489                                 $user{'expire'}=$line[13] if ($line[13] ne '');
490                                 $user{'change'}=int(time() / (60*60*24))
491                                         if ($line[3] ne '');
492                                 }
493                         elsif ($pft == 1 || $pft == 6) {
494                                 # BSD master.passwd information
495                                 $user{'class'}=$line[9] if ($line[9] ne '');
496                                 $user{'change'}=$line[10] if ($line[10] ne '');
497                                 $user{'expire'}=$line[11] if ($line[11] ne '');
498                                 }
499                         }
500
501                 # Check access control restrictions
502                 local $ch = &check_user(\%user, \%olduser);
503                 if ($ch) {
504                         print &text('batch_emaccess', $lnum, $ch),"\n";
505                         next;
506                         }
507
508                 # Run the before command
509                 &set_user_envs(\%user, 'MODIFY_USER', $user{'plainpass'},
510                                [ &secondary_groups($user{'user'}) ]);
511                 $merr = &making_changes();
512                 &error(&text('usave_emaking', "<tt>$merr</tt>"))
513                         if (defined($merr));
514
515                 # Move home directory if needed
516                 if ($olduser{'home'} ne $user{'home'} && $in{'movehome'} &&
517                     $user{'home'} ne '/' && $olduser{'home'} ne '/') {
518                         if (-d $olduser{'home'} && !-e $user{'home'}) {
519                                 local $out = &backquote_logged(
520                                         "mv \"$olduser{'home'}\" ".
521                                         "\"$user{'home'}\" 2>&1");
522                                 if ($?) { &error(&text('batch_emove',
523                                                  $lnum, $out)); }
524                                 }
525                         }
526
527                 # Change UIDs and GIDs
528                 if ($olduser{'gid'} != $user{'gid'} && $in{'chgid'}) {
529                         if ($in{'chgid'} == 1) {
530                                 &recursive_change($user{'home'},$olduser{'uid'},
531                                           $olduser{'gid'}, -1, $user{'gid'});
532                                 }
533                         else {
534                                 &recursive_change("/", $olduser{'uid'},
535                                           $olduser{'gid'}, -1, $user{'gid'});
536                                 }
537                         }
538                 if ($olduser{'uid'} != $user{'uid'} && $in{'chuid'}) {
539                         if ($in{'chuid'} == 1) {
540                                 &recursive_change($user{'home'},$olduser{'uid'},
541                                                   -1, $user{'uid'}, -1);
542                                 }
543                         else {
544                                 &recursive_change("/", $olduser{'uid'},
545                                                   -1, $user{'uid'}, -1);
546                                 }
547                         }
548
549                 # Actually modify the user
550                 &modify_user(\%olduser, \%user);
551
552                 # If the user has been renamed, update any secondary groups
553                 if ($olduser{'user'} ne $user{'user'}) {
554                         foreach $group (@glist) {
555                                 local @mems = split(/,/, $group->{'members'});
556                                 local $idx = &indexof($olduser{'user'}, @mems);
557                                 if ($idx >= 0) {
558                                         $mems[$idx] = $user{'user'};
559                                         $group->{'members'} = join(",", @mems);
560                                         &modify_group($group, $group);
561                                         }
562                                 }
563                         }
564
565                 &made_changes();
566
567                 # Modify in other modules, ignoring errors
568                 $error_must_die = 1;
569                 eval {
570                         &other_modules("useradmin_modify_user",
571                                        \%user, \%olduser)
572                                 if ($access{'mothers'} == 1 && $in{'others'} ||
573                                     $access{'mothers'} == 0);
574                         };
575                 $error_must_die = 0;
576                 $other_err = $@;
577
578                 print "<b>",&text('batch_modified',$olduser{'user'}),"</b>\n";
579                 print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
580                         if ($other_err);
581                 $modified++;
582                 }
583         else {
584                 print &text('batch_eaction', $lnum, $line[0]),"\n";
585                 next;
586                 }
587         }
588 print "</pre>\n";
589 &batch_end() if ($in{'batch'});
590 &unlock_user_files();
591 &webmin_log("batch", undef, $in{'source'} == 1 ? $in{'local'} : undef,
592             { 'created' => $created, 'modified' => $modified,
593               'deleted' => $deleted, 'lnum' => $lnum } );
594
595 &ui_print_footer("batch_form.cgi", $text{'batch_return'},
596                  "", $text{'index_return'});
597
598 # check_user(\%user, [\%olduser])
599 # Check access control restrictions for a user
600 sub check_user
601 {
602 # check if uid is within range
603 if ($access{'lowuid'} && $_[0]->{'uid'} < $access{'lowuid'}) {
604         return &text('usave_elowuid', $access{'lowuid'});
605         }
606 if ($access{'hiuid'} && $_[0]->{'uid'} > $access{'hiuid'}) {
607         return &text('usave_ehiuid', $access{'hiuid'});
608         }
609 if ($_[1] && !$access{'uuid'} && $_[1]->{'uid'} != $_[0]->{'uid'}) {
610         return $text{'usave_euuid'};
611         }
612
613 # make sure home dir is under the allowed root
614 if (!$access{'autohome'}) {
615         $al = length($access{'home'});
616         if (length($_[0]->{'home'}) < $al ||
617             substr($_[0]->{'home'}, 0, $al) ne $access{'home'}) {
618                 return &text('usave_ehomepath', $_[0]->{'home'});
619                 }
620         }
621
622 # check for invalid shell
623 if ($access{'shells'} ne '*' &&
624     &indexof($_[0]->{'shell'}, split(/\s+/, $access{'shells'})) < 0) {
625         return &text('usave_eshell', $_[0]->{'shell'});
626         }
627
628 # check for invalid primary group (unless one is dynamically assigned)
629 if ($user{'gid'} ne '') {
630         local $ng = &my_getgrgid($_[0]->{'gid'});
631         local $ni = &can_use_group(\%access, $ng);
632         if ($_[1]) {
633                 if ($_[1]->{'gid'} != $_[0]->{'gid'}) {
634                         local $og = &my_getgrgid($_[1]->{'gid'});
635                         local $oi = &can_use_group(\%access, $og);
636                         if (!$ni) { return &text('usave_eprimary', $ng); }
637                         if (!$oi) { return &text('usave_eprimaryr', $og); }
638                         }
639                 }
640         else {
641                 return &text('usave_eprimary', $ng) if (!$ni);
642                 }
643         }
644 return undef;
645 }
646
647 sub secondary_groups
648 {
649 local @secs;
650 foreach $g (@glist) {
651         @mems = split(/,/, $g->{'members'});
652         if (&indexof($_[0], @mems) >= 0) {
653                 push(@secs, $g->{'gid'});
654                 }
655         }
656 return @secs;
657 }
658
659 sub validate_batch_minmax
660 {
661 local ($user, $lnum) = @_;
662 foreach my $f ('min', 'max', 'warn', 'inactive', 'expire', 'change') {
663         $user->{$f} =~ /^(\-|\+|)\d*$/ ||
664                 return &text('batch_e'.$f, $lnum, $user->{$f});
665         }
666 return undef;
667 }
668