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	&copy_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	&copy_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	&copy_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	&copy_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	&copy_cron_temp($_[0]);
433	&replace_file_line($cron_temp_file, $_[0]->{'line'},
434			   &cron_job_line($_[0])."\n");
435	&copy_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	&copy_cron_temp($_[0]);
453	&replace_file_line($cron_temp_file, $_[0]->{'line'});
454	&copy_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&copy_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	&copy_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	&copy_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	&copy_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