Merge branch 'master' of github.com:webmin/webmin

This commit is contained in:
Jamie Cameron
2026-10-12 21:25:16 -07:00
6 changed files with 127 additions and 11 deletions

View File

@@ -1079,6 +1079,8 @@ if ($in{'special_def'}) {
foreach $arr (@pers) {
delete($job->{$arr});
}
# Special times are single words like daily or reboot
$in{'special'} =~ /^[a-z]+$/i || &error($text{'save_especial'});
$job->{'special'} = $in{'special'};
}
else {
@@ -1092,6 +1094,11 @@ else {
# Need to work out and simplify ranges selected
local (@range, @newrange, $i);
@range = split(/\0/, $in{$arr});
# Selected times are plain numbers
foreach my $v (@range) {
$v =~ /^\d+$/ || &error(&text('save_etime',
$text{"edit_$arr"}));
}
@range = sort { $a <=> $b } @range;
local $start = -1;
for($i=0; $i<@range; $i++) {
@@ -1168,6 +1175,11 @@ if ($in->{'range_def'}) {
else {
# Validate and store range
foreach my $r ("start", "end") {
# Dates go into the job's command, so only allow numbers
foreach my $f ("day", "month", "year") {
$in->{'range_'.$r.'_'.$f} =~ /^\d+$/ ||
&error($text{'range_e'.$r});
}
eval { timelocal(0, 0, 0, $in->{'range_'.$r.'_day'},
$in->{'range_'.$r.'_month'}-1,
$in->{'range_'.$r.'_year'}-1900) };

View File

@@ -81,6 +81,9 @@ lcedit_special_reboot=when system boots
save_err=Failed to save cron job
save_ecmd=You did not enter a command to execute
save_enewline=The command and comment cannot contain line breaks
save_especial=Invalid special time selected
save_etime=Invalid $1 selected
save_ename=Missing or invalid environment variable name
save_evalue=Invalid environment variable value
save_euser=You must select a user

View File

@@ -38,6 +38,10 @@ foreach $f (@files) { &lock_file($f); }
if ($in{"cmd"} !~ /\S/ && $access{'command'}) {
&error($text{'save_ecmd'});
}
# A line break would add extra lines to the crontab
if ($in{'cmd'} =~ /[\r\n]/ || $in{'comment'} =~ /[\r\n]/) {
&error($text{'save_enewline'});
}
if (&supports_users()) {
if (!$in{'user'}) {
&error($text{'save_euser'});

View File

@@ -81,15 +81,23 @@ ok(!cron_move_affects_env(\@jobs, $jobs[0], $jobs[6]),
my $cronfile = "$vardir/crontab";
my $crondir = "$vardir/crontabs";
make_path($crondir);
open(my $modfh, ">", "$confdir/cron/config") or die "cron config: $!";
print $modfh "single_file=$cronfile\n";
print $modfh "cron_dir=$crondir\n";
print $modfh "system_crontab=$vardir/system-crontab\n";
print $modfh "cronfiles_dir=$vardir/cron.d\n";
print $modfh "vixie_cron=1\n";
close($modfh);
make_path("$vardir/cron.d");
# Write the Cron module config, with the given lines added.
sub write_cron_config
{
my (@lines) = @_;
open(my $fh, ">", "$confdir/cron/config") or die "cron config: $!";
print $fh map { "$_\n" } (@lines,
"cron_dir=$crondir",
"system_crontab=$vardir/system-crontab",
"cronfiles_dir=$vardir/cron.d",
"vixie_cron=1",
"cron_deny_all=2");
close($fh);
}
write_cron_config("single_file=$cronfile");
# Reset the crontab fixture to its original contents.
sub write_crontab
{
@@ -246,6 +254,39 @@ like($result->{'out'}, qr/^Location:/m,
is(read_crontab_fixture(), $original,
"re-enabling the normal job restores the fixture");
# Job fields must not add lines or fields to the crontab
seed_acl(%full_acl);
my %edit = ( 'idx' => 0, 'user' => 'root', 'active' => 1,
'cmd' => 'echo first', 'range_def' => 1,
'special_def' => 0, 'all_mins' => 1, 'all_hours' => 1,
'all_days' => 1, 'all_months' => 1, 'all_weekdays' => 1 );
foreach my $case (
[ "a line break in the command",
{ 'cmd' => "echo first\nPATH=/tmp" }, qr/cannot contain line breaks/ ],
[ "a line break in the comment",
{ 'comment' => "x\nPATH=/tmp" }, qr/cannot contain line breaks/ ],
[ "a special time with spaces",
{ 'special_def' => 1, 'special' => "reboot echo x #" },
qr/Invalid special time/ ],
[ "a minute with spaces",
{ 'all_mins' => 0, 'mins' => "* * * * * echo x #" },
qr/Invalid Minutes selected/ ],
[ "a minute with a line break",
{ 'all_mins' => 0, 'mins' => "5\nPATH=/tmp" },
qr/Invalid Minutes selected/ ],
[ "a range date with shell text",
{ 'range_def' => 0, 'range_start_day' => '1;echo x;',
'range_start_month' => 1, 'range_start_year' => 2026,
'range_end_day' => 31, 'range_end_month' => 12,
'range_end_year' => 2026 }, qr/invalid start date/i ],
) {
my ($desc, $form, $re) = @$case;
$result = run_cgi("save_cron.cgi", { %edit, %$form });
like($result->{'out'}, $re, "save_cron.cgi rejects $desc");
is(read_crontab_fixture(), $original,
"rejected $desc leaves the crontab unchanged");
}
seed_acl(%full_acl);
$result = run_cgi("move.cgi", { 'idx' => 4, 'top' => 1 });
like($result->{'out'}, qr/^Location:/m,
@@ -253,4 +294,23 @@ like($result->{'out'}, qr/^Location:/m,
isnt(read_crontab_fixture(), $original,
"the permitted top move changes the crontab");
# Saving a job needs a real Unix user, so check a valid save in a per-user
# crontab, as on Linux. Plain shell commands stand in for the crontab
# command, so the real one never runs.
write_cron_config("cron_copy_command=cat >$crondir/USER",
"cron_delete_command=rm -f $crondir/USER");
$cronfile = "$crondir/root";
write_crontab();
$result = run_cgi("save_cron.cgi",
{ %edit, 'all_mins' => 0, 'mins' => [ 5, 6, 30 ],
'range_def' => 0, 'range_start_day' => 1,
'range_start_month' => 1, 'range_start_year' => 2026,
'range_end_day' => 31, 'range_end_month' => 12,
'range_end_year' => 2026 });
like($result->{'out'}, qr/^Location:/m,
"save_cron.cgi accepts valid times and dates");
like(read_crontab_fixture(),
qr/^5-6,30 \* \* \* \* \S+range\.pl 1-1-2026 31-12-2026 echo\\ first$/m,
"the saved job has the expected times and date range");
done_testing();

View File

@@ -173,6 +173,40 @@ subtest 'convert_to_json / convert_from_json' => sub {
is_deeply(main::convert_from_json($pretty), {a=>1,b=>2},
'pretty output still parses');
# Sorting is independent of whitespace, with the same contract in both engines.
for my $engine ('JSON::PP', 'JSON::XS') {
subtest "$engine canonical output" => sub {
eval "require $engine; 1" or plan skip_all => "$engine is not installed";
# Force the supported PP fallback even when XS is installed.
local $INC{'JSON/XS.pm'} = $engine eq 'JSON::PP' ? undef : $INC{'JSON/XS.pm'};
my $data = {z => 2, a => {z => 1, a => 0}};
is(main::convert_to_json($data, 0, 1, 1),
'{"a":{"a":0,"z":1},"z":2}', 'compact output sorts nested keys');
is(main::convert_to_json($data, 1),
$engine->new->pretty->canonical->latin1->encode($data),
'existing pretty calls retain sorted output');
is(main::convert_to_json($data, 1, 0, 0),
$engine->new->pretty->latin1->encode($data),
'explicit false disables sorting independently of pretty printing');
is(main::convert_to_json($data, 0, 0, undef),
$engine->new->latin1->encode($data),
'undefined canonical flag retains the default');
# JSON syntax and byte encoding must remain independent of sorting.
my $unicode = {label => "Caf\x{e9} \x{65e5}\x{672c} \x{1f642}"};
my $bytes = main::convert_to_json($unicode, 0, 1, 1);
ok(!utf8::is_utf8($bytes), 'UTF-8 mode returns bytes');
is_deeply(main::convert_from_json($bytes), $unicode, 'Unicode round-trips');
is($bytes, JSON::PP->new->canonical->utf8->encode($unicode),
'compact UTF-8 matches the existing canonical encoding');
my $backup = {content => join('', map { chr } 0..255)};
my $raw = main::convert_to_json($backup, 0, 0, 1);
is($raw, JSON::PP->new->canonical->encode($backup),
'raw configuration bytes retain their existing serialized form');
is_deeply(main::convert_from_json($raw, 1), $backup,
'raw configuration bytes round-trip');
};
}
# Current contract: undef input becomes {} (the `||= {}` default).
is(main::convert_to_json(undef), '{}', 'undef input → "{}"');

View File

@@ -13849,18 +13849,20 @@ for(my $i=0; $i<@sp1 || $i<@sp2; $i++) {
return 0;
}
=head2 convert_to_json(data, [pretty], [raw-utf8])
=head2 convert_to_json(data, [pretty], [raw-utf8], [canonical])
Converts the given Perl data structure to encoded binary string
=item data parameter is a hash/array reference
=item if the output should be prettified and hash keys sorted
=item pretty parameter enables indentation and newlines; also sorts hash keys unless canonical is supplied
=item raw-utf8 parameter, if set to 1, encodes data using UTF-8
=item canonical parameter controls hash key sorting independently of pretty printing; defaults to pretty
=cut
sub convert_to_json
{
my ($data, $pretty, $raw_utf8) = @_;
my ($data, $pretty, $raw_utf8, $canonical) = @_;
my $json;
if (eval { require JSON::XS }) {
@@ -13873,7 +13875,8 @@ else {
error("Neither JSON::XS nor JSON::PP Perl module is available on your system");
}
$json->pretty(!!$pretty);
$json->canonical(1) if ($pretty);
# Preserve sorting for existing pretty calls unless explicitly overridden.
$json->canonical(defined($canonical) ? !!$canonical : !!$pretty);
$data ||= {};
return $raw_utf8 ? $json->utf8->encode($data) : $json->latin1->encode($data);
}