3 Functions for listing, creating and managing Unix users' cron jobs.
5 foreign_require("cron", "cron-lib.pl");
6 @jobs = cron::list_cron_jobs();
7 $job = { 'user' => 'root',
9 'command' => 'ls -l >/dev/null',
10 'special' => 'hourly' };
11 cron::create_cron_job($job);
15 BEGIN { push(@INC, ".."); };
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";
28 $range_cmd = "$module_config_directory/range.pl";
29 $hourly_only = $access{'hourly'} == 0 ? 0 :
30 $access{'hourly'} == 1 ? 1 :
31 $config{'hourly_only'};
33 $temp_delete_cmd = "$module_config_directory/tempdelete.pl";
34 $cron_temp_file = &transname();
39 Returns a lists of structures of all cron jobs, each of which is a hash
40 reference with the following keys :
42 =item user - Unix user the job runs as.
44 =item command - The full command to be run.
46 =item active - Set to 0 if the job is commented out, 1 if active.
48 =item mins - Minute or comma-separated list of minutes the job will run, or * for all.
50 =item hours - Hour or comma-separated list of hours the job will run, or * for all.
52 =item days - Day or comma-separated list of days of the month the job will run, or * for all.
54 =item month - Month number or comma-separated list of months (started from 1) the job will run, or * for all.
56 =item weekday - Day of the week or comma-separated list of days (where 0 is sunday) the job will run, or * for all
61 local (@rv, $lnum, $f);
62 if (scalar(@cron_jobs_cache)) {
63 return @cron_jobs_cache;
66 # read the master crontab file
67 if ($config{'system_crontab'}) {
69 &open_readfile(TAB, $config{'system_crontab'});
71 # Comment line in Fedora 13
72 next if (/^#+\s+\*\s+\*\s+\*\s+\*\s+\*\s+command\s+to\s+be\s+executed/);
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'},
81 'mins' => $3, 'hours' => $4,
82 'days' => $5, 'months' => $6,
83 'weekdays' => $8, 'user' => $10,
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';
92 &fix_names($rv[$#rv]);
94 elsif (/^(#+)?\s*@([a-z]+)\s+(\S+)\s+(.*)/i) {
96 push(@rv, { 'file' => $config{'system_crontab'},
103 'index' => scalar(@rv) });
110 # read package-specific cron files
111 opendir(DIR, &translate_filename($config{'cronfiles_dir'}));
112 while($f = readdir(DIR)) {
113 next if ($f =~ /^\./);
115 &open_readfile(TAB, "$config{'cronfiles_dir'}/$f");
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",
123 'mins' => $3, 'hours' => $4,
124 'days' => $5, 'months' => $6,
125 'weekdays' => $8, 'user' => $10,
127 'index' => scalar(@rv) });
128 &fix_names($rv[$#rv]);
130 elsif (/^(#+)?\s*@([a-z]+)\s+(\S+)\s+(.*)/i) {
131 push(@rv, { 'file' => "$config{'cronfiles_dir'}/$f",
138 'index' => scalar(@rv) });
146 # Read a single user's crontab file
147 if ($config{'single_file'}) {
148 &open_readfile(TAB, $config{'single_file'});
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'},
156 'active' => !$1, 'nolog' => $2,
157 'mins' => $3, 'hours' => $4,
158 'days' => $5, 'months' => $6,
159 'weekdays' => $8, 'user' => "NONE",
161 'index' => scalar(@rv) });
162 &fix_names($rv[$#rv]);
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'},
174 'index' => scalar(@rv) });
182 # read per-user cron files
183 local $fcron = ($config{'cron_dir'} =~ /\/fcron$/);
186 @users = ( $single_user );
189 opendir(DIR, &translate_filename($config{'cron_dir'}));
190 @users = grep { !/^\./ } readdir(DIR);
193 foreach $f (@users) {
194 next if (!(@uinfo = getpwnam($f)));
197 &open_execute_command(TAB, $config{'cron_user_get_command'}, 1);
200 &open_execute_command(TAB,
201 &user_sub($config{'cron_get_command'}, $f), 1);
204 &open_readfile(TAB, "$config{'cron_dir'}/$f");
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",
212 'active' => !$1, 'nolog' => $2,
213 'mins' => $3, 'hours' => $4,
214 'days' => $5, 'months' => $6,
215 'weekdays' => $8, 'user' => $f,
217 'index' => scalar(@rv) });
218 $rv[$#rv]->{'file'} =~ s/\s+\|$//;
219 &fix_names($rv[$#rv]);
221 elsif (/^(#+)?\s*@([a-z]+)\s+(.*)/i) {
223 push(@rv, { 'file' => "$config{'cron_dir'}/$f",
230 'index' => scalar(@rv) });
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",
242 'index' => scalar(@rv) });
249 @cron_jobs_cache = @rv;
250 return @cron_jobs_cache;
253 =head2 cron_job_line(&job)
255 Internal function to generate a crontab format line for a cron job.
261 push(@c, "#") if (!$_[0]->{'active'});
262 if ($_[0]->{'name'}) {
263 push(@c, $_[0]->{'name'});
265 push(@c, $_[0]->{'value'} =~ /'/ ? "\"$_[0]->{'value'}\"" :
266 $_[0]->{'value'} =~ /"/ ? "'$_[0]->{'value'}'" :
267 $_[0]->{'value'} !~ /^\S+$/ ? "\"$_[0]->{'value'}\""
271 if ($_[0]->{'special'}) {
272 push(@c, ($_[0]->{'nolog'} ? '-' : '').'@'.$_[0]->{'special'});
275 push(@c, ($_[0]->{'nolog'} ? '-' : '').$_[0]->{'mins'},
276 $_[0]->{'hours'}, $_[0]->{'days'},
277 $_[0]->{'months'}, $_[0]->{'weekdays'});
279 push(@c, $_[0]->{'user'}) if ($_[0]->{'type'} != 0 &&
280 $_[0]->{'type'} != 3);
281 push(@c, $_[0]->{'command'});
283 return join(" ", @c);
286 =head2 copy_cron_temp(&job)
288 Copies a job's user's current cron configuration to the temp file. For internal
294 local $fcron = ($config{'cron_dir'} =~ /\/fcron$/);
295 unlink($cron_temp_file);
297 &execute_command($config{'cron_user_get_command'},
298 undef, $cron_temp_file, undef);
301 &execute_command(&user_sub($config{'cron_get_command'},$_[0]->{'user'}),
302 undef, $cron_temp_file, undef);
305 system("cp ".&translate_filename("$config{'cron_dir'}/$_[0]->{'user'}").
306 " $cron_temp_file 2>/dev/null");
310 =head2 create_cron_job(&job)
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.
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
323 local $lref = &read_file_lines($config{'add_file'});
324 push(@$lref, &cron_job_line($_[0]));
325 &flush_file_lines($config{'add_file'});
327 elsif ($config{'single_file'} && !$config{'cron_dir'}) {
328 # Add to the single file
330 local $lref = &read_file_lines($config{'single_file'});
331 push(@$lref, &cron_job_line($_[0]));
332 &flush_file_lines($config{'single_file'});
335 # Add to the specified user's crontab
336 ©_cron_temp($_[0]);
337 local $lref = &read_file_lines($cron_temp_file);
338 $_[0]->{'line'} = scalar(@$lref);
339 push(@$lref, &cron_job_line($_[0]));
341 &set_ownership_permissions($_[0]->{'user'}, undef, undef,
343 ©_crontab($_[0]->{'user'});
344 $_[0]->{'file'} = "$config{'cron_dir'}/$_[0]->{'user'}";
345 $_[0]->{'index'} = scalar(@cron_jobs_cache);
346 push(@cron_jobs_cache, $_[0]);
350 =head2 insert_cron_job(&job)
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.
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
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'});
368 # Insert into the user's crontab
369 ©_cron_temp($_[0]);
370 local $lref = &read_file_lines($cron_temp_file);
372 splice(@$lref, 0, 0, &cron_job_line($_[0]));
374 system("chown $_[0]->{'user'} $cron_temp_file");
375 ©_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]);
383 =head2 renumber(file, line, offset)
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.
392 foreach $j (@cron_jobs_cache) {
393 if ($j->{'line'} >= $_[1] &&
394 $j->{'file'} eq $_[0]) {
395 $j->{'line'} += $_[2];
400 =head2 renumber_index(index, offset)
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.
409 foreach $j (@cron_jobs_cache) {
410 if ($j->{'index'} >= $_[0]) {
411 $j->{'index'} += $_[1];
416 =head2 change_cron_job(&job)
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.
424 if ($_[0]->{'type'} == 0) {
425 ©_cron_temp($_[0]);
426 &replace_file_line($cron_temp_file, $_[0]->{'line'},
427 &cron_job_line($_[0])."\n");
428 ©_crontab($_[0]->{'user'});
431 &replace_file_line($_[0]->{'file'}, $_[0]->{'line'},
432 &cron_job_line($_[0])."\n");
436 =head2 delete_cron_job(&job)
438 Removes the cron job defined by the given hash ref, as returned by
444 if ($_[0]->{'type'} == 0) {
445 ©_cron_temp($_[0]);
446 &replace_file_line($cron_temp_file, $_[0]->{'line'});
447 ©_crontab($_[0]->{'user'});
450 &replace_file_line($_[0]->{'file'}, $_[0]->{'line'});
452 @cron_jobs_cache = grep { $_ ne $_[0] } @cron_jobs_cache;
453 &renumber($_[0]->{'file'}, $_[0]->{'line'}, -1);
454 &renumber_index($_[0]->{'index'}, -1);
457 =head2 read_crontab(user)
459 Return an array containing the lines of the cron table for some user. For
466 &open_readfile(TAB, "$config{cron_dir}/$_[0]");
469 if (@tab >= 3 && $tab[0] =~ /DO NOT EDIT/ &&
470 $tab[1] =~ /^\s*#/ && $tab[2] =~ /^\s*#/) {
471 @tab = @tab[3..$#tab];
476 =head2 copy_crontab(user)
478 Copy the cron temp file to that for this user. For internal use only.
483 if (&is_readonly_mode()) {
488 if (&read_file_contents($cron_temp_file) =~ /\S/) {
489 local $temp = &transname();
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");
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();
507 $rv = system($config{'cron_user_edit_command'}.
508 " >$temp 2>&1 <$notemp");
512 &user_sub($config{'cron_edit_command'},$_[0]).
513 " >$temp 2>&1 <$notemp");
519 # use the cron copy command
521 $rv = &execute_command(
522 $config{'cron_user_copy_command'},
523 $cron_temp_file, $temp, $temp);
526 $rv = &execute_command(
527 &user_sub($config{'cron_copy_command'}, $_[0]),
528 $cron_temp_file, $temp, $temp);
531 local $out = &read_file_contents($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>"));
539 # No more cron jobs left, so just delete
541 &execute_command($config{'cron_user_delete_command'});
544 &execute_command(&user_sub(
545 $config{'cron_delete_command'}, $_[0]));
548 unlink($cron_temp_file);
552 =head2 parse_job(job-line)
554 Parse a crontab line into an array containing:
555 active, mins, hrs, days, mons, weekdays, command
560 local($job, $active) = ($_[0], 1);
561 if ($job =~ /^#+\s*(.*)$/) {
565 $job =~ /^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/;
566 return ($active, $1, $2, $3, $4, $5, $6);
569 =head2 user_sub(command, user)
571 Replace the string 'USER' in the command with the user name. For internal
579 $tmp =~ s/USER/$_[1]/g;
586 Returns a list of all Unix usernames who are allowed to use Cron.
592 &open_readfile(ALLOW, $config{cron_allow_file});
595 chop; push(@rv, $_) if (/\S/);
604 Return a list of all Unix usernames who are not allowed to use Cron.
610 &open_readfile(DENY, $config{cron_deny_file});
613 chop; push(@rv, $_) if (/\S/);
620 =head2 save_allowed(user, user, ...)
622 Save the list of allowed Unix usernames.
628 &open_tempfile(ALLOW, ">$config{cron_allow_file}");
630 &print_tempfile(ALLOW, $_,"\n");
632 &close_tempfile(ALLOW);
633 chmod(0444, $config{cron_allow_file});
637 =head2 save_denied(user, user, ...)
639 Save the list of denied Unix usernames.
645 &open_tempfile(DENY, "> $config{cron_deny_file}");
647 &print_tempfile(DENY, $_,"\n");
649 &close_tempfile(DENY);
650 chmod(0444, $config{cron_deny_file});
653 =head2 read_envs(user)
655 Returns an array of "name value" strings containing the environment settings
656 from the crontab for some user
661 local(@tab, @rv, $_);
662 @tab = &read_crontab($_[0]);
665 if (/^\s*(\S+)\s*=\s*(.*)$/) { push(@rv, "$1 $2"); }
670 =head2 save_envs(user, [name, value]*)
672 Updates the cron file for some user with the given list of environment
673 variables. All others in the file are removed.
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";
685 chop($line = $_); $line =~ s/#.*$//g;
686 if ($line !~ /^\s*(\S+)\s*=\s*(.*)$/) { print TAB $_; }
689 ©_crontab($_[0]);
692 =head2 expand_run_parts(directory)
694 Internal function to convert a directory like /etc/cron.hourly into a list
695 of scripts in that directory.
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);
706 @rv = grep { !/^\./ } @rv;
707 @rv = map { $dir."/".$_ } @rv;
711 =head2 is_run_parts(command)
713 Returns the dir if some cron job runs a list of commands in some directory,
714 like /etc/cron.hourly. Returns undef otherwise.
719 local $rp = $config{'run_parts'};
720 return $rp && $_[0] =~ /$rp(.*)\s+(\-\-\S+\s+)*([a-z0-9\.\-\/_]+)(\s*\))?$/i ? $3 : undef;
723 =head2 can_edit_user(&access, user)
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.
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];
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'});
744 elsif ($_[0]->{'mode'} == 5) {
745 local @u = getpwnam($_[1]);
746 return $u[3] == $_[0]->{'users'};
753 =head2 show_times_input(&job, [nospecial])
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>
763 return &theme_show_times_input(@_) if (defined(&theme_show_times_input));
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";
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};
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";
784 # Javascript to disable and enable fields
787 function enable_cron_fields(name, form, ena)
789 var els = form.elements[name];
791 for(i=0; i<els.length; i++) {
792 els[i].disabled = !ena;
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";
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);
809 foreach $arr ("mins", "hours", "days", "months", "weekdays") {
810 # Find out which ones are being used
812 local $min = ($arr =~ /days|months/ ? 1 : 0);
813 local $max = $min+scalar(@$arr)-1;
814 foreach $w (split(/,/ , $job->{$arr})) {
817 for($j=$min; $j<=$max; $j++) { $inuse{$j}++; }
819 elsif ($w =~ /^\*\/(\d+)$/) {
821 for($j=$min; $j<=$max; $j+=$1) { $inuse{$j}++; }
823 elsif ($w =~ /^(\d+)-(\d+)\/(\d+)$/) {
824 # only every Nth of some range
825 for($j=$1; $j<=$2; $j+=$3) { $inuse{int($j)}++; }
827 elsif ($w =~ /^(\d+)-(\d+)$/) {
829 for($j=$1; $j<=$2; $j++) { $inuse{int($j)}++; }
836 if ($job->{$arr} eq "*") { undef(%inuse); }
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)'",
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)
858 $job->{$arr} eq "*" || $job->{$arr} eq "" ?
861 if ($v =~ /^(.*)=(.*)$/) { $disp = $1; $code = $2; }
862 else { $disp = $code = $v; }
863 printf "<option value=\"$code\" %s>$disp\n",
864 $inuse{$code} ? "selected" : "";
866 print "</select></td>\n";
868 print "</tr></table></td>\n";
870 print "</tr> <tr $cb> <td colspan=5>$text{'edit_ctrl'}</td> </tr>\n";
873 =head2 parse_times_input(&job, &in)
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
880 sub parse_times_input
883 local %in = %{$_[1]};
884 local @pers = ("mins", "hours", "days", "months", "weekdays");
886 if ($in{'special_def'}) {
887 # Job time is a special period
888 foreach $arr (@pers) {
889 delete($job->{$arr});
891 $job->{'special'} = $in{'special'};
894 # User selection of times
895 foreach $arr (@pers) {
896 if ($in{"all_$arr"}) {
897 # All mins/hrs/etc.. chosen
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;
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; }
912 # Not in a range at all
913 push(@newrange, $range[$i]);
916 # End of the range.. add it
917 $newrange[@newrange - 1] =
918 "$range[$start]-".$range[$i-1];
919 push(@newrange, $range[$i]);
924 # Reached the end while in a range
925 $newrange[@newrange - 1] =
926 "$range[$start]-".$range[$i-1];
928 $job->{$arr} = join(',' , @newrange);
931 &error(&text('save_enone', $text{"edit_$arr"}));
934 delete($job->{'special'});
938 =head2 show_range_input(&job)
940 Given a cron job, prints fields for selecting it's run date range.
946 local $has_start = $job->{'start'};
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),
960 print &ui_oneradio("range_def", 0, $rng, $has_start),"\n";
963 =head2 parse_range_input(&job, &in)
965 Updates the job object with the specified date range. May call &error
969 sub parse_range_input
971 local ($job, $in) = @_;
972 if ($in->{'range_def'}) {
974 delete($job->{'start'});
975 delete($job->{'end'});
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) };
984 &error($text{'range_e'.$r}." ".$@);
986 $job->{$r} = [ $in->{'range_'.$r.'_day'},
987 $in->{'range_'.$r.'_month'},
988 $in->{'range_'.$r.'_year'} ];
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' );
997 =head2 fix_names(&cron)
999 Convert day and month names to numbers. For internal use when parsing
1007 local @mts = split(/,/, $_[0]->{'months'});
1009 local $mi = &indexof(lc($m), @cron_month);
1010 $m = $mi+1 if ($mi >= 0);
1012 $_[0]->{'months'} = join(",", @mts);
1014 local @wds = split(/,/, $_[0]->{'weekdays'});
1016 local $di = &indexof(lc($w), @cron_weekday);
1017 $w = $di if ($di >= 0);
1018 $w = 0 if ($w == 7);
1020 $_[0]->{'weekdays'} = join(",", @wds);
1023 =head2 create_wrapper(wrapper-path, module, script)
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.
1030 The parameters are :
1032 =item wrapper-path - Full path to the wrapper to create, like /etc/webmin/yourmodule/foo.pl
1034 =item module - Module containing the real script to call.
1036 =item script - Program within that module for the wrapper to run.
1041 local $perl_path = &get_perl_path();
1042 &open_tempfile(CMD, ">$_[0]");
1043 &print_tempfile(CMD, <<EOF
1045 open(CONF, "$config_directory/miniserv.conf");
1047 \$root = \$1 if (/^root=(.*)/);
1050 \$ENV{'PERLLIB'} = "\$root";
1051 \$ENV{'WEBMIN_CONFIG'} = "$ENV{'WEBMIN_CONFIG'}";
1052 \$ENV{'WEBMIN_VAR'} = "$ENV{'WEBMIN_VAR'}";
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");
1064 # Can use exec on Unix systems
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");
1070 &print_tempfile(CMD, "chdir(\"\$root\");\n");
1071 &print_tempfile(CMD, "exec(\"\$root/$_[2]\", \@ARGV) || die \"Failed to run \$root/$_[2] : \$!\";\n");
1074 &close_tempfile(CMD);
1078 =head2 cron_file(&job)
1080 Returns the file that a cron job is in, or will be in when it is created
1081 based on the username.
1086 return $_[0]->{'file'} || $config{'add_file'} ||
1087 "$config{'cron_dir'}/$_[0]->{'user'}";
1090 =head2 when_text(&job, [upper-case-first])
1092 Returns a human-readable text string describing when a cron job is run.
1097 local $pfx = $_[1] ? "uc" : "";
1098 if ($_[0]->{'special'}) {
1099 $pfx = $_[1] ? "" : "lc";
1100 return $text{$pfx.'edit_special_'.$_[0]->{'special'}};
1102 elsif ($_[0]->{'mins'} eq '*' && $_[0]->{'hours'} eq '*' && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') {
1103 return $text{$pfx.'when_min'};
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'});
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'});
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'});
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'}});
1118 return &text($pfx.'when_cron', join(" ", $_[0]->{'mins'}, $_[0]->{'hours'}, $_[0]->{'days'}, $_[0]->{'months'}, $_[0]->{'weekdays'}));
1122 =head2 can_use_cron(user)
1124 Returns 1 if some user is allowed to use cron, based on cron.allow and
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; }
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; }
1141 elsif ($config{cron_deny_all} == 0) { $err = 1; }
1142 elsif ($config{cron_deny_all} == 1) {
1143 if ($in{user} ne "root") { $err = 1; }
1148 =head2 swap_cron_jobs(&job1, &job2)
1150 Swaps two Cron jobs, which must be in the same file, identified by their
1151 hash references as returned by list_cron_jobs.
1156 if ($_[0]->{'type'} == 0) {
1157 ©_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 ©_crontab($_[0]->{'user'});
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();
1172 =head2 find_cron_process(&job, [&procs])
1174 Finds the running process that was launched from a cron job. The parameters are:
1176 =item job - A cron job hash reference
1178 =item procs - An optional array reference of running process hash refs
1181 sub find_cron_process
1188 &foreign_require("proc", "proc-lib.pl");
1189 @procs = &proc::list_processes();
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/;
1199 if ($config{'match_mode'} == 1) {
1202 ($proc) = grep { $_->{'args'} =~ /\Q$cmd\E/ &&
1203 (!$config{'match_user'} || $_->{'user'} eq $_[0]->{'user'}) }
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'}) }
1216 =head2 find_cron_job(command, [&jobs], [user])
1218 Returns the cron job object that runs some command (perhaps with redirection)
1223 my ($cmd, $jobs, $user) = @_;
1225 $jobs = [ &list_cron_jobs() ];
1228 my @rv = grep { $_->{'user'} eq $user &&
1229 $_->{'command'} =~ /(^|[ \|\&;\/])\Q$cmd\E($|[ \|\&><;])/ } @$jobs;
1230 return wantarray ? @rv : $rv[0];
1233 =head2 extract_input(command)
1235 Given a line formatted like I<command%input>, returns the command and input
1236 parts, taking any escaping into account.
1243 local ($cmd, $input) = split(/\%/, $cmd, 2);
1246 return ($cmd, $input);
1249 =head2 convert_range(&job)
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.
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;
1266 $job->{'command'} .= '%'.$input;
1273 =head2 unconvert_range(&job)
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.
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'}})." ".
1289 $job->{'command'} .= '%'.$input;
1291 delete($job->{'start'});
1292 delete($job->{'end'});
1293 ©_source_dest("$module_root_directory/range.pl", $range_cmd);
1294 &set_ownership_permissions(undef, undef, 0755, $range_cmd);
1300 =head2 convert_comment(&job)
1302 Given a cron job with a # comment after the command, sets the comment field
1308 if ($job->{'command'} =~ /^(.*)\s*#([^#]*)$/) {
1309 $job->{'command'} = $1;
1310 $job->{'comment'} = $2;
1316 =head2 unconvert_comment(&job)
1318 Adds an comment back to the command in a cron job, based on the comment field
1319 of the given hash reference.
1322 sub unconvert_comment
1325 if ($job->{'comment'} =~ /\S/) {
1326 $job->{'command'} .= " #".$job->{'comment'};
1332 =head2 check_cron_config
1334 Returns an error message if the cron config doesn't look valid, or some needed
1338 sub check_cron_config
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>");
1344 if ($config{'cron_get_command'} =~ /^(\S+)/ && !&has_command("$1")) {
1345 return &text('index_ecmd', "<tt>$1</tt>");
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>");
1356 =head2 check_cron_config_or_error
1358 Calls check_cron_config, and then error if any problems were detected.
1361 sub check_cron_config_or_error
1363 local $err = &check_cron_config();
1365 &error(&text('index_econfigcheck', $err));
1369 =head2 cleanup_temp_files
1371 Called from cron to delete old files in the Webmin /tmp directory
1374 sub cleanup_temp_files
1376 # Don't run if disabled
1377 if (!$gconfig{'tempdelete_days'}) {
1378 print STDERR "Temp file clearing is disabled\n";
1381 if ($gconfig{'tempdir'} && !$gconfig{'tempdirdelete'}) {
1382 print STDERR "Temp file clearing is not done for the custom directory $gconfig{'tempdir'}\n";
1386 local $tempdir = &transname();
1387 $tempdir =~ s/\/([^\/]+)$//;
1388 if (!$tempdir || $tempdir eq "/") {
1389 $tempdir = "/tmp/.webmin";
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");