1=head1 cron-lib.pl 2 3Functions 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 15BEGIN { push(@INC, ".."); }; 16use WebminCore; 17&init_config(); 18%access = &get_module_acl(); 19$env_support = $config{'vixie_cron'}; 20if ($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 } 27else { 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(); 35use Time::Local; 36 37=head2 list_cron_jobs 38 39Returns a lists of structures of all cron jobs, each of which is a hash 40reference 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 59sub list_cron_jobs 60{ 61local (@rv, $lnum, $f); 62if (scalar(@cron_jobs_cache)) { 63 return @cron_jobs_cache; 64 } 65 66# read the master crontab file 67if ($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+(user-name\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 111opendir(DIR, &translate_filename($config{'cronfiles_dir'})); 112my @files = sort { $a cmp $b } readdir(DIR); 113closedir(DIR); 114foreach my $f (@files) { 115 next if ($f =~ /^\./); 116 $lnum = 0; 117 &open_readfile(TAB, "$config{'cronfiles_dir'}/$f"); 118 while(<TAB>) { 119 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) { 120 push(@rv, { 'file' => "$config{'cronfiles_dir'}/$f", 121 'line' => $lnum, 122 'type' => 2, 123 'active' => !$1, 124 'nolog' => $2, 125 'mins' => $3, 'hours' => $4, 126 'days' => $5, 'months' => $6, 127 'weekdays' => $8, 'user' => $10, 128 'command' => $11, 129 'index' => scalar(@rv) }); 130 &fix_names($rv[$#rv]); 131 } 132 elsif (/^(#+)?\s*@([a-z]+)\s+(\S+)\s+(.*)/i) { 133 push(@rv, { 'file' => "$config{'cronfiles_dir'}/$f", 134 'line' => $lnum, 135 'type' => 2, 136 'active' => !$1, 137 'special' => $2, 138 'user' => $3, 139 'command' => $4, 140 'index' => scalar(@rv) }); 141 } 142 $lnum++; 143 } 144 close(TAB); 145 } 146 147# Read a single user's crontab file 148if ($config{'single_file'}) { 149 &open_readfile(TAB, $config{'single_file'}); 150 $lnum = 0; 151 while(<TAB>) { 152 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) { 153 # A normal m h d m wd time 154 push(@rv, { 'file' => $config{'single_file'}, 155 'line' => $lnum, 156 'type' => 3, 157 'active' => !$1, 'nolog' => $2, 158 'mins' => $3, 'hours' => $4, 159 'days' => $5, 'months' => $6, 160 'weekdays' => $8, 'user' => "NONE", 161 'command' => $10, 162 'index' => scalar(@rv) }); 163 &fix_names($rv[$#rv]); 164 } 165 elsif (/^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*'([^']*)'/ || 166 /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*"([^']*)"/ || 167 /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*(\S+)/) { 168 # An environment variable 169 push(@rv, { 'file' => $config{'single_file'}, 170 'line' => $lnum, 171 'active' => !$1, 172 'name' => $2, 173 'value' => $3, 174 'user' => "NONE", 175 'command' => '', 176 'index' => scalar(@rv) }); 177 } 178 $lnum++; 179 } 180 close(TAB); 181 } 182 183 184# read per-user cron files 185local $fcron = ($config{'cron_dir'} =~ /\/fcron$/); 186local @users; 187if ($single_user) { 188 @users = ( $single_user ); 189 } 190else { 191 opendir(DIR, &translate_filename($config{'cron_dir'})); 192 @users = grep { !/^\./ } readdir(DIR); 193 closedir(DIR); 194 } 195foreach $f (@users) { 196 next if (!(@uinfo = getpwnam($f))); 197 $lnum = 0; 198 if ($single_user) { 199 &open_execute_command(TAB, $config{'cron_user_get_command'}, 1); 200 } 201 elsif ($fcron) { 202 &open_execute_command(TAB, 203 &user_sub($config{'cron_get_command'}, $f), 1); 204 } 205 else { 206 &open_readfile(TAB, "$config{'cron_dir'}/$f"); 207 } 208 while(<TAB>) { 209 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) { 210 # A normal m h d m wd time 211 push(@rv, { 'file' => "$config{'cron_dir'}/$f", 212 'line' => $lnum, 213 'type' => 0, 214 'active' => !$1, 'nolog' => $2, 215 'mins' => $3, 'hours' => $4, 216 'days' => $5, 'months' => $6, 217 'weekdays' => $8, 'user' => $f, 218 'command' => $10, 219 'index' => scalar(@rv) }); 220 $rv[$#rv]->{'file'} =~ s/\s+\|$//; 221 &fix_names($rv[$#rv]); 222 } 223 elsif (/^(#+)?\s*@([a-z]+)\s+(.*)/i) { 224 # An @ time 225 push(@rv, { 'file' => "$config{'cron_dir'}/$f", 226 'line' => $lnum, 227 'type' => 0, 228 'active' => !$1, 229 'special' => $2, 230 'user' => $f, 231 'command' => $3, 232 'index' => scalar(@rv) }); 233 } 234 elsif (/^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*'([^']*)'/ || 235 /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*"([^']*)"/ || 236 /^(#+)?\s*([a-zA-Z0-9\_]+)\s*=\s*(\S+)/) { 237 # An environment variable 238 push(@rv, { 'file' => "$config{'cron_dir'}/$f", 239 'line' => $lnum, 240 'active' => !$1, 241 'name' => $2, 242 'value' => $3, 243 'user' => $f, 244 'index' => scalar(@rv) }); 245 } 246 $lnum++; 247 } 248 close(TAB); 249 } 250closedir(DIR); 251@cron_jobs_cache = @rv; 252return @cron_jobs_cache; 253} 254 255=head2 cron_job_line(&job) 256 257Internal function to generate a crontab format line for a cron job. 258 259=cut 260sub cron_job_line 261{ 262local @c; 263push(@c, "#") if (!$_[0]->{'active'}); 264if ($_[0]->{'name'}) { 265 push(@c, $_[0]->{'name'}); 266 push(@c, "="); 267 push(@c, $_[0]->{'value'} =~ /'/ ? "\"$_[0]->{'value'}\"" : 268 $_[0]->{'value'} =~ /"/ ? "'$_[0]->{'value'}'" : 269 $_[0]->{'value'} !~ /^\S+$/ ? "\"$_[0]->{'value'}\"" 270 : $_[0]->{'value'}); 271 } 272else { 273 if ($_[0]->{'special'}) { 274 push(@c, ($_[0]->{'nolog'} ? '-' : '').'@'.$_[0]->{'special'}); 275 } 276 else { 277 push(@c, ($_[0]->{'nolog'} ? '-' : '').$_[0]->{'mins'}, 278 $_[0]->{'hours'}, $_[0]->{'days'}, 279 $_[0]->{'months'}, $_[0]->{'weekdays'}); 280 } 281 push(@c, $_[0]->{'user'}) if ($_[0]->{'type'} != 0 && 282 $_[0]->{'type'} != 3); 283 push(@c, $_[0]->{'command'}); 284 } 285if ($gconfig{'os_type'} eq 'syno-linux') { 286 return join("\t", @c); 287 } 288else { 289 return join(" ", @c); 290 } 291} 292 293=head2 copy_cron_temp(&job) 294 295Copies a job's user's current cron configuration to the temp file. For internal 296use only. 297 298=cut 299sub copy_cron_temp 300{ 301local $fcron = ($config{'cron_dir'} =~ /\/fcron$/); 302unlink($cron_temp_file); 303if ($single_user) { 304 &execute_command($config{'cron_user_get_command'}, 305 undef, $cron_temp_file, undef); 306 } 307elsif ($fcron) { 308 &execute_command(&user_sub($config{'cron_get_command'},$_[0]->{'user'}), 309 undef, $cron_temp_file, undef); 310 } 311else { 312 system("cp ".&translate_filename("$config{'cron_dir'}/$_[0]->{'user'}"). 313 " $cron_temp_file 2>/dev/null"); 314 } 315} 316 317=head2 create_cron_job(&job) 318 319Add a Cron job to a user's file. The job parameter must be a hash reference 320in the same format as returned by list_cron_jobs. 321 322=cut 323sub create_cron_job 324{ 325&check_cron_config_or_error(); 326&list_cron_jobs(); # init cache 327if ($config{'add_file'}) { 328 # Add to a specific file, typically something like /etc/cron.d/webmin 329 $_[0]->{'type'} = 1; 330 local $lref = &read_file_lines($config{'add_file'}); 331 push(@$lref, &cron_job_line($_[0])); 332 &flush_file_lines($config{'add_file'}); 333 } 334elsif ($config{'single_file'} && !$config{'cron_dir'}) { 335 # Add to the single file 336 $_[0]->{'type'} = 3; 337 local $lref = &read_file_lines($config{'single_file'}); 338 push(@$lref, &cron_job_line($_[0])); 339 &flush_file_lines($config{'single_file'}); 340 } 341else { 342 # Add to the specified user's crontab 343 ©_cron_temp($_[0]); 344 local $lref = &read_file_lines($cron_temp_file); 345 $_[0]->{'line'} = scalar(@$lref); 346 push(@$lref, &cron_job_line($_[0])); 347 &flush_file_lines($cron_temp_file); 348 &set_ownership_permissions($_[0]->{'user'}, undef, undef, 349 $cron_temp_file); 350 ©_crontab($_[0]->{'user'}); 351 $_[0]->{'file'} = "$config{'cron_dir'}/$_[0]->{'user'}"; 352 $_[0]->{'index'} = scalar(@cron_jobs_cache); 353 push(@cron_jobs_cache, $_[0]); 354 } 355} 356 357=head2 insert_cron_job(&job) 358 359Add a Cron job at the top of the user's file. The job parameter must be a hash 360reference in the same format as returned by list_cron_jobs. 361 362=cut 363sub insert_cron_job 364{ 365&check_cron_config_or_error(); 366&list_cron_jobs(); # init cache 367if ($config{'single_file'} && !$config{'cron_dir'}) { 368 # Insert into single file 369 $_[0]->{'type'} = 3; 370 local $lref = &read_file_lines($config{'single_file'}); 371 splice(@$lref, 0, 0, &cron_job_line($_[0])); 372 &flush_file_lines($config{'single_file'}); 373 } 374else { 375 # Insert into the user's crontab 376 ©_cron_temp($_[0]); 377 local $lref = &read_file_lines($cron_temp_file); 378 $_[0]->{'line'} = 0; 379 splice(@$lref, 0, 0, &cron_job_line($_[0])); 380 &flush_file_lines(); 381 system("chown $_[0]->{'user'} $cron_temp_file"); 382 ©_crontab($_[0]->{'user'}); 383 $_[0]->{'file'} = "$config{'cron_dir'}/$_[0]->{'user'}"; 384 $_[0]->{'index'} = scalar(@cron_jobs_cache); 385 &renumber($_[0]->{'file'}, $_[0]->{'line'}, 1); 386 push(@cron_jobs_cache, $_[0]); 387 } 388} 389 390=head2 renumber(file, line, offset) 391 392All jobs in this file whose line is at or after the given one will be 393incremented by the offset. For internal use. 394 395=cut 396sub renumber 397{ 398local $j; 399foreach $j (@cron_jobs_cache) { 400 if ($j->{'line'} >= $_[1] && 401 $j->{'file'} eq $_[0]) { 402 $j->{'line'} += $_[2]; 403 } 404 } 405} 406 407=head2 renumber_index(index, offset) 408 409Internal function to change the index of all cron jobs in the cache after 410some index by a given offset. For internal use. 411 412=cut 413sub renumber_index 414{ 415local $j; 416foreach $j (@cron_jobs_cache) { 417 if ($j->{'index'} >= $_[0]) { 418 $j->{'index'} += $_[1]; 419 } 420 } 421} 422 423=head2 change_cron_job(&job) 424 425Updates the given cron job, which must be a hash ref returned by list_cron_jobs 426and modified with a new active flag, command or schedule. 427 428=cut 429sub change_cron_job 430{ 431if ($_[0]->{'type'} == 0) { 432 ©_cron_temp($_[0]); 433 &replace_file_line($cron_temp_file, $_[0]->{'line'}, 434 &cron_job_line($_[0])."\n"); 435 ©_crontab($_[0]->{'user'}); 436 } 437else { 438 &replace_file_line($_[0]->{'file'}, $_[0]->{'line'}, 439 &cron_job_line($_[0])."\n"); 440 } 441} 442 443=head2 delete_cron_job(&job) 444 445Removes the cron job defined by the given hash ref, as returned by 446list_cron_jobs. 447 448=cut 449sub delete_cron_job 450{ 451if ($_[0]->{'type'} == 0) { 452 ©_cron_temp($_[0]); 453 &replace_file_line($cron_temp_file, $_[0]->{'line'}); 454 ©_crontab($_[0]->{'user'}); 455 } 456else { 457 &replace_file_line($_[0]->{'file'}, $_[0]->{'line'}); 458 } 459@cron_jobs_cache = grep { $_ ne $_[0] } @cron_jobs_cache; 460&renumber($_[0]->{'file'}, $_[0]->{'line'}, -1); 461&renumber_index($_[0]->{'index'}, -1); 462} 463 464=head2 read_crontab(user) 465 466Return an array containing the lines of the cron table for some user. For 467internal use mainly. 468 469=cut 470sub read_crontab 471{ 472local(@tab); 473&open_readfile(TAB, "$config{cron_dir}/$_[0]"); 474@tab = <TAB>; 475close(TAB); 476if (@tab >= 3 && $tab[0] =~ /DO NOT EDIT/ && 477 $tab[1] =~ /^\s*#/ && $tab[2] =~ /^\s*#/) { 478 @tab = @tab[3..$#tab]; 479 } 480return @tab; 481} 482 483=head2 copy_crontab(user) 484 485Copy the cron temp file to that for this user. For internal use only. 486 487=cut 488sub copy_crontab 489{ 490if (&is_readonly_mode()) { 491 # Do nothing 492 return undef; 493 } 494local($pwd); 495if (&read_file_contents($cron_temp_file) =~ /\S/) { 496 local $temp = &transname(); 497 local $rv; 498 if (!&has_crontab_cmd()) { 499 # We have no crontab command .. emulate by copying to user file 500 $rv = system("cat $cron_temp_file". 501 " >$config{'cron_dir'}/$_[0] 2>/dev/null"); 502 &set_ownership_permissions($_[0], undef, 0600, 503 "$config{'cron_dir'}/$_[0]"); 504 } 505 elsif ($config{'cron_edit_command'}) { 506 # fake being an editor 507 local $notemp = &transname(); 508 &open_tempfile(NO, ">$notemp"); 509 &print_tempfile(NO, "No\n"); 510 &print_tempfile(NO, "N\n"); 511 &print_tempfile(NO, "no\n"); 512 &close_tempfile(NO); 513 $ENV{"VISUAL"} = $ENV{"EDITOR"} = 514 "$module_root_directory/cron_editor.pl"; 515 $ENV{"CRON_EDITOR_COPY"} = $cron_temp_file; 516 system("chown $_[0] $cron_temp_file"); 517 local $oldpwd = &get_current_dir(); 518 chdir("/"); 519 if ($single_user) { 520 $rv = system($config{'cron_user_edit_command'}. 521 " >$temp 2>&1 <$notemp"); 522 } 523 else { 524 $rv = system( 525 &user_sub($config{'cron_edit_command'},$_[0]). 526 " >$temp 2>&1 <$notemp"); 527 } 528 unlink($notemp); 529 chdir($oldpwd); 530 531 } else { 532 # use the cron copy command 533 if ($single_user) { 534 $rv = &execute_command( 535 $config{'cron_user_copy_command'}, 536 $cron_temp_file, $temp, $temp); 537 } 538 else { 539 $rv = &execute_command( 540 &user_sub($config{'cron_copy_command'}, $_[0]), 541 $cron_temp_file, $temp, $temp); 542 } 543 } 544 local $out = &read_file_contents($temp); 545 unlink($temp); 546 if ($rv || $out =~ /error/i) { 547 local $cronin = &read_file_contents($cron_temp_file); 548 &error(&text('ecopy', "<pre>$out</pre>", "<pre>$cronin</pre>")); 549 } 550 } 551else { 552 # No more cron jobs left, so just delete 553 if (!&has_crontab_cmd()) { 554 # We have no crontab command .. emulate by deleting user crontab 555 $_[0] || &error("No user given!"); 556 &unlink_logged("$config{'cron_dir'}/$_[0]"); 557 } 558 else{ 559 if ($single_user) { 560 &execute_command($config{'cron_user_delete_command'}); 561 } 562 else { 563 &execute_command(&user_sub( 564 $config{'cron_delete_command'}, $_[0])); 565 } 566 } 567 } 568if (!&has_crontab_cmd()) { 569 # to reload config 570 &kill_byname("crond", "SIGHUP"); 571 } 572unlink($cron_temp_file); 573} 574 575 576=head2 parse_job(job-line) 577 578Parse a crontab line into an array containing: 579active, mins, hrs, days, mons, weekdays, command 580 581=cut 582sub parse_job 583{ 584local($job, $active) = ($_[0], 1); 585if ($job =~ /^#+\s*(.*)$/) { 586 $active = 0; 587 $job = $1; 588 } 589$job =~ /^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/; 590return ($active, $1, $2, $3, $4, $5, $6); 591} 592 593=head2 user_sub(command, user) 594 595Replace the string 'USER' in the command with the user name. For internal 596use only. 597 598=cut 599sub user_sub 600{ 601local($tmp); 602$tmp = $_[0]; 603$tmp =~ s/USER/$_[1]/g; 604return $tmp; 605} 606 607 608=head2 list_allowed 609 610Returns a list of all Unix usernames who are allowed to use Cron. 611 612=cut 613sub list_allowed 614{ 615local(@rv, $_); 616&open_readfile(ALLOW, $config{cron_allow_file}); 617while(<ALLOW>) { 618 next if (/^\s*#/); 619 chop; push(@rv, $_) if (/\S/); 620 } 621close(ALLOW); 622return @rv; 623} 624 625 626=head2 list_denied 627 628Return a list of all Unix usernames who are not allowed to use Cron. 629 630=cut 631sub list_denied 632{ 633local(@rv, $_); 634&open_readfile(DENY, $config{cron_deny_file}); 635while(<DENY>) { 636 next if (/^\s*#/); 637 chop; push(@rv, $_) if (/\S/); 638 } 639close(DENY); 640return @rv; 641} 642 643 644=head2 save_allowed(user, user, ...) 645 646Save the list of allowed Unix usernames. 647 648=cut 649sub save_allowed 650{ 651local($_); 652&open_tempfile(ALLOW, ">$config{cron_allow_file}"); 653foreach (@_) { 654 &print_tempfile(ALLOW, $_,"\n"); 655 } 656&close_tempfile(ALLOW); 657chmod(0444, $config{cron_allow_file}); 658} 659 660 661=head2 save_denied(user, user, ...) 662 663Save the list of denied Unix usernames. 664 665=cut 666sub save_denied 667{ 668local($_); 669&open_tempfile(DENY, "> $config{cron_deny_file}"); 670foreach (@_) { 671 &print_tempfile(DENY, $_,"\n"); 672 } 673&close_tempfile(DENY); 674chmod(0444, $config{cron_deny_file}); 675} 676 677=head2 read_envs(user) 678 679Returns an array of "name value" strings containing the environment settings 680from the crontab for some user 681 682=cut 683sub read_envs 684{ 685local(@tab, @rv, $_); 686@tab = &read_crontab($_[0]); 687foreach (@tab) { 688 chop; s/#.*$//g; 689 if (/^\s*(\S+)\s*=\s*(.*)$/) { push(@rv, "$1 $2"); } 690 } 691return @rv; 692} 693 694=head2 save_envs(user, [name, value]*) 695 696Updates the cron file for some user with the given list of environment 697variables. All others in the file are removed. 698 699=cut 700sub save_envs 701{ 702local($i, @tab, $line); 703@tab = &read_crontab($_[0]); 704open(TAB, ">".$cron_temp_file); 705for($i=1; $i<@_; $i+=2) { 706 print TAB "$_[$i]=$_[$i+1]\n"; 707 } 708foreach (@tab) { 709 chop($line = $_); $line =~ s/#.*$//g; 710 if ($line !~ /^\s*(\S+)\s*=\s*(.*)$/) { print TAB $_; } 711 } 712close(TAB); 713©_crontab($_[0]); 714} 715 716=head2 expand_run_parts(directory) 717 718Internal function to convert a directory like /etc/cron.hourly into a list 719of scripts in that directory. 720 721=cut 722sub expand_run_parts 723{ 724local $dir = $_[0]; 725$dir = "$config{'run_parts_dir'}/$dir" 726 if ($config{'run_parts_dir'} && $dir !~ /^\//); 727opendir(DIR, &translate_filename($dir)); 728local @rv = readdir(DIR); 729closedir(DIR); 730@rv = grep { /^[a-zA-Z0-9\_\-]+$/ } @rv; 731@rv = map { $dir."/".$_ } @rv; 732@rv = grep { -x $_ } @rv; 733return @rv; 734} 735 736=head2 is_run_parts(command) 737 738Returns the dir if some cron job runs a list of commands in some directory, 739like /etc/cron.hourly. Returns undef otherwise. 740 741=cut 742sub is_run_parts 743{ 744local ($cmd) = @_; 745local $rp = $config{'run_parts'}; 746$cmd =~ s/\s*#.*$//; 747return $rp && $cmd =~ /$rp(.*)\s+(\-\-\S+\s+)*([a-z0-9\.\-\/_]+)(\s*\))?$/i ? $3 : undef; 748} 749 750=head2 can_edit_user(&access, user) 751 752Returns 1 if the Webmin user whose permissions are defined by the access hash 753ref can manage cron jobs for a given Unix user. 754 755=cut 756sub can_edit_user 757{ 758local %umap; 759map { $umap{$_}++; } split(/\s+/, $_[0]->{'users'}) 760 if ($_[0]->{'mode'} == 1 || $_[0]->{'mode'} == 2); 761if ($_[0]->{'mode'} == 1 && !$umap{$_[1]} || 762 $_[0]->{'mode'} == 2 && $umap{$_[1]}) { return 0; } 763elsif ($_[0]->{'mode'} == 3) { 764 return $remote_user eq $_[1]; 765 } 766elsif ($_[0]->{'mode'} == 4) { 767 local @u = getpwnam($_[1]); 768 return (!$_[0]->{'uidmin'} || $u[2] >= $_[0]->{'uidmin'}) && 769 (!$_[0]->{'uidmax'} || $u[2] <= $_[0]->{'uidmax'}); 770 } 771elsif ($_[0]->{'mode'} == 5) { 772 local @u = getpwnam($_[1]); 773 return $u[3] == $_[0]->{'users'}; 774 } 775else { 776 return 1; 777 } 778} 779 780=head2 list_cron_specials() 781 782Returns a list of the names of special cron times, prefixed by an @ in crontab 783 784=cut 785sub list_cron_specials 786{ 787return ('hourly', 'daily', 'weekly', 'monthly', 'yearly', 'reboot'); 788} 789 790=head2 get_times_input(&job, [nospecial], [width-in-cols], [message]) 791 792Returns HTML for selecting the schedule for a cron job, defined by the first 793parameter which must be a hash ref returned by list_cron_jobs. Suitable for 794use inside a ui_table_start/end 795 796=cut 797sub get_times_input 798{ 799return &theme_get_times_input(@_) if (defined(&theme_get_times_input)); 800my ($job, $nospecial, $width, $msg) = @_; 801$width ||= 2; 802 803# Javascript to disable and enable fields 804my $rv = <<EOF; 805<script> 806function enable_cron_fields(name, form, ena) 807{ 808var els = form.elements[name]; 809els.disabled = !ena; 810for(i=0; i<els.length; i++) { 811 els[i].disabled = !ena; 812 } 813change_special_mode(form, 0); 814} 815 816function change_special_mode(form, special) 817{ 818 if(form.special_def) { 819 form.special_def[0].checked = special; 820 form.special_def[1].checked = !special; 821 } 822} 823</script> 824EOF 825 826if ($config{'vixie_cron'} && (!$nospecial || $job->{'special'})) { 827 # Allow selection of special @ times 828 my $sp = $job->{'special'} eq 'midnight' ? 'daily' : 829 $job->{'special'} eq 'annually' ? 'yearly' : $job->{'special'}; 830 my $specialsel = &ui_select("special", $sp, 831 [ map { [ $_, $text{'edit_special_'.$_} ] } 832 &list_cron_specials() ], 833 1, 0, 0, 0, "onChange='change_special_mode(form, 1)'"); 834 $rv .= &ui_table_row($msg, 835 &ui_radio("special_def", $job->{'special'} ? 1 : 0, 836 [ [ 1, $text{'edit_special1'}." ".$specialsel ], 837 [ 0, $text{'edit_special0'} ] ]), 838 $msg ? $width-1 : $width); 839 } 840 841# Section for time selections 842my $table = &ui_columns_start([ $text{'edit_mins'}, $text{'edit_hours'}, 843 $text{'edit_days'}, $text{'edit_months'}, 844 $text{'edit_weekdays'} ], 100); 845my @mins = (0..59); 846my @hours = (0..23); 847my @days = (1..31); 848my @months = map { $text{"month_$_"}."=".$_ } (1 .. 12); 849my @weekdays = map { $text{"day_$_"}."=".$_ } (0 .. 6); 850my %arrmap = ( 'mins' => \@mins, 851 'hours' => \@hours, 852 'days' => \@days, 853 'months' => \@months, 854 'weekdays' => \@weekdays ); 855my @cols; 856foreach my $arr ("mins", "hours", "days", "months", "weekdays") { 857 # Find out which ones are being used 858 my %inuse; 859 my $min = ($arr =~ /days|months/ ? 1 : 0); 860 my @arrlist = @{$arrmap{$arr}}; 861 my $max = $min+scalar(@arrlist)-1; 862 foreach my $w (split(/,/ , $job->{$arr})) { 863 if ($w eq "*") { 864 # all values 865 for($j=$min; $j<=$max; $j++) { $inuse{$j}++; } 866 } 867 elsif ($w =~ /^\*\/(\d+)$/) { 868 # only every Nth 869 for($j=$min; $j<=$max; $j+=$1) { $inuse{$j}++; } 870 } 871 elsif ($w =~ /^(\d+)-(\d+)\/(\d+)$/) { 872 # only every Nth of some range 873 for($j=$1; $j<=$2; $j+=$3) { $inuse{int($j)}++; } 874 } 875 elsif ($w =~ /^(\d+)-(\d+)$/) { 876 # all of some range 877 for($j=$1; $j<=$2; $j++) { $inuse{int($j)}++; } 878 } 879 else { 880 # One value 881 $inuse{int($w)}++; 882 } 883 } 884 if ($job->{$arr} eq "*") { 885 %inuse = ( ); 886 } 887 888 # Output selection list 889 my $dis = $arr eq "mins" && $hourly_only; 890 my $col = &ui_radio( 891 "all_$arr", $job->{$arr} eq "*" || 892 $job->{$arr} eq "" ? 1 : 0, 893 [ [ 1, $text{'edit_all'}."<br>", 894 "onClick='enable_cron_fields(\"$arr\", form, 0)'" ], 895 [ 0, $text{'edit_selected'}."<br>", 896 "onClick='enable_cron_fields(\"$arr\", form, 1)'" ] ], 897 $dis); 898 $col .= "<table> <tr>\n"; 899 for(my $j=0; $j<@arrlist; $j+=($arr eq "mins" && $hourly_only ? 60 : 12)) { 900 my $jj = $j+($arr eq "mins" && $hourly_only ? 59 : 11); 901 if ($jj >= @arrlist) { $jj = @arrlist - 1; } 902 my @sec = @arrlist[$j .. $jj]; 903 my @opts; 904 foreach my $v (@sec) { 905 if ($v =~ /^(.*)=(.*)$/) { 906 push(@opts, [ $2, $1 ]); 907 } 908 else { 909 push(@opts, [ $v, $v ]); 910 } 911 } 912 my $dis = $job->{$arr} eq "*" || $job->{$arr} eq ""; 913 $col .= "<td valign=top>". 914 &ui_select($arr, [ keys %inuse ], \@opts, 915 @sec > 12 ? ($arr eq "mins" && $hourly_only ? 1 : 12) 916 : scalar(@sec), 917 $arr eq "mins" && $hourly_only ? 0 : 1, 918 0, $dis). 919 "</td>\n"; 920 } 921 $col .= "</tr></table>\n"; 922 push(@cols, $col); 923 } 924$table .= &ui_columns_row(\@cols, [ "valign=top", "valign=top", "valign=top", 925 "valign=top", "valign=top" ]); 926$table .= &ui_columns_end(); 927$table .= $text{'edit_ctrl'}; 928$rv .= &ui_table_row(undef, $table, $width); 929return $rv; 930} 931 932=head2 show_times_input(&job, [nospecial]) 933 934Print HTML for inputs for selecting the schedule for a cron job, defined 935by the first parameter which must be a hash ref returned by list_cron_jobs. 936This must be used inside a <table>, as the HTML starts and ends with <tr> 937tags. 938 939=cut 940sub show_times_input 941{ 942return &theme_show_times_input(@_) if (defined(&theme_show_times_input)); 943local $job = $_[0]; 944if ($config{'vixie_cron'} && (!$_[1] || $_[0]->{'special'})) { 945 # Allow selection of special @ times 946 print "<tr data-schedule-tr $cb> <td colspan=6>\n"; 947 printf "<input type=radio name=special_def value=1 %s> %s\n", 948 $job->{'special'} ? "checked" : "", $text{'edit_special1'}; 949 print "<select name=special onChange='change_special_mode(form, 1)'>\n"; 950 local $s; 951 local $sp = $job->{'special'} eq 'midnight' ? 'daily' : 952 $job->{'special'} eq 'annually' ? 'yearly' : $job->{'special'}; 953 foreach $s ('hourly', 'daily', 'weekly', 'monthly', 'yearly', 'reboot'){ 954 printf "<option value=%s %s>%s</option>\n", 955 $s, $sp eq $s ? "selected" : "", $text{'edit_special_'.$s}; 956 } 957 print "</select>\n"; 958 printf "<input type=radio name=special_def value=0 %s> %s\n", 959 $job->{'special'} ? "" : "checked", $text{'edit_special0'}; 960 print "</td></tr>\n"; 961 } 962 963# Javascript to disable and enable fields 964print <<EOF; 965<script> 966function enable_cron_fields(name, form, ena) 967{ 968var els = form.elements[name]; 969els.disabled = !ena; 970for(i=0; i<els.length; i++) { 971 els[i].disabled = !ena; 972 } 973change_special_mode(form, 0); 974} 975 976function change_special_mode(form, special) 977{ 978 if(form.special_def) { 979 form.special_def[0].checked = special; 980 form.special_def[1].checked = !special; 981 } 982} 983</script> 984EOF 985 986print "<tr $tb>\n"; 987print "<td><b>$text{'edit_mins'}</b></td> <td><b>$text{'edit_hours'}</b></td> ", 988 "<td><b>$text{'edit_days'}</b></td> <td><b>$text{'edit_months'}</b></td>", 989 "<td><b>$text{'edit_weekdays'}</b></td> </tr> <tr $cb>\n"; 990 991local @mins = (0..59); 992local @hours = (0..23); 993local @days = (1..31); 994local @months = map { $text{"month_$_"}."=".$_ } (1 .. 12); 995local @weekdays = map { $text{"day_$_"}."=".$_ } (0 .. 6); 996 997foreach $arr ("mins", "hours", "days", "months", "weekdays") { 998 # Find out which ones are being used 999 local %inuse; 1000 local $min = ($arr =~ /days|months/ ? 1 : 0); 1001 local $max = $min+scalar(@$arr)-1; 1002 foreach $w (split(/,/ , $job->{$arr})) { 1003 if ($w eq "*") { 1004 # all values 1005 for($j=$min; $j<=$max; $j++) { $inuse{$j}++; } 1006 } 1007 elsif ($w =~ /^\*\/(\d+)$/) { 1008 # only every Nth 1009 for($j=$min; $j<=$max; $j+=$1) { $inuse{$j}++; } 1010 } 1011 elsif ($w =~ /^(\d+)-(\d+)\/(\d+)$/) { 1012 # only every Nth of some range 1013 for($j=$1; $j<=$2; $j+=$3) { $inuse{int($j)}++; } 1014 } 1015 elsif ($w =~ /^(\d+)-(\d+)$/) { 1016 # all of some range 1017 for($j=$1; $j<=$2; $j++) { $inuse{int($j)}++; } 1018 } 1019 else { 1020 # One value 1021 $inuse{int($w)}++; 1022 } 1023 } 1024 if ($job->{$arr} eq "*") { undef(%inuse); } 1025 1026 # Output selection list 1027 print "<td valign=top>\n"; 1028 printf "<input type=radio name=all_$arr value=1 %s %s %s> %s<br>\n", 1029 $arr eq "mins" && $hourly_only ? "disabled" : "", 1030 $job->{$arr} eq "*" || $job->{$arr} eq "" ? "checked" : "", 1031 "onClick='enable_cron_fields(\"$arr\", form, 0)'", 1032 $text{'edit_all'}; 1033 printf "<input type=radio name=all_$arr value=0 %s %s> %s<br>\n", 1034 $job->{$arr} eq "*" || $job->{$arr} eq "" ? "" : "checked", 1035 "onClick='enable_cron_fields(\"$arr\", form, 1)'", 1036 $text{'edit_selected'}; 1037 print "<table> <tr>\n"; 1038 for($j=0; $j<@$arr; $j+=($arr eq "mins" && $hourly_only ? 60 : 12)) { 1039 $jj = $j+($arr eq "mins" && $hourly_only ? 59 : 11); 1040 if ($jj >= @$arr) { $jj = @$arr - 1; } 1041 @sec = @$arr[$j .. $jj]; 1042 printf "<td valign=top><select %s size=%d name=$arr %s %s>\n", 1043 $arr eq "mins" && $hourly_only ? "" : "multiple", 1044 @sec > 12 ? ($arr eq "mins" && $hourly_only ? 1 : 12) 1045 : scalar(@sec), 1046 $job->{$arr} eq "*" || $job->{$arr} eq "" ? 1047 "disabled" : "", 1048 "onChange='change_special_mode(form, 0)'"; 1049 foreach $v (@sec) { 1050 if ($v =~ /^(.*)=(.*)$/) { $disp = $1; $code = $2; } 1051 else { $disp = $code = $v; } 1052 printf "<option value=\"$code\" %s>$disp</option>\n", 1053 $inuse{$code} ? "selected" : ""; 1054 } 1055 print "</select></td>\n"; 1056 } 1057 print "</tr></table></td>\n"; 1058 } 1059print "</tr> <tr $cb> <td colspan=5>$text{'edit_ctrl'}</td> </tr>\n"; 1060} 1061 1062=head2 parse_times_input(&job, &in) 1063 1064Parses inputs from the form generated by show_times_input, and updates a cron 1065job hash ref. The in parameter must be a hash ref as generated by the 1066ReadParse function. 1067 1068=cut 1069sub parse_times_input 1070{ 1071local $job = $_[0]; 1072local %in = %{$_[1]}; 1073local @pers = ("mins", "hours", "days", "months", "weekdays"); 1074local $arr; 1075if ($in{'special_def'}) { 1076 # Job time is a special period 1077 foreach $arr (@pers) { 1078 delete($job->{$arr}); 1079 } 1080 $job->{'special'} = $in{'special'}; 1081 } 1082else { 1083 # User selection of times 1084 foreach $arr (@pers) { 1085 if ($in{"all_$arr"}) { 1086 # All mins/hrs/etc.. chosen 1087 $job->{$arr} = "*"; 1088 } 1089 elsif (defined($in{$arr})) { 1090 # Need to work out and simplify ranges selected 1091 local (@range, @newrange, $i); 1092 @range = split(/\0/, $in{$arr}); 1093 @range = sort { $a <=> $b } @range; 1094 local $start = -1; 1095 for($i=0; $i<@range; $i++) { 1096 if ($i && $range[$i]-1 == $range[$i-1]) { 1097 # ok.. looks like a range 1098 if ($start < 0) { $start = $i-1; } 1099 } 1100 elsif ($start < 0) { 1101 # Not in a range at all 1102 push(@newrange, $range[$i]); 1103 } 1104 else { 1105 # End of the range.. add it 1106 $newrange[@newrange - 1] = 1107 "$range[$start]-".$range[$i-1]; 1108 push(@newrange, $range[$i]); 1109 $start = -1; 1110 } 1111 } 1112 if ($start >= 0) { 1113 # Reached the end while in a range 1114 $newrange[@newrange - 1] = 1115 "$range[$start]-".$range[$i-1]; 1116 } 1117 $job->{$arr} = join(',' , @newrange); 1118 } 1119 else { 1120 &error(&text('save_enone', $text{"edit_$arr"})); 1121 } 1122 } 1123 delete($job->{'special'}); 1124 } 1125} 1126 1127=head2 show_range_input(&job) 1128 1129Given a cron job, prints fields for selecting it's run date range. 1130 1131=cut 1132sub show_range_input 1133{ 1134local ($job) = @_; 1135local $has_start = $job->{'start'}; 1136local $rng; 1137$rng = &text('range_start', &ui_date_input( 1138 $job->{'start'}->[0], $job->{'start'}->[1], $job->{'start'}->[2], 1139 "range_start_day", "range_start_month", "range_start_year"))."\n". 1140 &date_chooser_button( 1141 "range_start_day", "range_start_month", "range_start_year")."\n". 1142 &text('range_end', &ui_date_input( 1143 $job->{'end'}->[0], $job->{'end'}->[1], $job->{'end'}->[2], 1144 "range_end_day", "range_end_month", "range_end_year"))."\n". 1145 &date_chooser_button( 1146 "range_end_day", "range_end_month", "range_end_year")."\n"; 1147print &ui_oneradio("range_def", 1, $text{'range_all'}, !$has_start), 1148 "<br>\n"; 1149print &ui_oneradio("range_def", 0, $rng, $has_start),"\n"; 1150} 1151 1152=head2 parse_range_input(&job, &in) 1153 1154Updates the job object with the specified date range. May call &error 1155for invalid inputs. 1156 1157=cut 1158sub parse_range_input 1159{ 1160local ($job, $in) = @_; 1161if ($in->{'range_def'}) { 1162 # No range used 1163 delete($job->{'start'}); 1164 delete($job->{'end'}); 1165 } 1166else { 1167 # Validate and store range 1168 foreach my $r ("start", "end") { 1169 eval { timelocal(0, 0, 0, $in->{'range_'.$r.'_day'}, 1170 $in->{'range_'.$r.'_month'}-1, 1171 $in->{'range_'.$r.'_year'}-1900) }; 1172 if ($@) { 1173 &error($text{'range_e'.$r}." ".$@); 1174 } 1175 $job->{$r} = [ $in->{'range_'.$r.'_day'}, 1176 $in->{'range_'.$r.'_month'}, 1177 $in->{'range_'.$r.'_year'} ]; 1178 } 1179 } 1180} 1181 1182@cron_month = ( 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 1183 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ); 1184@cron_weekday = ( 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ); 1185 1186=head2 fix_names(&cron) 1187 1188Convert day and month names to numbers. For internal use when parsing 1189the crontab file. 1190 1191=cut 1192sub fix_names 1193{ 1194local ($m, $w); 1195 1196local @mts = split(/,/, $_[0]->{'months'}); 1197foreach $m (@mts) { 1198 local $mi = &indexof(lc($m), @cron_month); 1199 $m = $mi+1 if ($mi >= 0); 1200 } 1201$_[0]->{'months'} = join(",", @mts); 1202 1203local @wds = split(/,/, $_[0]->{'weekdays'}); 1204foreach $w (@wds) { 1205 local $di = &indexof(lc($w), @cron_weekday); 1206 $w = $di if ($di >= 0); 1207 $w = 0 if ($w == 7); 1208 } 1209$_[0]->{'weekdays'} = join(",", @wds); 1210} 1211 1212=head2 create_wrapper(wrapper-path, module, script) 1213 1214Creates a wrapper script which calls a script in some module's directory 1215with the proper webmin environment variables set. This should always be used 1216when setting up a cron job, instead of attempting to run a command in the 1217module directory directly. 1218 1219The parameters are : 1220 1221=item wrapper-path - Full path to the wrapper to create, like /etc/webmin/yourmodule/foo.pl 1222 1223=item module - Module containing the real script to call. 1224 1225=item script - Program within that module for the wrapper to run. 1226 1227=cut 1228sub create_wrapper 1229{ 1230local $perl_path = &get_perl_path(); 1231&open_tempfile(CMD, ">$_[0]"); 1232&print_tempfile(CMD, <<EOF 1233#!$perl_path 1234open(CONF, "<$config_directory/miniserv.conf") || die "Failed to open $config_directory/miniserv.conf : \$!"; 1235while(<CONF>) { 1236 \$root = \$1 if (/^root=(.*)/); 1237 } 1238close(CONF); 1239\$root || die "No root= line found in $config_directory/miniserv.conf"; 1240\$ENV{'PERLLIB'} = "\$root"; 1241\$ENV{'WEBMIN_CONFIG'} = "$ENV{'WEBMIN_CONFIG'}"; 1242\$ENV{'WEBMIN_VAR'} = "$ENV{'WEBMIN_VAR'}"; 1243delete(\$ENV{'MINISERV_CONFIG'}); 1244EOF 1245 ); 1246if ($gconfig{'os_type'} eq 'windows') { 1247 # On windows, we need to chdir to the drive first, and use system 1248 &print_tempfile(CMD, "if (\$root =~ /^([a-z]:)/i) {\n"); 1249 &print_tempfile(CMD, " chdir(\"\$1\");\n"); 1250 &print_tempfile(CMD, " }\n"); 1251 &print_tempfile(CMD, "chdir(\"\$root/$_[1]\");\n"); 1252 &print_tempfile(CMD, "exit(system(\"\$root/$_[1]/$_[2]\", \@ARGV));\n"); 1253 } 1254else { 1255 # Can use exec on Unix systems 1256 if ($_[1]) { 1257 &print_tempfile(CMD, "chdir(\"\$root/$_[1]\");\n"); 1258 &print_tempfile(CMD, "exec(\"\$root/$_[1]/$_[2]\", \@ARGV) || die \"Failed to run \$root/$_[1]/$_[2] : \$!\";\n"); 1259 } 1260 else { 1261 &print_tempfile(CMD, "chdir(\"\$root\");\n"); 1262 &print_tempfile(CMD, "exec(\"\$root/$_[2]\", \@ARGV) || die \"Failed to run \$root/$_[2] : \$!\";\n"); 1263 } 1264 } 1265&close_tempfile(CMD); 1266chmod(0755, $_[0]); 1267} 1268 1269=head2 cron_file(&job) 1270 1271Returns the file that a cron job is in, or will be in when it is created 1272based on the username. 1273 1274=cut 1275sub cron_file 1276{ 1277return $_[0]->{'file'} || $config{'add_file'} || 1278 "$config{'cron_dir'}/$_[0]->{'user'}"; 1279} 1280 1281=head2 when_text(&job, [upper-case-first]) 1282 1283Returns a human-readable text string describing when a cron job is run. 1284 1285=cut 1286sub when_text 1287{ 1288local $pfx = $_[1] ? "uc" : ""; 1289if ($_[0]->{'interval'}) { 1290 return &text($pfx.'when_interval', $_[0]->{'interval'}); 1291 } 1292elsif ($_[0]->{'special'}) { 1293 $pfx = $_[1] ? "" : "lc"; 1294 return $text{$pfx.'edit_special_'.$_[0]->{'special'}}; 1295 } 1296elsif ($_[0]->{'boot'}) { 1297 return &text($pfx.'when_boot'); 1298 } 1299elsif ($_[0]->{'mins'} eq '*' && $_[0]->{'hours'} eq '*' && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') { 1300 return $text{$pfx.'when_min'}; 1301 } 1302elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} eq '*' && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') { 1303 return &text($pfx.'when_hour', $_[0]->{'mins'}); 1304 } 1305elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') { 1306 return &text($pfx.'when_day', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'}); 1307 } 1308elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} =~ /^\d+$/ && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} eq '*') { 1309 return &text($pfx.'when_month', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'}, $_[0]->{'days'}); 1310 } 1311elsif ($_[0]->{'mins'} =~ /^\d+$/ && $_[0]->{'hours'} =~ /^\d+$/ && $_[0]->{'days'} eq '*' && $_[0]->{'months'} eq '*' && $_[0]->{'weekdays'} =~ /^\d+$/) { 1312 return &text($pfx.'when_weekday', sprintf("%2.2d", $_[0]->{'mins'}), $_[0]->{'hours'}, $text{"day_".$_[0]->{'weekdays'}}); 1313 } 1314else { 1315 return &text($pfx.'when_cron', join(" ", $_[0]->{'mins'}, $_[0]->{'hours'}, $_[0]->{'days'}, $_[0]->{'months'}, $_[0]->{'weekdays'})); 1316 } 1317} 1318 1319=head2 can_use_cron(user) 1320 1321Returns 1 if some user is allowed to use cron, based on cron.allow and 1322cron.deny files. 1323 1324=cut 1325sub can_use_cron 1326{ 1327local ($user) = @_; 1328defined(getpwnam($user)) || return 0; # User does not exist 1329local $err; 1330if (-r $config{cron_allow_file}) { 1331 local @allowed = &list_allowed(); 1332 if (&indexof($user, @allowed) < 0 && 1333 &indexof("all", @allowed) < 0) { $err = 1; } 1334 } 1335elsif (-r $config{cron_deny_file}) { 1336 local @denied = &list_denied(); 1337 if (&indexof($user, @denied) >= 0 || 1338 &indexof("all", @denied) >= 0) { $err = 1; } 1339 } 1340elsif ($config{cron_deny_all} == 0) { $err = 1; } 1341elsif ($config{cron_deny_all} == 1) { 1342 if ($in{user} ne "root") { $err = 1; } 1343 } 1344return !$err; 1345} 1346 1347=head2 swap_cron_jobs(&job1, &job2) 1348 1349Swaps two Cron jobs, which must be in the same file, identified by their 1350hash references as returned by list_cron_jobs. 1351 1352=cut 1353sub swap_cron_jobs 1354{ 1355if ($_[0]->{'type'} == 0) { 1356 ©_cron_temp($_[0]); 1357 local $lref = &read_file_lines($cron_temp_file); 1358 ($lref->[$_[0]->{'line'}], $lref->[$_[1]->{'line'}]) = 1359 ($lref->[$_[1]->{'line'}], $lref->[$_[0]->{'line'}]); 1360 &flush_file_lines(); 1361 ©_crontab($_[0]->{'user'}); 1362 } 1363else { 1364 local $lref = &read_file_lines($_[0]->{'file'}); 1365 ($lref->[$_[0]->{'line'}], $lref->[$_[1]->{'line'}]) = 1366 ($lref->[$_[1]->{'line'}], $lref->[$_[0]->{'line'}]); 1367 &flush_file_lines(); 1368 } 1369} 1370 1371=head2 find_cron_process(&job, [&procs]) 1372 1373Finds the running process that was launched from a cron job. The parameters are: 1374 1375=item job - A cron job hash reference 1376 1377=item procs - An optional array reference of running process hash refs 1378 1379=cut 1380sub find_cron_process 1381{ 1382local @procs; 1383if ($_[1]) { 1384 @procs = @{$_[1]}; 1385 } 1386else { 1387 &foreign_require("proc", "proc-lib.pl"); 1388 @procs = &proc::list_processes(); 1389 } 1390local $rpd = &is_run_parts($_[0]->{'command'}); 1391local @exp = $rpd ? &expand_run_parts($rpd) : (); 1392local $cmd = $exp[0] || $_[0]->{'command'}; 1393$cmd =~ s/^\s*\[.*\]\s+\&\&\s+//; 1394$cmd =~ s/^\s*\[.*\]\s+\|\|\s+//; 1395while($cmd =~ s/(\d*)(<|>)((\/\S+)|&\d+)\s*$//) { } 1396$cmd =~ s/^\((.*)\)\s*$/$1/; 1397$cmd =~ s/\s+$//; 1398my $eos; 1399if ($config{'match_mode'} == 1) { 1400 $cmd =~ s/\s.*$//; 1401 $eos = '$'; 1402 } 1403else { 1404 my $cmd_ = $cmd; 1405 $cmd_ =~ s/\s.*$//; 1406 if ($cmd_ eq $cmd) { 1407 $eos = '$'; 1408 } 1409 } 1410 1411# If `Input to command` is set, remove it for test case 1412# otherwise cmd will always be displayed as not running 1413my $cmd_ = $cmd; 1414 1415# First remove only input to command and preserve 1416# potential arguments containing escaped %, like \\% 1417$cmd_ =~ s/(?<!\\)%.*//; 1418 1419# Now replace escaped \\ special chars for testing purposes 1420$cmd_ =~ s/\\//g; 1421 1422($proc) = grep { $_->{'args'} =~ /\Q$cmd_\E$eos/ && 1423 (!$config{'match_user'} || $_->{'user'} eq $_[0]->{'user'}) } 1424 @procs; 1425if (!$proc && $cmd =~ /^$config_directory\/(.*\.pl)(.*)$/) { 1426 # Must be a Webmin wrapper 1427 $cmd = "$root_directory/$1$2"; 1428 ($proc) = grep { $_->{'args'} =~ /\Q$cmd\E/ && 1429 (!$config{'match_user'} || 1430 $_->{'user'} eq $_[0]->{'user'}) } 1431 @procs; 1432 } 1433return $proc; 1434} 1435 1436=head2 find_cron_job(command, [&jobs], [user]) 1437 1438Returns the cron job object that runs some command (perhaps with redirection) 1439 1440=cut 1441sub find_cron_job 1442{ 1443my ($cmd, $jobs, $user) = @_; 1444if (!$jobs) { 1445 $jobs = [ &list_cron_jobs() ]; 1446 } 1447$user ||= "root"; 1448my @rv = grep { $_->{'user'} eq $user && 1449 $_->{'command'} =~ /(^|[ \|\&;\/])\Q$cmd\E($|[ \|\&><;])/ } @$jobs; 1450return wantarray ? @rv : $rv[0]; 1451} 1452 1453=head2 extract_input(command) 1454 1455Given a line formatted like I<command%input>, returns the command and input 1456parts, taking any escaping into account. 1457 1458=cut 1459sub extract_input 1460{ 1461local ($cmd) = @_; 1462$cmd =~ s/\\%/\0/g; 1463local ($cmd, $input) = split(/\%/, $cmd, 2); 1464$cmd =~ s/\0/\\%/g; 1465$input =~ s/\0/\\%/g; 1466return ($cmd, $input); 1467} 1468 1469=head2 convert_range(&job) 1470 1471Given a cron job that uses range.pl, work out the date range and update 1472the job object command. Mainly for internal use. 1473 1474=cut 1475sub convert_range 1476{ 1477local ($job) = @_; 1478local ($cmd, $input) = &extract_input($job->{'command'}); 1479if ($cmd =~ /^\Q$range_cmd\E\s+(\d+)\-(\d+)\-(\d+)\s+(\d+)\-(\d+)\-(\d+)\s+(.*)$/) { 1480 # Looks like a range command 1481 $job->{'start'} = [ $1, $2, $3 ]; 1482 $job->{'end'} = [ $4, $5, $6 ]; 1483 $job->{'command'} = $7; 1484 $job->{'command'} =~ s/\\(.)/$1/g; 1485 if ($input) { 1486 $job->{'command'} .= '%'.$input; 1487 } 1488 return 1; 1489 } 1490return 0; 1491} 1492 1493=head2 unconvert_range(&job) 1494 1495Give a cron job with start and end fields, updates the command to wrap it in 1496range.pl with those dates as parameters. 1497 1498=cut 1499sub unconvert_range 1500{ 1501local ($job) = @_; 1502if ($job->{'start'}) { 1503 # Need to add range command 1504 local ($cmd, $input) = &extract_input($job->{'command'}); 1505 $job->{'command'} = $range_cmd." ".join("-", @{$job->{'start'}})." ". 1506 join("-", @{$job->{'end'}})." ". 1507 quotemeta($cmd); 1508 if ($input) { 1509 $job->{'command'} .= '%'.$input; 1510 } 1511 delete($job->{'start'}); 1512 delete($job->{'end'}); 1513 ©_source_dest("$module_root_directory/range.pl", $range_cmd); 1514 &set_ownership_permissions(undef, undef, 0755, $range_cmd); 1515 return 1; 1516 } 1517return 0; 1518} 1519 1520=head2 convert_comment(&job) 1521 1522Given a cron job with a # comment after the command, sets the comment field 1523 1524=cut 1525sub convert_comment 1526{ 1527local ($job) = @_; 1528if ($job->{'command'} =~ /^(.*\S)\s*#([^#]*)$/) { 1529 $job->{'command'} = $1; 1530 $job->{'comment'} = $2; 1531 return 1; 1532 } 1533return 0; 1534} 1535 1536=head2 unconvert_comment(&job) 1537 1538Adds an comment back to the command in a cron job, based on the comment field 1539of the given hash reference. 1540 1541=cut 1542sub unconvert_comment 1543{ 1544local ($job) = @_; 1545if ($job->{'comment'} =~ /\S/) { 1546 $job->{'command'} .= " #".$job->{'comment'}; 1547 return 1; 1548 } 1549return 0; 1550} 1551 1552=head2 check_cron_config 1553 1554Returns an error message if the cron config doesn't look valid, or some needed 1555command is missing. 1556 1557=cut 1558sub check_cron_config 1559{ 1560# Check for single file and getter command 1561if ($config{'single_file'} && !-r $config{'single_file'}) { 1562 return &text('index_esingle', "<tt>$config{'single_file'}</tt>"); 1563 } 1564if (!&has_crontab_cmd() && $config{'cron_get_command'} =~ /^(\S+)/ && 1565 !&has_command("$1")) { 1566 return &text('index_ecmd', "<tt>$1</tt>"); 1567 } 1568# Check for directory 1569local $fcron = ($config{'cron_dir'} =~ /\/fcron$/); 1570if (!$single_user && !$config{'single_file'} && 1571 !$fcron && !-d $config{'cron_dir'}) { 1572 if (!$in{'create_dir'}) { 1573 return &text('index_ecrondir', "<tt>$config{'cron_dir'}</tt>"). 1574 "<p><a href=\"index.cgi?create_dir=yes\">".&text('index_ecrondir_create' ,"<tt>$config{'cron_dir'}</tt>")."</a></p>"; 1575 } else { 1576 &make_dir($config{'cron_dir'}, 0755); 1577 } 1578 } 1579return undef; 1580} 1581 1582=head2 check_cron_config_or_error 1583 1584Calls check_cron_config, and then error if any problems were detected. 1585 1586=cut 1587sub check_cron_config_or_error 1588{ 1589local $err = &check_cron_config(); 1590if ($err) { 1591 &error(&text('index_econfigcheck', $err)); 1592 } 1593} 1594 1595=head2 cleanup_temp_files 1596 1597Called from cron to delete old files in the Webmin /tmp directory 1598 1599=cut 1600sub cleanup_temp_files 1601{ 1602# Don't run if disabled 1603if (!$gconfig{'tempdelete_days'}) { 1604 print STDERR "Temp file clearing is disabled\n"; 1605 return; 1606 } 1607if ($gconfig{'tempdir'} && !$gconfig{'tempdirdelete'}) { 1608 print STDERR "Temp file clearing is not done for the custom directory $gconfig{'tempdir'}\n"; 1609 return; 1610 } 1611 1612local $tempdir = &transname(); 1613$tempdir =~ s/\/([^\/]+)$//; 1614if (!$tempdir || $tempdir eq "/") { 1615 $tempdir = "/tmp/.webmin"; 1616 } 1617 1618local $cutoff = time() - $gconfig{'tempdelete_days'}*24*60*60; 1619opendir(DIR, $tempdir); 1620foreach my $f (readdir(DIR)) { 1621 next if ($f eq "." || $f eq ".."); 1622 local @st = lstat("$tempdir/$f"); 1623 if ($st[9] < $cutoff) { 1624 &unlink_file("$tempdir/$f"); 1625 } 1626 } 1627closedir(DIR); 1628} 1629 1630=head2 list_cron_files() 1631 1632Returns a list of all files containing cron jobs 1633 1634=cut 1635sub list_cron_files 1636{ 1637my @jobs = &list_cron_jobs(); 1638my @files = map { $_->{'file'} } grep { $_->{'file'} } @jobs; 1639if ($config{'system_crontab'}) { 1640 push(@files, $config{'system_crontab'}); 1641 } 1642if ($config{'cronfiles_dir'}) { 1643 push(@files, glob(&translate_filename($config{'cronfiles_dir'})."/*")); 1644 } 1645return &unique(@files); 1646} 1647 1648=head2 has_crontab_cmd() 1649 1650Returns 1 if the crontab command exists on this system 1651 1652=cut 1653sub has_crontab_cmd 1654{ 1655my $cmd = $config{'cron_edit_command'}; 1656if ($cmd) { 1657 $cmd =~ s/^su.*-c\s+//; 1658 ($cmd) = &split_quoted_string($cmd); 1659 my $rv = &has_command($cmd); 1660 return $rv if ($rv); 1661 } 1662return &has_command("crontab"); 1663} 1664 1665=head2 next_run(&job) 1666 1667Given a cron job, returns the unix time on which it will run next. 1668 1669=cut 1670sub next_run 1671{ 1672my ($job) = @_; 1673my $now = time(); 1674my @tm = localtime($now); 1675if ($job->{'special'} eq 'hourly') { 1676 $job = { 'mins' => 0, 1677 'hours' => '*', 1678 'days' => '*', 1679 'months' => '*', 1680 'weekdays' => '*' }; 1681 } 1682elsif ($job->{'special'} eq 'daily') { 1683 $job = { 'mins' => 0, 1684 'hours' => 0, 1685 'days' => '*', 1686 'months' => '*', 1687 'weekdays' => '*' }; 1688 } 1689elsif ($job->{'special'} eq 'weekly') { 1690 $job = { 'mins' => 0, 1691 'hours' => 0, 1692 'days' => '*', 1693 'months' => '*', 1694 'weekdays' => 0 }; 1695 } 1696elsif ($job->{'special'} eq 'yearly') { 1697 $job = { 'mins' => 0, 1698 'hours' => 0, 1699 'days' => 1, 1700 'months' => 1, 1701 'weekdays' => '*' }; 1702 } 1703elsif ($job->{'special'} eq 'reboot') { 1704 return undef; 1705 } 1706my @mins = &cron_all_ranges($job->{'mins'}, 0, 59); 1707my @hours = &cron_all_ranges($job->{'hours'}, 0, 23); 1708my @days = &cron_all_ranges($job->{'days'}, 1, 31); 1709my @months = &cron_all_ranges($job->{'months'}, 1, 12); 1710my @weekdays = &cron_all_ranges($job->{'weekdays'}, 0, 6); 1711my ($min, $hour, $day, $month, $year); 1712my @possible; 1713foreach $min (@mins) { 1714 foreach $hour (@hours) { 1715 foreach $day (@days) { 1716 foreach $month (@months) { 1717 foreach $year ($tm[5] .. $tm[5]+7) { 1718 my $tt; 1719 eval { $tt = timelocal(0, $min, $hour, $day, $month-1, $year) }; 1720 next if ($tt < $now); 1721 my @ttm = localtime($tt); 1722 next if (&indexof($ttm[6], @weekdays) < 0); 1723 push(@possible, $tt); 1724 last; 1725 } 1726 } 1727 } 1728 } 1729 } 1730@possible = sort { $a <=> $b } @possible; 1731return $possible[0]; 1732} 1733 1734=head2 cron_range(range, min, max) 1735 1736=cut 1737sub cron_range 1738{ 1739my ($w, $min, $max) = @_; 1740my $j; 1741my %inuse; 1742if ($w eq "*") { 1743 # all values 1744 for($j=$min; $j<=$max; $j++) { $inuse{$j}++; } 1745 } 1746elsif ($w =~ /^\*\/(\d+)$/) { 1747 # only every Nth 1748 my $step = $1 || 1; 1749 for($j=$min; $j<=$max; $j+=$step) { $inuse{$j}++; } 1750 } 1751elsif ($w =~ /^(\d+)-(\d+)\/(\d+)$/) { 1752 # only every Nth of some range 1753 my $step = $3 || 1; 1754 for($j=$1; $j<=$2; $j+=$step) { $inuse{int($j)}++; } 1755 } 1756elsif ($w =~ /^(\d+)-(\d+)$/) { 1757 # all of some range 1758 for($j=$1; $j<=$2; $j++) { $inuse{int($j)}++; } 1759 } 1760else { 1761 # One value 1762 $inuse{int($w)}++; 1763 } 1764return sort { $a <=> $b } (keys %inuse); 1765} 1766 1767=head2 cron_all_ranges(comma-list, min, max) 1768 1769=cut 1770sub cron_all_ranges 1771{ 1772my @rv; 1773foreach $r (split(/,/, $_[0])) { 1774 push(@rv, &cron_range($r, $_[1], $_[2])); 1775 } 1776return sort { $a <=> $b } @rv; 1777} 1778 17791; 1780 1781