diff --git a/t/web-lib-funcs-acl.t b/t/web-lib-funcs-acl.t
new file mode 100644
index 000000000..1a8d46124
--- /dev/null
+++ b/t/web-lib-funcs-acl.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+# Unit tests for ACL preservation helpers in web-lib-funcs.pl.
+
+use strict;
+use warnings;
+use Test::More;
+use File::Basename qw(dirname);
+use File::Spec;
+
+my $script = File::Spec->rel2abs(
+ File::Spec->catfile(dirname(__FILE__), '..', 'web-lib-funcs.pl'));
+require $script;
+
+subtest 'physical restore is enabled when supported' => sub {
+ no warnings 'redefine';
+ my $calls = 0;
+ local *main::backquote_command = sub {
+ $calls++;
+ return "setfacl 2.4.0 -- set file access control lists\n".
+ "Usage: setfacl [-bkndRLP] { -m|-M|-x|-X ... } file ...\n".
+ " setfacl [-P] --restore=file\n".
+ " -P, --physical physical walk, do not follow symbolic links\n".
+ " --restore=file restore ACLs (inverse of `getfacl -R')\n";
+ };
+
+ is(main::get_setfacl_restore_command('/usr/bin/setfacl'),
+ '/usr/bin/setfacl -P --restore=-',
+ 'uses physical restore with setfacl 2.4.0 and later');
+ is(main::get_setfacl_restore_command('/usr/bin/setfacl'),
+ '/usr/bin/setfacl -P --restore=-',
+ 'keeps using physical restore on later calls');
+ is($calls, 1, 'caches the capability check');
+};
+
+subtest 'older setfacl remains supported' => sub {
+ no warnings 'redefine';
+ local *main::backquote_command = sub {
+ return "setfacl 2.3.2 -- set file access control lists\n".
+ "Usage: setfacl [-bkndRLP] { -m|-M|-x|-X ... } file ...\n".
+ " -P, --physical physical walk, do not follow symbolic links\n".
+ " --restore=file restore ACLs (inverse of `getfacl -R')\n";
+ };
+
+ is(main::get_setfacl_restore_command('/usr/local/bin/setfacl'),
+ '/usr/local/bin/setfacl --restore=-',
+ 'does not pass incompatible -P option to older versions');
+};
+
+done_testing();
diff --git a/t/xmlrpc.t b/t/xmlrpc.t
index 8ec2e3d55..c01d04b9e 100644
--- a/t/xmlrpc.t
+++ b/t/xmlrpc.t
@@ -110,6 +110,14 @@ subtest 'encode_xml_value type selection' => sub {
# Plain strings.
like(encode_xml_value('hello'), qr{^hello\s*$}, 'word -> string');
like(encode_xml_value(''), qr{^\s*$}, 'empty string -> empty ');
+ my @warnings;
+ my $undef_xml;
+ {
+ local $SIG{__WARN__} = sub { push(@warnings, @_); };
+ $undef_xml = encode_xml_value(undef);
+ }
+ like($undef_xml, qr{^\s*$}, 'undef -> empty ');
+ is_deeply(\@warnings, [], 'undef emits no warnings');
# A value with control characters cannot live in a (the
# printable-range regex fails), so it must fall through to base64.
@@ -189,6 +197,19 @@ subtest 'parse_xml_value' => sub {
is(parse_xml_value(value_tree('1')),1, 'boolean parsed');
is(parse_xml_value(value_tree('2.5')),'2.5', 'double parsed');
is(parse_xml_value(value_tree('hi')), 'hi', 'string parsed');
+ my @warnings;
+ {
+ local $SIG{__WARN__} = sub { push(@warnings, @_); };
+ is(parse_xml_value(value_tree('')), '',
+ 'empty string parsed');
+ is(parse_xml_value(value_tree('')), '', 'implicit empty string parsed');
+ is(parse_xml_value(value_tree('')), '',
+ 'empty base64 parsed');
+ my $empty_name = parse_xml_value(value_tree(
+ 'x'));
+ is_deeply($empty_name, { '' => 'x' }, 'empty struct member name parsed');
+ }
+ is_deeply(\@warnings, [], 'empty XML values emit no warnings');
# base64 is decoded back to its raw bytes.
my $b64 = encode_base64("ab\x00cd");
diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl
index 04ac0cd16..b0399c104 100755
--- a/web-lib-funcs.pl
+++ b/web-lib-funcs.pl
@@ -10359,16 +10359,17 @@ out progress of an HTTP request.
=cut
sub progress_callback
{
+my ($mode, $arg) = @_;
if (defined(&theme_progress_callback)) {
# Call the theme override
return &theme_progress_callback(@_);
}
-if ($_[0] == 2) {
+if ($mode == 2) {
# Got size
print $progress_callback_prefix;
- if ($_[1]) {
- $progress_size = $_[1];
- $progress_step = int($_[1] / 10);
+ if ($arg) {
+ $progress_size = $arg;
+ $progress_step = int($arg / 10);
print &text('progress_size2',
&html_escape($progress_callback_url),
&nice_size($progress_size)),"
\n";
@@ -10380,42 +10381,42 @@ if ($_[0] == 2) {
}
$last_progress_time = $last_progress_size = undef;
}
-elsif ($_[0] == 3) {
+elsif ($mode == 3) {
# Got data update
my $sp = $progress_callback_prefix.(" " x 5);
if ($progress_size) {
# And we have a size to compare against
- my $st = int(($_[1] * 10) / $progress_size);
+ my $st = int(($arg * 10) / $progress_size);
my $time_now = time();
if ($st != $progress_step ||
$time_now - $last_progress_time > 60) {
# Show progress every 10% or 60 seconds
- print $sp,&text('progress_datan', &nice_size($_[1]),
- int($_[1]*100/$progress_size)),"
\n";
+ print $sp,&text('progress_datan', &nice_size($arg),
+ int($arg*100/$progress_size)),"
\n";
$last_progress_time = $time_now;
}
$progress_step = $st;
}
else {
# No total size .. so only show in 1M jumps
- if ($_[1] > $last_progress_size+1024*1024) {
+ if ($arg > $last_progress_size+1024*1024) {
print $sp,&text('progress_data2n',
- &nice_size($_[1])),"
\n";
- $last_progress_size = $_[1];
+ &nice_size($arg)),"
\n";
+ $last_progress_size = $arg;
}
}
}
-elsif ($_[0] == 4) {
+elsif ($mode == 4) {
# All done downloading
print $progress_callback_prefix,&text('progress_done'),"
\n";
}
-elsif ($_[0] == 5) {
+elsif ($mode == 5) {
# Got new location after redirect
- $progress_callback_url = $_[1];
+ $progress_callback_url = $arg;
}
-elsif ($_[0] == 6) {
+elsif ($mode == 6) {
# URL is in cache
- $progress_callback_url = $_[1];
+ $progress_callback_url = $arg;
print &text('progress_incache',
&html_escape($progress_callback_url)),"
\n";
}
@@ -11629,7 +11630,8 @@ elsif (defined($main::open_tempfiles{$_[0]})) {
}
if ($file_acls) {
# Set original ACLs
- open(my $pipe, '|-', "$setfacl --restore=-");
+ my $restore_command = &get_setfacl_restore_command($setfacl);
+ open(my $pipe, '|-', $restore_command);
print($pipe $file_acls);
close($pipe);
}
@@ -11694,6 +11696,27 @@ if (!defined($main::selinux_enabled_cache)) {
return $main::selinux_enabled_cache;
}
+=head2 get_setfacl_restore_command(setfacl-command)
+
+Returns a command for restoring ACLs from standard input. Uses physical
+restore when supported by setfacl, while remaining compatible with versions
+older than 2.4.0 which reject -P together with --restore.
+
+=cut
+sub get_setfacl_restore_command
+{
+my ($setfacl) = @_;
+state %physical_restore_cache;
+
+if (!exists($physical_restore_cache{$setfacl})) {
+ my $help = &backquote_command("$setfacl --help 2>&1 [1]->[2];
+ return $scalar->[1]->[2] // "";
}
elsif ($date) {
# Need to decode date
@@ -26,7 +26,7 @@ elsif ($date) {
}
elsif ($base64) {
# Convert to binary
- return &decode_base64($base64->[1]->[2]);
+ return &decode_base64($base64->[1]->[2] // "");
}
elsif ($struct) {
# Parse member names and values
@@ -35,7 +35,7 @@ elsif ($struct) {
my ($name) = &find_xmls("name", $member, 1);
my ($value) = &find_xmls("value", $member, 1);
my $perlv = &parse_xml_value($value);
- $rv{$name->[1]->[2]} = $perlv;
+ $rv{$name->[1]->[2] // ""} = $perlv;
}
return \%rv;
}
@@ -51,7 +51,7 @@ elsif ($array) {
}
else {
# Fallback - just a string directly in the value
- return $value->[1]->[2];
+ return $value->[1]->[2] // "";
}
}
@@ -60,6 +60,7 @@ else {
sub encode_xml_value
{
my ($perlv) = @_;
+$perlv = "" if (!defined($perlv));
if (ref($perlv) eq "ARRAY") {
# Convert to array XML format
my $xmlrv = "\n\n";