Handle hostnames with upper-case letters
[webmin.git] / cron / cron-lib.pl
1 =head1 cron-lib.pl
2
3 Functions for listing, creating and managing Unix users' cron jobs.
4
5  foreign_require("cron", "cron-lib.pl");
6  @jobs = cron::list_cron_jobs();
7  $job = { 'user' => 'root',
8           'active' => 1,
9           'command' => 'ls -l >/dev/null',
10           'special' => 'hourly' };
11  cron::create_cron_job($job);
12
13 =cut
14
15 BEGIN { push(@INC, ".."); };
16 use WebminCore;
17 &init_config();
18 %access = &get_module_acl();
19 $env_support = $config{'vixie_cron'};
20 if ($module_info{'usermin'}) {
21         $single_user = $remote_user;
22         &switch_to_remote_user();
23         &create_user_config_dirs();
24         $range_cmd = "$user_module_config_directory/range.pl";
25         $hourly_only = 0;
26         }
27 else {
28         $range_cmd = "$module_config_directory/range.pl";
29         $hourly_only = $access{'hourly'} == 0 ? 0 :
30                        $access{'hourly'} == 1 ? 1 :
31                         $config{'hourly_only'};
32         }
33 $temp_delete_cmd = "$module_config_directory/tempdelete.pl";
34 $cron_temp_file = &transname();
35 use Time::Local;
36
37 =head2 list_cron_jobs
38
39 Returns a lists of structures of all cron jobs, each of which is a hash
40 reference with the following keys :
41
42 =item user - Unix user the job runs as.
43
44 =item command - The full command to be run.
45
46 =item active - Set to 0 if the job is commented out, 1 if active.
47
48 =item mins - Minute or comma-separated list of minutes the job will run, or * for all.
49
50 =item hours - Hour or comma-separated list of hours the job will run, or * for all.
51
52 =item days - Day or comma-separated list of days of the month the job will run, or * for all.
53
54 =item month - Month number or comma-separated list of months (started from 1) the job will run, or * for all.
55
56 =item weekday - Day of the week or comma-separated list of days (where 0 is sunday) the job will run, or * for all
57
58 =cut
59 sub list_cron_jobs
60 {
61 local (@rv, $lnum, $f);
62 if (scalar(@cron_jobs_cache)) {
63         return @cron_jobs_cache;
64         }
65
66 # read the master crontab file
67 if ($config{'system_crontab'}) {
68         $lnum = 0;
69         &open_readfile(TAB, $config{'system_crontab'});
70         while(<TAB>) {
71                 # Comment line in Fedora 13
72                 next if (/^#+\s+\*\s+\*\s+\*\s+\*\s+\*\s+command\s+to\s+be\s+executed/);
73
74                 if (/^(#+)?[\s\&]*(-)?\s*([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+(([0-9\-\*\/]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|,)+)\s+(([0-9\-\*\/]+|sun|mon|tue|wed|thu|fri|sat|,)+)\s+(\S+)\s+(.*)/i) {
75                         # A normal h m s d w time
76                         push(@rv, { 'file' => $config{'system_crontab'},
77                                     'line' => $lnum,
78                                     'type' => 1,
79                                     'nolog' => $2,
80                                     'active' => !$1,
81                                     'mins' => $3, 'hours' => $4,
82                                     'days' => $5, 'months' => $6,
83                                     'weekdays' => $8, 'user' => $10,
84                                     'command' => $11,
85                                     'index' => scalar(@rv) });
86                         if ($rv[$#rv]->{'user'} =~ /^\//) {
87                                 # missing the user, as in redhat 7 !
88                                 $rv[$#rv]->{'command'} = $rv[$#rv]->{'user'}.
89                                         ' '.$rv[$#rv]->{'command'};
90                                 $rv[$#rv]->{'user'} = 'root';
91                                 }
92                         &fix_names($rv[$#rv]);
93                         }
94                 elsif (/^(#+)?\s*@([a-z]+)\s+(\S+)\s+(.*)/i) {
95                         # An @ time
96                         push(@rv, { 'file' => $config{'system_crontab'},
97                                     'line' => $lnum,
98                                     'type' => 1,
99                                     'active' => !$1,
100                                     'special' => $2,
101                                     'user' => $3,
102                                     'command' => $4,
103                                     'index' => scalar(@rv) });
104                         }
105                 $lnum++;
106                 }
107         close(TAB);
108         }
109
110 # read package-specific cron files
111 opendir(DIR, &translate_filename($config{'cronfiles_dir'}));
112 while($f = readdir(DIR)) {
113         next if ($f =~ /^\./);
114         $lnum = 0;
115         &open_readfile(TAB, "$config{'cronfiles_dir'}/$f");
116         while(<TAB>) {
117                 if (/^(#+)?[\s\&]*(-)?\s*([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+(([0-9\-\*\/]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|,)+)\s+(([0-9\-\*\/]+|sun|mon|tue|wed|thu|fri|sat|,)+)\s+(\S+)\s+(.*)/i) {
118                         push(@rv, { 'file' => "$config{'cronfiles_dir'}/$f",
119                                     'line' => $lnum,
120                                     'type' => 2,
121                                     'active' => !$1,
122                                     'nolog' => $2,
123                                     'mins' => $3, 'hours' => $4,
124                                     'days' => $5, 'months' => $6,
125                                     'weekdays' => $8, 'user' => $10,
126                                     'command' => $11,
127                                     'index' => scalar(@rv) });
128                         &fix_names($rv[$#rv]);
129                         }
130                 elsif (/^(#+)?\s*@([a-z]+)\s+(\S+)\s+(.*)/i) {
131                         push(@rv, { 'file' => "$config{'cronfiles_dir'}/$f",
132                                     'line' => $lnum,
133                                     'type' => 2,
134                                     'active' => !$1,
135                                     'special' => $2,
136                                     'user' => $3,
137                                     'command' => $4,
138                                     'index' => scalar(@rv) });
139                         }
140                 $lnum++;
141                 }
142         close(TAB);
143         }
144 closedir(DIR);
145
146 # Read a single user's crontab file
147 if ($config{'single_file'}) {
148         &open_readfile(TAB, $config{'single_file'});
149         $lnum = 0;
150         while(<TAB>) {
151                 if (/^(#+)?[\s\&]*(-)?\s*([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+(([0-9\-\*\/]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|,)+)\s+(([0-9\-\*\/]+|sun|mon|tue|wed|thu|fri|sat|,)+)\s+(.*)/i) {
152                         # A normal m h d m wd time
153                         push(@rv, { 'file' => $config{'single_file'},
154                                     'line' => $lnum,
155                                     'type' => 3,
156                                     'active' => !$1, 'nolog' => $2,
157                                     'mins' => $3, 'hours' => $4,
158                                     'days' => $5, 'months' => $6,
159                                     'weekdays' => $8, 'user' => "NONE",
160                                     'command' => $10,
161                                     'index' => scalar(@rv) });
162                         &fix_names($rv[$#rv]);
163                         }
164                 elsif (/^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*'([^']*)'/ ||
165                        /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*"([^']*)"/ ||
166                        /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*(\S+)/) {
167                         # An environment variable
168                         push(@rv, { 'file' => $config{'single_file'},
169                                     'line' => $lnum,
170                                     'active' => !$1,
171                                     'name' => $2,
172                                     'value' => $3,
173                                     'user' => "NONE",
174                                     'index' => scalar(@rv) });
175                         }
176                 $lnum++;
177                 }
178         close(TAB);
179         }
180
181
182 # read per-user cron files
183 local $fcron = ($config{'cron_dir'} =~ /\/fcron$/);
184 local @users;
185 if ($single_user) {
186         @users = ( $single_user );
187         }
188 else {
189         opendir(DIR, &translate_filename($config{'cron_dir'}));
190         @users = grep { !/^\./ } readdir(DIR);
191         closedir(DIR);
192         }
193 foreach $f (@users) {
194         next if (!(@uinfo = getpwnam($f)));
195         $lnum = 0;
196         if ($single_user) {
197                 &open_execute_command(TAB, $config{'cron_user_get_command'}, 1);
198                 }
199         elsif ($fcron) {
200                 &open_execute_command(TAB,
201                         &user_sub($config{'cron_get_command'}, $f), 1);
202                 }
203         else {
204                 &open_readfile(TAB, "$config{'cron_dir'}/$f");
205                 }
206         while(<TAB>) {
207                 if (/^(#+)?[\s\&]*(-)?\s*([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+([0-9\-\*\/,]+)\s+(([0-9\-\*\/]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|,)+)\s+(([0-9\-\*\/]+|sun|mon|tue|wed|thu|fri|sat|,)+)\s+(.*)/i) {
208                         # A normal m h d m wd time
209                         push(@rv, { 'file' => "$config{'cron_dir'}/$f",
210                                     'line' => $lnum,
211                                     'type' => 0,
212                                     'active' => !$1, 'nolog' => $2,
213                                     'mins' => $3, 'hours' => $4,
214                                     'days' => $5, 'months' => $6,
215                                     'weekdays' => $8, 'user' => $f,
216                                     'command' => $10,
217                                     'index' => scalar(@rv) });
218                         $rv[$#rv]->{'file'} =~ s/\s+\|$//;
219                         &fix_names($rv[$#rv]);
220                         }
221                 elsif (/^(#+)?\s*@([a-z]+)\s+(.*)/i) {
222                         # An @ time
223                         push(@rv, { 'file' => "$config{'cron_dir'}/$f",
224                                     'line' => $lnum,
225                                     'type' => 0,
226                                     'active' => !$1,
227                                     'special' => $2,
228                                     'user' => $f,
229                                     'command' => $3,
230                                     'index' => scalar(@rv) });
231                         }
232                 elsif (/^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*'([^']*)'/ ||
233                        /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*"([^']*)"/ ||
234                        /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*(\S+)/) {
235                         # An environment variable
236                         push(@rv, { 'file' => "$config{'cron_dir'}/$f",
237                                     'line' => $lnum,
238                                     'active' => !$1,
239                                     'name' => $2,
240                                     'value' => $3,
241                                     'user' => $f,
242                                     'index' => scalar(@rv) });
243                         }
244                 $lnum++;
245                 }
246         close(TAB);
247         }
248 closedir(DIR);
249 @cron_jobs_cache = @rv;
250 return @cron_jobs_cache;
251 }
252
253 =head2 cron_job_line(&job)
254
255 Internal function to generate a crontab format line for a cron job.
256
257 =cut
258 sub cron_job_line
259 {
260 local @c;
261 push(@c, "#") if (!$_[0]->{'active'});
262 if ($_[0]->{'name'}) {
263         push(@c, $_[0]->{'name'});
264         push(@c, "=");
265         push(@c, $_[0]->{'value'} =~ /'/ ? "\"$_[0]->{'value'}\"" :
266                  $_[0]->{'value'} =~ /"/ ? "'$_[0]->{'value'}'" :
267                  $_[0]->{'value'} !~ /^\S+$/ ? "\"$_[0]->{'value'}\""
268                                           : $_[0]->{'value'});
269         }
270 else {
271         if ($_[0]->{'special'}) {
272                 push(@c, ($_[0]->{'nolog'} ? '-' : '').'@'.$_[0]->{'special'});
273                 }
274         else {
275                 push(@c, ($_[0]->{'nolog'} ? '-' : '').$_[0]->{'mins'},
276                          $_[0]->{'hours'}, $_[0]->{'days'},
277                          $_[0]->{'months'}, $_[0]->{'weekdays'});
278                 }
279         push(@c, $_[0]->{'user'}) if ($_[0]->{'type'} != 0 &&
280                                       $_[0]->{'type'} != 3);
281         push(@c, $_[0]->{'command'});
282         }
283 return join(" ", @c);
284 }
285
286 =head2 copy_cron_temp(&job)
287
288 Copies a job's user's current cron configuration to the temp file. For internal
289 use only.
290
291 =cut
292 sub copy_cron_temp
293 {
294 local $fcron = ($config{'cron_dir'} =~ /\/fcron$/);
295 unlink($cron_temp_file);
296 if ($single_user) {
297         &execute_command($config{'cron_user_get_command'},
298                          undef, $cron_temp_file, undef);
299         }
300 elsif ($fcron) {
301         &execute_command(&user_sub($config{'cron_get_command'},$_[0]->{'user'}),
302                          undef, $cron_temp_file, undef);
303         }
304 else {
305         system("cp ".&translate_filename("$config{'cron_dir'}/$_[0]->{'user'}").
306                " $cron_temp_file 2>/dev/null");
307         }
308 }
309
310 =head2 create_cron_job(&job)
311
312 Add a Cron job to a user's file. The job parameter must be a hash reference
313 in the same format as returned by list_cron_jobs.
314
315 =cut
316 sub create_cron_job
317 {
318 &check_cron_config_or_error();
319 &list_cron_jobs();      # init cache
320 if ($config{'add_file'}) {
321         # Add to a specific file, typically something like /etc/cron.d/webmin
322         $_[0]->{'type'} = 1;
323         local $lref = &read_file_lines($config{'add_file'});
324         push(@$lref, &cron_job_line($_[0]));
325         &flush_file_lines($config{'add_file'});
326         }
327 elsif ($config{'single_file'} && !$config{'cron_dir'}) {
328         # Add to the single file
329         $_[0]->{'type'} = 3;
330         local $lref = &read_file_lines($config{'single_file'});
331         push(@$lref, &cron_job_line($_[0]));
332         &flush_file_lines($config{'single_file'});
333         }
334 else {
335         # Add to the specified user's crontab
336         &copy_cron_temp($_[0]);
337         local $lref = &read_file_lines($cron_temp_file);
338         $_[0]->{'line'} = scalar(@$lref);
339         push(@$lref, &cron_job_line($_[0]));
340         &flush_file_lines();
341         &set_ownership_permissions($_[0]->{'user'}, undef, undef,
342                                    $cron_temp_file);
343         &copy_crontab($_[0]->{'user'});
344         $_[0]->{'file'} = "$config{'cron_dir'}/$_[0]->{'user'}";
345         $_[0]->{'index'} = scalar(@cron_jobs_cache);
346         push(@cron_jobs_cache, $_[0]);
347         }
348 }
349
350 =head2 insert_cron_job(&job)
351
352 Add a Cron job at the top of the user's file. The job parameter must be a hash
353 reference in the same format as returned by list_cron_jobs.
354
355 =cut
356 sub insert_cron_job
357 {
358 &check_cron_config_or_error();
359 &list_cron_jobs();      # init cache
360 if ($config{'single_file'} && !$config{'cron_dir'}) {
361         # Insert into single file
362         $_[0]->{'type'} = 3;
363         local $lref = &read_file_lines($config{'single_file'});
364         splice(@$lref, 0, 0, &cron_job_line($_[0]));
365         &flush_file_lines($config{'single_file'});
366         }
367 else {
368         # Insert into the user's crontab
369         &copy_cron_temp($_[0]);
370         local $lref = &read_file_lines($cron_temp_file);
371         $_[0]->{'line'} = 0;
372         splice(@$lref, 0, 0, &cron_job_line($_[0]));
373         &flush_file_lines();
374         system("chown $_[0]->{'user'} $cron_temp_file");
375         &copy_crontab($_[0]->{'user'});
376         $_[0]->{'file'} = "$config{'cron_dir'}/$_[0]->{'user'}";
377         $_[0]->{'index'} = scalar(@cron_jobs_cache);
378         &renumber($_[0]->{'file'}, $_[0]->{'line'}, 1);
379         push(@cron_jobs_cache, $_[0]);
380         }
381 }
382
383 =head2 renumber(file, line, offset)
384
385 All jobs in this file whose line is at or after the given one will be
386 incremented by the offset. For internal use.
387
388 =cut
389 sub renumber
390 {
391 local $j;
392 foreach $j (@cron_jobs_cache) {
393         if ($j->{'line'} >= $_[1] &&
394             $j->{'file'} eq $_[0]) {
395                 $j->{'line'} += $_[2];
396                 }
397         }
398 }
399
400 =head2 renumber_index(index, offset)
401
402 Internal function to change the index of all cron jobs in the cache after
403 some index by a given offset. For internal use.
404
405 =cut
406 sub renumber_index
407 {
408 local $j;
409 foreach $j (@cron_jobs_cache) {
410         if ($j->{'index'} >= $_[0]) {
411                 $j->{'index'} += $_[1];
412                 }
413         }
414 }
415
416 =head2 change_cron_job(&job)
417
418 Updates the given cron job, which must be a hash ref returned by list_cron_jobs
419 and modified with a new active flag, command or schedule.
420
421 =cut
422 sub change_cron_job
423 {
424 if ($_[0]->{'type'} == 0) {
425         &copy_cron_temp($_[0]);
426         &replace_file_line($cron_temp_file, $_[0]->{'line'},
427                            &cron_job_line($_[0])."\n");
428         &copy_crontab($_[0]->{'user'});
429         }
430 else {
431         &replace_file_line($_[0]->{'file'}, $_[0]->{'line'},
432                            &cron_job_line($_[0])."\n");
433         }
434 }
435
436 =head2 delete_cron_job(&job)
437
438 Removes the cron job defined by the given hash ref, as returned by
439 list_cron_jobs.
440
441 =cut
442 sub delete_cron_job
443 {
444 if ($_[0]->{'type'} == 0) {
445         &copy_cron_temp($_[0]);
446         &replace_file_line($cron_temp_file, $_[0]->{'line'});
447         &copy_crontab($_[0]->{'user'});
448         }
449 else {
450         &replace_file_line($_[0]->{'file'}, $_[0]->{'line'});
451         }
452 @cron_jobs_cache = grep { $_ ne $_[0] } @cron_jobs_cache;
453 &renumber($_[0]->{'file'}, $_[0]->{'line'}, -1);
454 &renumber_index($_[0]->{'index'}, -1);
455 }
456
457 =head2 read_crontab(user)
458
459 Return an array containing the lines of the cron table for some user. For
460 internal use mainly.
461
462 =cut
463 sub read_crontab
464 {
465 local(@tab);
466 &open_readfile(TAB, "$config{cron_dir}/$_[0]");
467 @tab = <TAB>;
468 close(TAB);
469 if (@tab >= 3 && $tab[0] =~ /DO NOT EDIT/ &&
470     $tab[1] =~ /^\s*#/ && $tab[2] =~ /^\s*#/) {
471         @tab = @tab[3..$#tab];
472         }
473 return @tab;
474 }
475
476 =head2 copy_crontab(user)
477
478 Copy the cron temp file to that for this user. For internal use only.
479
480 =cut
481 sub copy_crontab
482 {
483 if (&is_readonly_mode()) {
484         # Do nothing
485         return undef;
486         }
487 local($pwd);
488 if (&read_file_contents($cron_temp_file) =~ /\S/) {
489         local $temp = &transname();
490         local $rv;
491         if ($config{'cron_edit_command'}) {
492                 # fake being an editor
493                 # XXX does not work in translated command mode!
494                 local $notemp = &transname();
495                 &open_tempfile(NO, ">$notemp");
496                 &print_tempfile(NO, "No\n");
497                 &print_tempfile(NO, "N\n");
498                 &print_tempfile(NO, "no\n");
499                 &close_tempfile(NO);
500                 $ENV{"VISUAL"} = $ENV{"EDITOR"} =
501                         "$module_root_directory/cron_editor.pl";
502                 $ENV{"CRON_EDITOR_COPY"} = $cron_temp_file;
503                 system("chown $_[0] $cron_temp_file");
504                 local $oldpwd = &get_current_dir();
505                 chdir("/");
506                 if ($single_user) {
507                         $rv = system($config{'cron_user_edit_command'}.
508                                      " >$temp 2>&1 <$notemp");
509                         }
510                 else {
511                         $rv = system(
512                                 &user_sub($config{'cron_edit_command'},$_[0]).
513                                 " >$temp 2>&1 <$notemp");
514                         }
515                 unlink($notemp);
516                 chdir($oldpwd);
517                 }
518         else {
519                 # use the cron copy command
520                 if ($single_user) {
521                         $rv = &execute_command(
522                                 $config{'cron_user_copy_command'},
523                                 $cron_temp_file, $temp, $temp);
524                         }
525                 else {
526                         $rv = &execute_command(
527                                 &user_sub($config{'cron_copy_command'}, $_[0]),
528                                 $cron_temp_file, $temp, $temp);
529                         }
530                 }
531         local $out = &read_file_contents($temp);
532         unlink($temp);
533         if ($rv || $out =~ /error/i) {
534                 local $cronin = &read_file_contents($cron_temp_file);
535                 &error(&text('ecopy', "<pre>$out</pre>", "<pre>$cronin</pre>"));
536                 }
537         }
538 else {
539         # No more cron jobs left, so just delete
540         if ($single_user) {
541                 &execute_command($config{'cron_user_delete_command'});
542                 }
543         else {
544                 &execute_command(&user_sub(
545                         $config{'cron_delete_command'}, $_[0]));
546                 }
547         }
548 unlink($cron_temp_file);
549 }
550
551
552 =head2 parse_job(job-line)
553
554 Parse a crontab line into an array containing:
555 active, mins, hrs, days, mons, weekdays, command
556
557 =cut
558 sub parse_job
559 {
560 local($job, $active) = ($_[0], 1);
561 if ($job =~ /^#+\s*(.*)$/) {
562         $active = 0;
563         $job = $1;
564         }
565 $job =~ /^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/;
566 return ($active, $1, $2, $3, $4, $5, $6);
567 }
568
569 =head2 user_sub(command, user)
570
571 Replace the string 'USER' in the command with the user name. For internal
572 use only.
573
574 =cut
575 sub user_sub
576 {
577 local($tmp);
578 $tmp = $_[0];
579 $tmp =~ s/USER/$_[1]/g;
580 return $tmp;
581 }
582
583
584 =head2 list_allowed
585
586 Returns a list of all Unix usernames who are allowed to use Cron.
587
588 =cut
589 sub list_allowed
590 {
591 local(@rv, $_);
592 &open_readfile(ALLOW, $config{cron_allow_file});
593 while(<ALLOW>) {
594         next if (/^\s*#/);
595         chop; push(@rv, $_) if (/\S/);
596         }
597 close(ALLOW);
598 return @rv;
599 }
600
601
602 =head2 list_denied
603
604 Return a list of all Unix usernames who are not allowed to use Cron.
605
606 =cut
607 sub list_denied
608 {
609 local(@rv, $_);
610 &open_readfile(DENY, $config{cron_deny_file});
611 while(<DENY>) {
612         next if (/^\s*#/);
613         chop; push(@rv, $_) if (/\S/);
614         }
615 close(DENY);
616 return @rv;
617 }
618
619
620 =head2 save_allowed(user, user, ...)
621
622 Save the list of allowed Unix usernames.
623
624 =cut
625 sub save_allowed
626 {
627 local($_);
628 &open_tempfile(ALLOW, ">$config{cron_allow_file}");
629 foreach (@_) {
630         &print_tempfile(ALLOW, $_,"\n");
631         }
632 &close_tempfile(ALLOW);
633 chmod(0444, $config{cron_allow_file});
634 }
635
636
637 =head2 save_denied(user, user, ...)
638
639 Save the list of denied Unix usernames.
640
641 =cut
642 sub save_denied
643 {
644 local($_);
645 &open_tempfile(DENY, "> $config{cron_deny_file}");
646 foreach (@_) {
647         &print_tempfile(DENY, $_,"\n");
648         }
649 &close_tempfile(DENY);
650 chmod(0444, $config{cron_deny_file});
651 }
652
653 =head2 read_envs(user)
654
655 Returns an array of "name value" strings containing the environment settings
656 from the crontab for some user
657
658 =cut
659 sub read_envs
660 {
661 local(@tab, @rv, $_);
662 @tab = &read_crontab($_[0]);
663 foreach (@tab) {
664         chop; s/#.*$//g;
665         if (/^\s*(\S+)\s*=\s*(.*)$/) { push(@rv, "$1 $2"); }
666         }
667 return @rv;
668 }
669
670 =head2 save_envs(user, [name, value]*)
671
672 Updates the cron file for some user with the given list of environment
673 variables. All others in the file are removed.
674
675 =cut
676 sub save_envs
677 {
678 local($i, @tab, $line);
679 @tab = &read_crontab($_[0]);
680 open(TAB, ">$cron_temp_file");
681 for($i=1; $i<@_; $i+=2) {
682         print TAB "$_[$i]=$_[$i+1]\n";
683         }
684 foreach (@tab) {
685         chop($line = $_); $line =~ s/#.*$//g;
686         if ($line !~ /^\s*(\S+)\s*=\s*(.*)$/) { print TAB $_; }
687         }
688 close(TAB);
689 &copy_crontab($_[0]);
690 }
691
692 =head2 expand_run_parts(directory)
693
694 Internal function to convert a directory like /etc/cron.hourly into a list
695 of scripts in that directory.
696
697 =cut
698 sub expand_run_parts
699 {
700 local $dir = $_[0];
701 $dir = "$config{'run_parts_dir'}/$dir"
702         if ($config{'run_parts_dir'} && $dir !~ /^\//);
703 opendir(DIR, &translate_filename($dir));
704 local @rv = readdir(DIR);
705 closedir(DIR);
706 @rv = grep { !/^\./ } @rv;
707 @rv = map { $dir."/".$_ } @rv;
708 return @rv;
709 }
710
711 =head2 is_run_parts(command)
712
713 Returns the dir if some cron job runs a list of commands in some directory, 
714 like /etc/cron.hourly. Returns undef otherwise.
715
716 =cut
717 sub is_run_parts
718 {
719 local $rp = $config{'run_parts'};
720 return $rp && $_[0] =~ /$rp(.*)\s+(\-\-\S+\s+)*([a-z0-9\.\-\/_]+)(\s*\))?$/i ? $3 : undef;
721 }
722
723 =head2 can_edit_user(&access, user)
724
725 Returns 1 if the Webmin user whose permissions are defined by the access hash
726 ref can manage cron jobs for a given Unix user.
727
728 =cut
729 sub can_edit_user
730 {
731 local %umap;
732 map { $umap{$_}++; } split(/\s+/, $_[0]->{'users'})
733         if ($_[0]->{'mode'} == 1 || $_[0]->{'mode'} == 2);
734 if ($_[0]->{'mode'} == 1 && !$umap{$_[1]} ||
735     $_[0]->{'mode'} == 2 && $umap{$_[1]}) { return 0; }
736 elsif ($_[0]->{'mode'} == 3) {
737         return $remote_user eq $_[1];
738         }
739 elsif ($_[0]->{'mode'} == 4) {
740         local @u = getpwnam($_[1]);
741         return (!$_[0]->{'uidmin'} || $u[2] >= $_[0]->{'uidmin'}) &&
742                (!$_[0]->{'uidmax'} || $u[2] <= $_[0]->{'uidmax'});
743         }
744 elsif ($_[0]->{'mode'} == 5) {
745         local @u = getpwnam($_[1]);
746         return $u[3] == $_[0]->{'users'};
747         }
748 else {
749         return 1;
750         }
751 }
752
753 =head2 show_times_input(&job, [nospecial])
754
755 Print HTML for inputs for selecting the schedule for a cron job, defined
756 by the first parameter which must be a hash ref returned by list_cron_jobs.
757 This must be used inside a <table>, as the HTML starts and ends with <tr>
758 tags.
759
760 =cut
761 sub show_times_input
762 {
763 return &theme_show_times_input(@_) if (defined(&theme_show_times_input));
764 local $job = $_[0];
765 if ($config{'vixie_cron'} && (!$_[1] || $_[0]->{'special'})) {
766         # Allow selection of special @ times
767         print "<tr $cb> <td colspan=6>\n";
768         printf "<input type=radio name=special_def value=1 %s> %s\n",
769                 $job->{'special'} ? "checked" : "", $text{'edit_special1'};
770         print "<select name=special>\n";
771         local $s;
772         local $sp = $job->{'special'} eq 'midnight' ? 'daily' :
773             $job->{'special'} eq 'annually' ? 'yearly' : $job->{'special'};
774         foreach $s ('hourly', 'daily', 'weekly', 'monthly', 'yearly', 'reboot'){
775                 printf "<option value=%s %s>%s\n",
776                     $s, $sp eq $s ? "selected" : "", $text{'edit_special_'.$s};
777                 }
778         print "</select>\n";
779         printf "<input type=radio name=special_def value=0 %s> %s\n",
780                 $job->{'special'} ? "" : "checked", $text{'edit_special0'};
781         print "</td></tr>\n";
782         }
783
784 # Javascript to disable and enable fields
785 print <<EOF;
786 <script>
787 function enable_cron_fields(name, form, ena)
788 {
789 var els = form.elements[name];
790 els.disabled = !ena;
791 for(i=0; i<els.length; i++) {
792   els[i].disabled = !ena;
793   }
794 }
795 </script>
796 EOF
797
798 print "<tr $tb>\n";
799 print "<td><b>$text{'edit_mins'}</b></td> <td><b>$text{'edit_hours'}</b></td> ",
800       "<td><b>$text{'edit_days'}</b></td> <td><b>$text{'edit_months'}</b></td>",
801       "<td><b>$text{'edit_weekdays'}</b></td> </tr> <tr $cb>\n";
802
803 local @mins = (0..59);
804 local @hours = (0..23);
805 local @days = (1..31);
806 local @months = map { $text{"month_$_"}."=".$_ } (1 .. 12);
807 local @weekdays = map { $text{"day_$_"}."=".$_ } (0 .. 6);
808
809 foreach $arr ("mins", "hours", "days", "months", "weekdays") {
810         # Find out which ones are being used
811         local %inuse;
812         local $min = ($arr =~ /days|months/ ? 1 : 0);
813         local $max = $min+scalar(@$arr)-1;
814         foreach $w (split(/,/ , $job->{$arr})) {
815                 if ($w eq "*") {
816                         # all values
817                         for($j=$min; $j<=$max; $j++) { $inuse{$j}++; }
818                         }
819                 elsif ($w =~ /^\*\/(\d+)$/) {
820                         # only every Nth
821                         for($j=$min; $j<=$max; $j+=$1) { $inuse{$j}++; }
822                         }
823                 elsif ($w =~ /^(\d+)-(\d+)\/(\d+)$/) {
824                         # only every Nth of some range
825                         for($j=$1; $j<=$2; $j+=$3) { $inuse{int($j)}++; }
826                         }
827                 elsif ($w =~ /^(\d+)-(\d+)$/) {
828                         # all of some range
829                         for($j=$1; $j<=$2; $j++) { $inuse{int($j)}++; }
830                         }
831                 else {
832                         # One value
833                         $inuse{int($w)}++;
834                         }
835                 }
836         if ($job->{$arr} eq "*") { undef(%inuse); }
837
838         # Output selection list
839         print "<td valign=top>\n";
840         printf "<input type=radio name=all_$arr value=1 %s %s %s> %s<br>\n",
841                 $arr eq "mins" && $hourly_only ? "disabled" : "",
842                 $job->{$arr} eq "*" ||  $job->{$arr} eq "" ? "checked" : "",
843                 "onClick='enable_cron_fields(\"$arr\", form, 0)'",
844                 $text{'edit_all'};
845         printf "<input type=radio name=all_$arr value=0 %s %s> %s<br>\n",
846                 $job->{$arr} eq "*" || $job->{$arr} eq "" ? "" : "checked",
847                 "onClick='enable_cron_fields(\"$arr\", form, 1)'",
848                 $text{'edit_selected'};
849         print "<table> <tr>\n";
850         for($j=0; $j<@$arr; $j+=($arr eq "mins" && $hourly_only ? 60 : 12)) {
851                 $jj = $j+($arr eq "mins" && $hourly_only ? 59 : 11);
852                 if ($jj >= @$arr) { $jj = @$arr - 1; }
853                 @sec = @$arr[$j .. $jj];
854                 printf "<td valign=top><select %s size=%d name=$arr %s>\n",
855                         $arr eq "mins" && $hourly_only ? "" : "multiple",
856                         @sec > 12 ? ($arr eq "mins" && $hourly_only ? 1 : 12)
857                                   : scalar(@sec),
858                         $job->{$arr} eq "*" ||  $job->{$arr} eq "" ?
859                                 "disabled" : "";
860                 foreach $v (@sec) {
861                         if ($v =~ /^(.*)=(.*)$/) { $disp = $1; $code = $2; }
862                         else { $disp = $code = $v; }
863                         printf "<option value=\"$code\" %s>$disp\n",
864                                 $inuse{$code} ? "selected" : "";
865                         }
866                 print "</select></td>\n";
867                 }
868         print "</tr></table></td>\n";
869         }
870 print "</tr> <tr $cb> <td colspan=5>$text{'edit_ctrl'}</td> </tr>\n";
871 }
872
873 =head2 parse_times_input(&job, &in)
874
875 Parses inputs from the form generated by show_times_input, and updates a cron
876 job hash ref. The in parameter must be a hash ref as generated by the 
877 ReadParse function.
878
879 =cut
880 sub parse_times_input
881 {
882 local $job = $_[0];
883 local %in = %{$_[1]};
884 local @pers = ("mins", "hours", "days", "months", "weekdays");
885 local $arr;
886 if ($in{'special_def'}) {
887         # Job time is a special period
888         foreach $arr (@pers) {
889                 delete($job->{$arr});
890                 }
891         $job->{'special'} = $in{'special'};
892         }
893 else {
894         # User selection of times
895         foreach $arr (@pers) {
896                 if ($in{"all_$arr"}) {
897                         # All mins/hrs/etc.. chosen
898                         $job->{$arr} = "*";
899                         }
900                 elsif (defined($in{$arr})) {
901                         # Need to work out and simplify ranges selected
902                         local (@range, @newrange, $i);
903                         @range = split(/\0/, $in{$arr});
904                         @range = sort { $a <=> $b } @range;
905                         local $start = -1;
906                         for($i=0; $i<@range; $i++) {
907                                 if ($i && $range[$i]-1 == $range[$i-1]) {
908                                         # ok.. looks like a range
909                                         if ($start < 0) { $start = $i-1; }
910                                         }
911                                 elsif ($start < 0) {
912                                         # Not in a range at all
913                                         push(@newrange, $range[$i]);
914                                         }
915                                 else {
916                                         # End of the range.. add it
917                                         $newrange[@newrange - 1] =
918                                                 "$range[$start]-".$range[$i-1];
919                                         push(@newrange, $range[$i]);
920                                         $start = -1;
921                                         }
922                                 }
923                         if ($start >= 0) {
924                                 # Reached the end while in a range
925                                 $newrange[@newrange - 1] =
926                                         "$range[$start]-".$range[$i-1];
927                                 }
928                         $job->{$arr} = join(',' , @newrange);
929                         }
930                 else {
931                         &error(&text('save_enone', $text{"edit_$arr"}));
932                         }
933                 }
934         delete($job->{'special'});
935         }
936 }
937
938 =head2 show_range_input(&job)
939
940 Given a cron job, prints fields for selecting it's run date range.
941
942 =cut
943 sub show_range_input
944 {
945 local ($job) = @_;
946 local $has_start = $job->{'start'};
947 local $rng;
948 $rng = &text('range_start', &ui_date_input(
949         $job->{'start'}->[0], $job->{'start'}->[1], $job->{'start'}->[2],
950         "range_start_day", "range_start_month", "range_start_year"))."\n".
951       &date_chooser_button(
952         "range_start_day", "range_start_month", "range_start_year")."\n".
953       &text('range_end', &ui_date_input(
954         $job->{'end'}->[0], $job->{'end'}->[1], $job->{'end'}->[2],
955         "range_end_day", "range_end_month", "range_end_year"))."\n".
956       &date_chooser_button(
957         "range_end_day", "range_end_month", "range_end_year")."\n";
958 print &ui_oneradio("range_def", 1, $text{'range_all'}, !$has_start),
959       "<br>\n";
960 print &ui_oneradio("range_def", 0, $rng, $has_start),"\n";
961 }
962
963 =head2 parse_range_input(&job, &in)
964
965 Updates the job object with the specified date range. May call &error
966 for invalid inputs.
967
968 =cut
969 sub parse_range_input
970 {
971 local ($job, $in) = @_;
972 if ($in->{'range_def'}) {
973         # No range used
974         delete($job->{'start'});
975         delete($job->{'end'});
976         }
977 else {
978         # Validate and store range
979         foreach my $r ("start", "end") {
980                 eval { timelocal(0, 0, 0, $in->{'range_'.$r.'_day'},
981                                           $in->{'range_'.$r.'_month'}-1,
982                                           $in->{'range_'.$r.'_year'}-1900) };
983                 if ($@) {
984                         &error($text{'range_e'.$r}." ".$@);
985                         }
986                 $job->{$r} = [ $in->{'range_'.$r.'_day'},
987                                $in->{'range_'.$r.'_month'},
988                                $in->{'range_'.$r.'_year'} ];
989                 }
990         }
991 }
992
993 @cron_month = ( 'jan', 'feb', 'mar', 'apr', 'may', 'jun',
994                 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' );
995 @cron_weekday = ( 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' );
996
997 =head2 fix_names(&cron)
998
999 Convert day and month names to numbers. For internal use when parsing
1000 the crontab file.
1001
1002 =cut
1003 sub fix_names
1004 {
1005 local ($m, $w);
1006
1007 local @mts = split(/,/, $_[0]->{'months'});
1008 foreach $m (@mts) {
1009         local $mi = &indexof(lc($m), @cron_month);
1010         $m = $mi+1 if ($mi >= 0);
1011         }
1012 $_[0]->{'months'} = join(",", @mts);
1013
1014 local @wds = split(/,/, $_[0]->{'weekdays'});
1015 foreach $w (@wds) {
1016         local $di = &indexof(lc($w), @cron_weekday);
1017         $w = $di if ($di >= 0);
1018         $w = 0 if ($w == 7);
1019         }
1020 $_[0]->{'weekdays'} = join(",", @wds);
1021 }
1022
1023 =head2 create_wrapper(wrapper-path, module, script)
1024
1025 Creates a wrapper script which calls a script in some module's directory
1026 with the proper webmin environment variables set. This should always be used
1027 when setting up a cron job, instead of attempting to run a command in the
1028 module directory directly.
1029
1030 The parameters are :
1031
1032 =item wrapper-path - Full path to the wrapper to create, like /etc/webmin/yourmodule/foo.pl
1033
1034 =item module - Module containing the real script to call.
1035
1036 =item script - Program within that module for the wrapper to run.
1037
1038 =cut
1039 sub create_wrapper
1040 {
1041 local $perl_path = &get_perl_path();
1042 &open_tempfile(CMD, ">$_[0]");
1043 &print_tempfile(CMD, <<EOF
1044 #!$perl_path
1045 open(CONF, "$config_directory/miniserv.conf");
1046 while(<CONF>) {
1047         \$root = \$1 if (/^root=(.*)/);
1048         }
1049 close(CONF);
1050 \$ENV{'PERLLIB'} = "\$root";
1051 \$ENV{'WEBMIN_CONFIG'} = "$ENV{'WEBMIN_CONFIG'}";
1052 \$ENV{'WEBMIN_VAR'} = "$ENV{'WEBMIN_VAR'}";
1053 EOF
1054         );
1055 if ($gconfig{'os_type'} eq 'windows') {
1056         # On windows, we need to chdir to the drive first, and use system
1057         &print_tempfile(CMD, "if (\$root =~ /^([a-z]:)/i) {\n");
1058         &print_tempfile(CMD, "       chdir(\"\$1\");\n");
1059         &print_tempfile(CMD, "       }\n");
1060         &print_tempfile(CMD, "chdir(\"\$root/$_[1]\");\n");
1061         &print_tempfile(CMD, "exit(system(\"\$root/$_[1]/$_[2]\", \@ARGV));\n");
1062         }
1063 else {
1064         # Can use exec on Unix systems
1065         if ($_[1]) {
1066                 &print_tempfile(CMD, "chdir(\"\$root/$_[1]\");\n");
1067                 &print_tempfile(CMD, "exec(\"\$root/$_[1]/$_[2]\", \@ARGV) || die \"Failed to run \$root/$_[1]/$_[2] : \$!\";\n");
1068                 }
1069         else {
1070                 &print_tempfile(CMD, "chdir(\"\$root\");\n");
1071                 &print_tempfile(CMD, "exec(\"\$root/$_[2]\", \@ARGV) || die \"Failed to run \$root/$_[2] : \$!\";\n");
1072                 }
1073         }
1074 &close_tempfile(CMD);
1075 chmod(0755, $_[0]);
1076 }
1077
1078 =head2 cron_file(&job)
1079
1080 Returns the file that a cron job is in, or will be in when it is created
1081 based on the username.
1082
1083 =cut
1084 sub cron_file
1085 {
1086 return $_[0]->{'file'} || $config{'add_file'} ||
1087        "$config{'cron_dir'}/$_[0]->{'user'}";
1088 }
1089
1090 =head2 when_text(&job, [upper-case-first])
1091
1092 Returns a human-readable text string describing when a cron job is run.
1093
1094 =cut
1095 sub when_text
1096 {
1097 local $pfx = $_[1] ? "uc" : "";
1098 if ($_[0]->{'special'}) {
1099         $pfx = $_[1] ? "" : "lc";
1100         return $text{$pfx.'edit_special_'.$_[0]->{'special'}};
1101         }
1102 elsif ($_[0]->{'mins'} eq '*' && $_[0]->{'hours'} eq '*' && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') {
1103         return $text{$pfx.'when_min'};
1104         }
1105 elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} eq '*' && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') {
1106         return &text($pfx.'when_hour', $_[0]->{'mins'});
1107         }
1108 elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') {
1109         return &text($pfx.'when_day', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'});
1110         }
1111 elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} =~ /^\d+$/ && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') {
1112         return &text($pfx.'when_month', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'}, $_[0]->{'days'});
1113         }
1114 elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} =~ /^\d+$/) {
1115         return &text($pfx.'when_weekday', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'}, $text{"day_".$_[0]->{'weekdays'}});
1116         }
1117 else {
1118         return &text($pfx.'when_cron', join(" ", $_[0]->{'mins'}, $_[0]->{'hours'}, $_[0]->{'days'}, $_[0]->{'months'}, $_[0]->{'weekdays'}));
1119         }
1120 }
1121
1122 =head2 can_use_cron(user)
1123
1124 Returns 1 if some user is allowed to use cron, based on cron.allow and
1125 cron.deny files.
1126
1127 =cut
1128 sub can_use_cron
1129 {
1130 local $err;
1131 if (-r $config{cron_allow_file}) {
1132         local @allowed = &list_allowed();
1133         if (&indexof($_[0], @allowed) < 0 &&
1134             &indexof("all", @allowed) < 0) { $err = 1; }
1135         }
1136 elsif (-r $config{cron_deny_file}) {
1137         local @denied = &list_denied();
1138         if (&indexof($_[0], @denied) >= 0 ||
1139             &indexof("all", @denied) >= 0) { $err = 1; }
1140         }
1141 elsif ($config{cron_deny_all} == 0) { $err = 1; }
1142 elsif ($config{cron_deny_all} == 1) {
1143         if ($in{user} ne "root") { $err = 1; }
1144         }
1145 return !$err;
1146 }
1147
1148 =head2 swap_cron_jobs(&job1, &job2)
1149
1150 Swaps two Cron jobs, which must be in the same file, identified by their
1151 hash references as returned by list_cron_jobs.
1152
1153 =cut
1154 sub swap_cron_jobs
1155 {
1156 if ($_[0]->{'type'} == 0) {
1157         &copy_cron_temp($_[0]);
1158         local $lref = &read_file_lines($cron_temp_file);
1159         ($lref->[$_[0]->{'line'}], $lref->[$_[1]->{'line'}]) =
1160                 ($lref->[$_[1]->{'line'}], $lref->[$_[0]->{'line'}]);
1161         &flush_file_lines();
1162         &copy_crontab($_[0]->{'user'});
1163         }
1164 else {
1165         local $lref = &read_file_lines($_[0]->{'file'});
1166         ($lref->[$_[0]->{'line'}], $lref->[$_[1]->{'line'}]) =
1167                 ($lref->[$_[1]->{'line'}], $lref->[$_[0]->{'line'}]);
1168         &flush_file_lines();
1169         }
1170 }
1171
1172 =head2 find_cron_process(&job, [&procs])
1173
1174 Finds the running process that was launched from a cron job. The parameters are:
1175
1176 =item job - A cron job hash reference
1177
1178 =item procs - An optional array reference of running process hash refs
1179
1180 =cut
1181 sub find_cron_process
1182 {
1183 local @procs;
1184 if ($_[1]) {
1185         @procs = @{$_[1]};
1186         }
1187 else {
1188         &foreign_require("proc", "proc-lib.pl");
1189         @procs = &proc::list_processes();
1190         }
1191 local $rpd = &is_run_parts($_[0]->{'command'});
1192 local @exp = $rpd ? &expand_run_parts($rpd) : ();
1193 local $cmd = $exp[0] || $_[0]->{'command'};
1194 $cmd =~ s/^\s*\[.*\]\s+\&\&\s+//;
1195 $cmd =~ s/^\s*\[.*\]\s+\|\|\s+//;
1196 while($cmd =~ s/(\d*)(<|>)((\/\S+)|&\d+)\s*$//) { }
1197 $cmd =~ s/^\((.*)\)\s*$/$1/;
1198 $cmd =~ s/\s+$//;
1199 if ($config{'match_mode'} == 1) {
1200         $cmd =~ s/\s.*$//;
1201         }
1202 ($proc) = grep { $_->{'args'} =~ /\Q$cmd\E/ &&
1203                  (!$config{'match_user'} || $_->{'user'} eq $_[0]->{'user'}) }
1204                 @procs;
1205 if (!$proc && $cmd =~ /^$config_directory\/(.*\.pl)(.*)$/) {
1206         # Must be a Webmin wrapper
1207         $cmd = "$root_directory/$1$2";
1208         ($proc) = grep { $_->{'args'} =~ /\Q$cmd\E/ &&
1209                          (!$config{'match_user'} ||
1210                           $_->{'user'} eq $_[0]->{'user'}) }
1211                         @procs;
1212         }
1213 return $proc;
1214 }
1215
1216 =head2 find_cron_job(command, [&jobs], [user])
1217
1218 Returns the cron job object that runs some command (perhaps with redirection)
1219
1220 =cut
1221 sub find_cron_job
1222 {
1223 my ($cmd, $jobs, $user) = @_;
1224 if (!$jobs) {
1225         $jobs = [ &list_cron_jobs() ];
1226         }
1227 $user ||= "root";
1228 my @rv = grep { $_->{'user'} eq $user &&
1229              $_->{'command'} =~ /(^|[ \|\&;\/])\Q$cmd\E($|[ \|\&><;])/ } @$jobs;
1230 return wantarray ? @rv : $rv[0];
1231 }
1232
1233 =head2 extract_input(command)
1234
1235 Given a line formatted like I<command%input>, returns the command and input
1236 parts, taking any escaping into account.
1237
1238 =cut
1239 sub extract_input
1240 {
1241 local ($cmd) = @_;
1242 $cmd =~ s/\\%/\0/g;
1243 local ($cmd, $input) = split(/\%/, $cmd, 2);
1244 $cmd =~ s/\0/%/g;
1245 $input =~ s/\0/%/g;
1246 return ($cmd, $input);
1247 }
1248
1249 =head2 convert_range(&job)
1250
1251 Given a cron job that uses range.pl, work out the date range and update
1252 the job object command. Mainly for internal use.
1253
1254 =cut
1255 sub convert_range
1256 {
1257 local ($job) = @_;
1258 local ($cmd, $input) = &extract_input($job->{'command'});
1259 if ($cmd =~ /^\Q$range_cmd\E\s+(\d+)\-(\d+)\-(\d+)\s+(\d+)\-(\d+)\-(\d+)\s+(.*)$/) {
1260         # Looks like a range command
1261         $job->{'start'} = [ $1, $2, $3 ];
1262         $job->{'end'} = [ $4, $5, $6 ];
1263         $job->{'command'} = $7;
1264         $job->{'command'} =~ s/\\(.)/$1/g;
1265         if ($input) {
1266                 $job->{'command'} .= '%'.$input;
1267                 }
1268         return 1;
1269         }
1270 return 0;
1271 }
1272
1273 =head2 unconvert_range(&job)
1274
1275 Give a cron job with start and end fields, updates the command to wrap it in
1276 range.pl with those dates as parameters.
1277
1278 =cut
1279 sub unconvert_range
1280 {
1281 local ($job) = @_;
1282 if ($job->{'start'}) {
1283         # Need to add range command
1284         local ($cmd, $input) = &extract_input($job->{'command'});
1285         $job->{'command'} = $range_cmd." ".join("-", @{$job->{'start'}})." ".
1286                                            join("-", @{$job->{'end'}})." ".
1287                                            quotemeta($cmd);
1288         if ($input) {
1289                 $job->{'command'} .= '%'.$input;
1290                 }
1291         delete($job->{'start'});
1292         delete($job->{'end'});
1293         &copy_source_dest("$module_root_directory/range.pl", $range_cmd);
1294         &set_ownership_permissions(undef, undef, 0755, $range_cmd);
1295         return 1;
1296         }
1297 return 0;
1298 }
1299
1300 =head2 convert_comment(&job)
1301
1302 Given a cron job with a # comment after the command, sets the comment field
1303
1304 =cut
1305 sub convert_comment
1306 {
1307 local ($job) = @_;
1308 if ($job->{'command'} =~ /^(.*)\s*#([^#]*)$/) {
1309         $job->{'command'} = $1;
1310         $job->{'comment'} = $2;
1311         return 1;
1312         }
1313 return 0;
1314 }
1315
1316 =head2 unconvert_comment(&job)
1317
1318 Adds an comment back to the command in a cron job, based on the comment field
1319 of the given hash reference.
1320
1321 =cut
1322 sub unconvert_comment
1323 {
1324 local ($job) = @_;
1325 if ($job->{'comment'} =~ /\S/) {
1326         $job->{'command'} .= " #".$job->{'comment'};
1327         return 1;
1328         }
1329 return 0;
1330 }
1331
1332 =head2 check_cron_config
1333
1334 Returns an error message if the cron config doesn't look valid, or some needed
1335 command is missing.
1336
1337 =cut
1338 sub check_cron_config
1339 {
1340 # Check for single file and getter command
1341 if ($config{'single_file'} && !-r $config{'single_file'}) {
1342         return &text('index_esingle', "<tt>$config{'single_file'}</tt>");
1343         }
1344 if ($config{'cron_get_command'} =~ /^(\S+)/ && !&has_command("$1")) {
1345         return &text('index_ecmd', "<tt>$1</tt>");
1346         }
1347 # Check for directory
1348 local $fcron = ($config{'cron_dir'} =~ /\/fcron$/);
1349 if (!$single_user && !$config{'single_file'} &&
1350     !$fcron && !-d $config{'cron_dir'}) {
1351         return &text('index_ecrondir', "<tt>$config{'cron_dir'}</tt>");
1352         }
1353 return undef;
1354 }
1355
1356 =head2 check_cron_config_or_error
1357
1358 Calls check_cron_config, and then error if any problems were detected.
1359
1360 =cut
1361 sub check_cron_config_or_error
1362 {
1363 local $err = &check_cron_config();
1364 if ($err) {
1365         &error(&text('index_econfigcheck', $err));
1366         }
1367 }
1368
1369 =head2 cleanup_temp_files
1370
1371 Called from cron to delete old files in the Webmin /tmp directory
1372
1373 =cut
1374 sub cleanup_temp_files
1375 {
1376 # Don't run if disabled
1377 if (!$gconfig{'tempdelete_days'}) {
1378         print STDERR "Temp file clearing is disabled\n";
1379         return;
1380         }
1381 if ($gconfig{'tempdir'} && !$gconfig{'tempdirdelete'}) {
1382         print STDERR "Temp file clearing is not done for the custom directory $gconfig{'tempdir'}\n";
1383         return;
1384         }
1385
1386 local $tempdir = &transname();
1387 $tempdir =~ s/\/([^\/]+)$//;
1388 if (!$tempdir || $tempdir eq "/") {
1389         $tempdir = "/tmp/.webmin";
1390         }
1391
1392 local $cutoff = time() - $gconfig{'tempdelete_days'}*24*60*60;
1393 opendir(DIR, $tempdir);
1394 foreach my $f (readdir(DIR)) {
1395         next if ($f eq "." || $f eq "..");
1396         local @st = lstat("$tempdir/$f");
1397         if ($st[9] < $cutoff) {
1398                 &unlink_file("$tempdir/$f");
1399                 }
1400         }
1401 closedir(DIR);
1402 }
1403
1404 1;
1405