Compare commits

...

32 Commits

Author SHA1 Message Date
Ilia Ross
d2b4fa89c5 Fix to match short GPG key IDs to full fingerprints
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
https://forum.virtualmin.com/t/gpg-encryption-in-usermin/136729/32?u=ilia
2026-03-29 15:20:10 +02:00
Ilia Ross
3a3b202a96 Add safe explicit TLS fallback for FTP backups for fsdump module
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
https://github.com/webmin/webmin/pull/2646
2026-03-26 12:02:24 +02:00
Ilia Ross
30f08f73fb Fix to support newer GnuPG passphrase handling
* Note: Use loopback pinentry for decrypt operation and retry decryption with the discovered secret key's stored passphrase on newer GnuPG versions

https://forum.virtualmin.com/t/gpg-encryption-in-usermin/136729/26?u=ilia
2026-03-26 11:48:40 +02:00
Ilia Ross
e499b5b3a5 Fix to use loopback pinentry mode for GPG passphrase handling
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
https://forum.virtualmin.com/t/gpg-encryption-in-usermin/136729/19?u=ilia
2026-03-25 18:30:13 +02:00
Ilia Ross
443cf449eb Fix to use loopback pinentry for GPG decryption
https://forum.virtualmin.com/t/gpg-encryption-in-usermin/136729/19?u=ilia
2026-03-25 12:52:26 +02:00
Jamie Cameron
58d6308589 Properly check allowed directory paths
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-24 21:20:36 -07:00
Jamie Cameron
02ed8e8fbd New version bump
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-23 21:50:30 -07:00
Jamie Cameron
3a0dea1d2c Merge pull request #2648 from swelljoe/bind8-fix-warnings
Fix bind8 undefined warnings and sort/splice with non-numeric value
2026-03-23 20:27:08 -07:00
Joe Cooper
916d22b55b One more undefined 2026-03-23 22:22:31 -05:00
Joe Cooper
1f1a7e4562 Fix sort/splice bug 2026-03-23 21:54:18 -05:00
Joe Cooper
ac68a0be0c Fix bind8 undefined warnings 2026-03-23 21:36:29 -05:00
Ilia Ross
263cc142a6 Update changelog for 2.630
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-24 00:40:34 +02:00
Ilia Ross
19fdea395b Fix tab data escaping with proper JSON encoding
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
*Note: This is important if we want to support tags inside tab name, for example in case of showing a count of elements using HTML tag created with `ui_tag('tt')`
2026-03-21 21:00:10 +02:00
Ilia Ross
40f82d0df3 Revert "Fix to always avoid new lines inside the tag"
This reverts commit 39ab2c5f02.
2026-03-21 20:56:01 +02:00
Ilia Ross
39ab2c5f02 Fix to always avoid new lines inside the tag 2026-03-21 20:17:41 +02:00
Ilia Ross
f39a59bdce Fix to add module recommended packages
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-19 18:46:01 +02:00
Ilia Ross
a4846f5f32 Add an option to disable external programs from performing upgrades
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-17 20:11:36 +02:00
Ilia Ross
5558910722 Add API to activate and deactivate a service 2026-03-17 20:05:06 +02:00
Ilia Ross
bebd99d656 Add informational note about updates #2639
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-17 12:56:03 +02:00
Ilia Ross
bad0d2f821 Fix fields size
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
[no-build]
2026-03-16 11:22:13 +02:00
Jamie Cameron
4797852f6f Merge branch 'master' of github.com:webmin/webmin
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-15 12:26:50 -07:00
Jamie Cameron
d467810076 Fix layout of from address field
https://github.com/webmin/webmin/issues/2644
2026-03-15 12:26:43 -07:00
Ilia Ross
6d1ec1a3e1 Fix missing tags
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-15 16:28:38 +02:00
Jamie Cameron
82ea895c81 No need for PHP prefix since all we're installing is PHP
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
https://github.com/webmin/webmin/issues/2641
2026-03-14 17:43:06 -07:00
Jamie Cameron
36e699eb29 Merge branch 'master' of github.com:webmin/webmin 2026-03-14 17:37:14 -07:00
Jamie Cameron
04e8df863a Better button name
https://github.com/webmin/webmin/issues/2643
2026-03-14 17:36:59 -07:00
Ilia Ross
96c2312349 Fix to improve the name of the downloaded backup file #2570
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-14 13:19:05 +02:00
Jamie Cameron
1d594e82f0 Sometimes ntfs is in lower case
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
https://github.com/webmin/webmin/issues/2635
2026-03-11 22:24:06 -07:00
Jamie Cameron
cbd96a4176 Make code more readable 2026-03-11 16:43:46 -07:00
Ilia Ross
ed17ade510 Fix not to leak 2FA auth secret to logs
https://github.com/webmin/webmin/pull/2638

[no-build]
2026-03-12 01:17:42 +02:00
Ilia Ross
dc63aa22a5 Fix to build HTML nicely
[no-build]
2026-03-12 01:06:49 +02:00
Ilia Ross
1b9b9ae21f Fix to show test form for two-factor only when enrolling for yourself
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-03-10 23:31:47 +02:00
30 changed files with 307 additions and 98 deletions

View File

@@ -1,5 +1,15 @@
## Changelog ## Changelog
#### 2.630 (March 24, 2026)
* Add improvements to user input validation across all modules
* Update Authentic theme to the latest version with various improvements and fixes:
- Add a new airy button style to the light palette to match the dark one
- Fix to optimize stats server to reduce WebSocket memory usage
- Fix the real-time follow indicator when viewing the journal
- Fix regex-based match highlighting when viewing the journal
- Fix mail compose panel sizing in HTML mode on low-DPR screens
- Fix display of the 2FA QR code in the dark palette
#### 2.621 (January 25, 2026) #### 2.621 (January 25, 2026)
* Fix to prevent NAT from dropping idle RPC sessions during long transfers * Fix to prevent NAT from dropping idle RPC sessions during long transfers
* Fix to improve the message when socket authentication is used in the MySQL/MariaDB module * Fix to improve the message when socket authentication is used in the MySQL/MariaDB module

View File

@@ -66,14 +66,18 @@ if ($in{'enable'}) {
{ 'provider' => $user->{'twofactor_provider'}, { 'provider' => $user->{'twofactor_provider'},
'id' => $user->{'twofactor_id'} }); 'id' => $user->{'twofactor_id'} });
# Show a test form, so the user can validate # Show a test form only when enrolling for yourself
print &ui_form_start("test_twofactor.cgi"); if ($user->{'name'} eq $base_remote_user) {
print $text{'twofactor_testdesc'},"<p>\n"; print &ui_form_start("test_twofactor.cgi");
print "$text{'twofactor_testfield'}&nbsp;\n", print &ui_tag('p', $text{'twofactor_testdesc'});
&ui_textbox("test", undef, 12),"\n"; print &ui_tag('p', "$text{'twofactor_testfield'}".
print &ui_hidden("user", $in{'user'}) if ($in{'user'}); "&nbsp;&nbsp;".
print "<p>\n"; &ui_textbox("test", undef, 12));
print &ui_form_end([ [ undef, $text{'twofactor_test'} ] ]); print &ui_hidden("user", $in{'user'}) if ($in{'user'});
print &ui_tag('p');
print &ui_form_end([ [ undef,
$text{'twofactor_test'} ] ]);
}
} }
&ui_print_footer("", $text{'index_return'}); &ui_print_footer("", $text{'index_return'});

View File

@@ -60,12 +60,11 @@ print &ui_tabs_end_tab();
# Show immediate form # Show immediate form
print &ui_tabs_start_tab("tab", "backup"); print &ui_tabs_start_tab("tab", "backup");
my $filename = 'webmin-backup-config-on-'; my $hostname = &get_system_hostname() || "localhost";
my $hostname = &get_system_hostname();
$hostname =~ s/\./-/g; $hostname =~ s/\./-/g;
$filename .= $hostname; my $filename = $hostname."+configuration_backup-webmin-".
$filename .= "-".strftime("%Y-%m-%d-%H-%M", localtime); strftime("%Y-%m-%d-%H-%M", localtime);
print &ui_form_start("backup.cgi/$filename.tgz", "post"); print &ui_form_start("backup.cgi/$filename.tar.gz", "post");
print &ui_table_start($text{'index_header'}, undef, 2); print &ui_table_start($text{'index_header'}, undef, 2);
my @dmods = split(/\s+/, $config{'mods'} || ""); my @dmods = split(/\s+/, $config{'mods'} || "");

View File

@@ -700,7 +700,7 @@ if ($v && $v->{'members'}) {
push(@av, join(" ", $av->{'name'}, @{$av->{'values'}})); push(@av, join(" ", $av->{'name'}, @{$av->{'values'}}));
} }
} }
if ($_[3] == 0) { if (!$_[3]) {
# text area # text area
return &ui_table_row($_[0], return &ui_table_row($_[0],
&ui_textarea($_[1], join("\n", @av), 3, 50)); &ui_textarea($_[1], join("\n", @av), 3, 50));
@@ -805,14 +805,14 @@ my $v = &find($_[1], $_[2]);
my $n; my $n;
($n = $_[1]) =~ s/[^A-Za-z0-9_]/_/g; ($n = $_[1]) =~ s/[^A-Za-z0-9_]/_/g;
return &ui_table_row($_[0], return &ui_table_row($_[0],
&ui_opt_textbox($n, $v ? $v->{'value'} : "", $_[4], $_[3])." ".$_[5], &ui_opt_textbox($n, $v ? $v->{'value'} : "", $_[4], $_[3])." ".($_[5] // ""),
$_[4] > 30 ? 3 : 1); $_[4] > 30 ? 3 : 1);
} }
sub save_opt sub save_opt
{ {
my ($dir, $n, $err); my ($dir, $n, $err);
($n = $_[0]) =~ s/[^A-Za-z0-9_]/_/g; ($n = ($_[0] // "")) =~ s/[^A-Za-z0-9_]/_/g;
if ($in{"${n}_def"}) { &save_directive($_[2], $_[0], [ ], $_[3]); } if ($in{"${n}_def"}) { &save_directive($_[2], $_[0], [ ], $_[3]); }
elsif ($err = &{$_[1]}($in{$n})) { elsif ($err = &{$_[1]}($in{$n})) {
&error($err); &error($err);
@@ -906,7 +906,7 @@ my ($fwdconf, $fwdfile, $fwdrec, $ipv6);
# find forward domain # find forward domain
my $host = $_[0]; $host =~ s/\.$//; my $host = $_[0]; $host =~ s/\.$//;
my @zl = grep { $_->{'type'} ne 'view' } &list_zone_names(); my @zl = grep { $_->{'type'} ne 'view' } &list_zone_names();
if ($_[1] ne '' && $_[1] ne 'any') { if ($_[1] && $_[1] ne 'any') {
@zl = grep { $_->{'view'} && $_->{'viewindex'} == $_[1] } @zl; @zl = grep { $_->{'view'} && $_->{'viewindex'} == $_[1] } @zl;
} }
else { else {
@@ -968,6 +968,7 @@ else {
# Returns 1 if some zone can be edited # Returns 1 if some zone can be edited
sub can_edit_zone sub can_edit_zone
{ {
$access{'zones'} //= '*';
my %zcan; my %zcan;
my ($zn, $vn, $file); my ($zn, $vn, $file);
if ($_[0]->{'members'}) { if ($_[0]->{'members'}) {
@@ -2405,7 +2406,7 @@ return undef;
sub is_bind_running sub is_bind_running
{ {
my $pidfile = &get_pid_file(); my $pidfile = &get_pid_file();
my $rv = &check_pid_file(&make_chroot($pidfile, 1)); my $rv = &check_pid_file(&make_chroot($pidfile, 1)) || 0;
if (!$rv && $gconfig{'os_type'} eq 'windows') { if (!$rv && $gconfig{'os_type'} eq 'windows') {
# Fall back to checking for process # Fall back to checking for process
$rv = &find_byname("named"); $rv = &find_byname("named");
@@ -2536,6 +2537,7 @@ if ($changed || !$znc{'version'} ||
next if (!$type); next if (!$type);
$type = lc($type); $type = lc($type);
my $file = &find_value("file", $z->{'members'}); my $file = &find_value("file", $z->{'members'});
$file //= "";
my $up = &find("update-policy", $z->{'members'}); my $up = &find("update-policy", $z->{'members'});
my $au = &find("allow-update", $z->{'members'}); my $au = &find("allow-update", $z->{'members'});
my $dynamic = $up || $au || $gau ? 1 : 0; my $dynamic = $up || $au || $gau ? 1 : 0;
@@ -3198,6 +3200,7 @@ else {
$zonename = $zone->{'name'}; $zonename = $zone->{'name'};
$zonefile = $zone->{'file'}; $zonefile = $zone->{'file'};
} }
return () if (!$zonename || !$zonefile);
my $out = &backquote_command( my $out = &backquote_command(
$config{'checkzone'}." ".quotemeta($zonename)." ". $config{'checkzone'}." ".quotemeta($zonename)." ".
quotemeta(&make_chroot(&absolute_path($zonefile)))." 2>&1 </dev/null"); quotemeta(&make_chroot(&absolute_path($zonefile)))." 2>&1 </dev/null");
@@ -3221,6 +3224,7 @@ else {
$zonename = $zone->{'name'}; $zonename = $zone->{'name'};
$zonefile = $zone->{'file'}; $zonefile = $zone->{'file'};
} }
return () if (!$zonename || !$zonefile);
my $absfile = &make_chroot(&absolute_path($zonefile)); my $absfile = &make_chroot(&absolute_path($zonefile));
my $out = &backquote_command( my $out = &backquote_command(
$config{'checkzone'}." ".quotemeta($zonename)." ". $config{'checkzone'}." ".quotemeta($zonename)." ".
@@ -3363,7 +3367,7 @@ if (!$access{'ro'} && $access{'apply'}) {
if ($zone && ($access{'apply'} == 1 || $access{'apply'} == 2)) { if ($zone && ($access{'apply'} == 1 || $access{'apply'} == 2)) {
# Apply this zone # Apply this zone
my $link = "restart_zone.cgi?return=$r&". my $link = "restart_zone.cgi?return=$r&".
"view=$zone->{'viewindex'}&". "view=".($zone->{'viewindex'} // "")."&".
"zone=$zone->{'name'}"; "zone=$zone->{'name'}";
push(@rv, &ui_link($link, $text{'links_apply'}) ); push(@rv, &ui_link($link, $text{'links_apply'}) );
} }
@@ -3934,7 +3938,7 @@ if (&find_byname("nscd")) {
sub transfer_slave_records sub transfer_slave_records
{ {
my ($dom, $masters, $file, $source, $sourceport) = @_; my ($dom, $masters, $file, $source, $sourceport) = @_;
my $sourcearg; my $sourcearg = "";
if ($source && $source ne "*") { if ($source && $source ne "*") {
$sourcearg = "-t ".$source; $sourcearg = "-t ".$source;
if ($sourceport) { if ($sourceport) {

View File

@@ -28,12 +28,12 @@ for(my $i=0; $i<@servers; $i++) {
my @cols = ( ); my @cols = ( );
push(@cols, &ui_textbox("ip_$i", $s->{'value'}, 30)); push(@cols, &ui_textbox("ip_$i", $s->{'value'}, 30));
my $bogus = &find_value("bogus", $s->{'members'}); my $bogus = &find_value("bogus", $s->{'members'}) // "";
push(@cols, &ui_radio("bogus_$i", lc($bogus) eq 'yes' ? 1 : 0, push(@cols, &ui_radio("bogus_$i", lc($bogus) eq 'yes' ? 1 : 0,
[ [ 1, $text{'yes'} ], [ [ 1, $text{'yes'} ],
[ 0, $text{'no'} ] ])); [ 0, $text{'no'} ] ]));
my $format = &find_value("transfer-format", $s->{'members'}); my $format = &find_value("transfer-format", $s->{'members'}) // "";
push(@cols, &ui_radio("format_$i", lc($format), push(@cols, &ui_radio("format_$i", lc($format),
[ [ 'one-answer', $text{'servers_one'} ], [ [ 'one-answer', $text{'servers_one'} ],
[ 'many-answers', $text{'servers_many'} ], [ 'many-answers', $text{'servers_many'} ],

View File

@@ -45,7 +45,7 @@ else {
my %bumpedrev; my %bumpedrev;
my @delr; my @delr;
foreach my $d (sort { $b <=> $a } @d) { foreach my $d (sort { ($b =~ /^(\d+)/)[0] <=> ($a =~ /^(\d+)/)[0] } @d) {
my ($num, $id) = split(/\//, $d, 2); my ($num, $id) = split(/\//, $d, 2);
my $r = &find_record_by_id(\@recs, $id, $num); my $r = &find_record_by_id(\@recs, $id, $num);
next if (!$r); next if (!$r);
@@ -77,7 +77,7 @@ else {
# Delete the actual record # Delete the actual record
&lock_file(&make_chroot($r->{'file'})); &lock_file(&make_chroot($r->{'file'}));
&delete_record($r->{'file'}, $r); &delete_record($r->{'file'}, $r);
splice(@recs, $d, 1); splice(@recs, $num, 1);
push(@delr, $r); push(@delr, $r);
} }
&bump_soa_record($zone->{'file'}, \@recs); &bump_soa_record($zone->{'file'}, \@recs);

View File

@@ -172,6 +172,7 @@ for(my $i=0; $i<@_; $i++) {
else { else {
$name = $r->{'name'}; $name = $r->{'name'};
} }
$name //= "";
my @cols; my @cols;
$name = &html_escape($name); $name = &html_escape($name);
my $id = &record_id($r); my $id = &record_id($r);

View File

@@ -488,6 +488,7 @@ else {
} }
else { else {
# For other record types, just save the lines # For other record types, just save the lines
$in{'values'} //= "";
$in{'values'} =~ s/\r//g; $in{'values'} =~ s/\r//g;
my @vlines = split(/\n/, $in{'values'}); my @vlines = split(/\n/, $in{'values'});
$vals = join(" ",map { $_ =~ /\s|;/ ? "\"$_\"" : $_ } @vlines); $vals = join(" ",map { $_ =~ /\s|;/ ? "\"$_\"" : $_ } @vlines);

View File

@@ -918,7 +918,7 @@ if ($has_parted) {
elsif ($tag eq "linux-swap") { elsif ($tag eq "linux-swap") {
@rv = ( "swap" ); @rv = ( "swap" );
} }
elsif ($tag eq "NTFS") { elsif ($tag eq "NTFS" || $tag eq "ntfs") {
@rv = ( "ntfs" ); @rv = ( "ntfs" );
} }
elsif ($tag eq "reiserfs") { elsif ($tag eq "reiserfs") {

View File

@@ -4,6 +4,16 @@
$no_acl_check++; $no_acl_check++;
require './fsdump-lib.pl'; require './fsdump-lib.pl';
sub start_tls
{
my ($fh, $what) = @_;
eval { require IO::Socket::SSL; IO::Socket::SSL->import(); 1; } ||
&error_exit("FTP server requires TLS, but IO::Socket::SSL is not installed");
IO::Socket::SSL->start_SSL($fh, SSL_verify_mode => 0) ||
&error_exit("FTP $what TLS handshake failed : ".
IO::Socket::SSL::errstr());
}
# Parse args, and get password # Parse args, and get password
select(STDERR); $| = 1; select(STDOUT); select(STDERR); $| = 1; select(STDOUT);
$host = $ARGV[0]; $host = $ARGV[0];
@@ -36,6 +46,15 @@ while(1) {
&error_exit("FTP connection failed : $err") if ($err); &error_exit("FTP connection failed : $err") if ($err);
&ftp_command("", 2, \$err) || &ftp_command("", 2, \$err) ||
&error_exit("FTP prompt failed : $err"); &error_exit("FTP prompt failed : $err");
$ssl_enabled = 0;
if (&ftp_command("AUTH TLS", 2, \$err)) {
&start_tls(\*SOCK, "control");
&ftp_command("PBSZ 0", 2, \$err) ||
&error_exit("FTP TLS setup failed : $err");
&ftp_command("PROT P", 2, \$err) ||
&error_exit("FTP TLS setup failed : $err");
$ssl_enabled = 1;
}
# Login to server # Login to server
@urv = &ftp_command("USER $user", [ 2, 3 ], \$err); @urv = &ftp_command("USER $user", [ 2, 3 ], \$err);
@@ -174,5 +193,8 @@ elsif ($mode == 2) {
else { else {
$opened = 0; $opened = 0;
} }
if ($opened && $ssl_enabled) {
&start_tls(\*CON, "data");
}
} }

View File

@@ -430,8 +430,8 @@ if (!$main::ui_hidden_start_donejs++) {
} }
# Build list of tab titles and names # Build list of tab titles and names
my $tabnames = "[".join(",", map { "\"".&quote_escape($_->[0])."\"" } @$tabs)."]"; my $tabnames = &convert_to_json([map { $_->[0] } @$tabs]);
my $tabtitles = "[".join(",", map { "\"".&quote_escape($_->[1])."\"" } @$tabs)."]"; my $tabtitles = &convert_to_json([map { $_->[1] } @$tabs]);
$rv .= "<script>\n"; $rv .= "<script>\n";
$rv .= "document.${name}_tabnames = $tabnames;\n"; $rv .= "document.${name}_tabnames = $tabnames;\n";
$rv .= "document.${name}_tabtitles = $tabtitles;\n"; $rv .= "document.${name}_tabtitles = $tabtitles;\n";
@@ -649,7 +649,7 @@ if (!$main::WRAPPER_OPEN) { # If we're not already inside of a wrapper, wrap it
} }
$main::WRAPPER_OPEN++; $main::WRAPPER_OPEN++;
my $colspan = 1; my $colspan = 1;
$rv .= "<details class='ui_hidden_table_start'$opened>"; $rv .= "<details data-name='$name' class='ui_hidden_table_start'$opened $tabletags>";
$rv .= "<summary>$header $rheader</summary>\n"; $rv .= "<summary>$header $rheader</summary>\n";
$rv .= "<table width=100%>\n"; $rv .= "<table width=100%>\n";
$main::ui_table_cols = $cols || 4; $main::ui_table_cols = $cols || 4;

View File

@@ -1175,6 +1175,47 @@ elsif ($init_mode eq "launchd") {
} }
} }
=head2 activate_action(name)
Unmasks some action, enables it at boot time, and starts it if not running.
Returns 1 if the action exists, 0 if not.
=cut
sub activate_action
{
my ($name) = @_;
my $st = &action_status($name);
return 0 if (!$st);
&unmask_action($name);
&enable_at_boot($name);
my $running = &status_action($name);
if ($running != 1) { # unknown or stopped
&start_action($name);
}
return 1;
}
=head2 deactivate_action(name, [mask])
Stops some action if currently running, disables it at boot time, and masks it
on systemd systems. The optional mask flag can be set to 0 to skip masking.
Returns 1 if the action exists, 0 if not.
=cut
sub deactivate_action
{
my ($name, $mask) = @_;
my $st = &action_status($name);
return 0 if (!$st);
my $running = &status_action($name);
if ($running != 0) { # unknown or running
&stop_action($name);
}
&disable_at_boot($name);
&mask_action($name) if (!defined($mask) || $mask);
return 1;
}
=head2 delete_at_boot(name) =head2 delete_at_boot(name)
Delete the init script, RC script or whatever with some name Delete the init script, RC script or whatever with some name

View File

@@ -221,12 +221,23 @@ else {
print &ui_table_row($text{'index_email'}, $efield); print &ui_table_row($text{'index_email'}, $efield);
# Install or just notify? # Install or just notify?
print &ui_table_row($text{'index_action'}, $action_ui = &ui_select("action", int($config{'sched_action'}),
&ui_radio("action", int($config{'sched_action'}), [ [ -1, $text{'index_action-1'} ],
[ [ -1, $text{'index_action-1'} ], [ 0, $text{'index_action0'} ],
[ 0, $text{'index_action0'} ], [ 1, $text{'index_action1'} ],
[ 1, $text{'index_action1'} ], [ 2, $text{'index_action2'} ] ]);
[ 2, $text{'index_action2'} ] ])); if (my @auto_updates = &list_enabled_auto_update_services()) {
# If any auto-update services are enabled, show option to disable them
$auto_update_names = join(", ", map { $_->{'name'} } @auto_updates);
$action_ui .= " ".
&ui_checkbox("disable_auto_updates", 1,
&text('index_action_disable',
$auto_update_names), 0);
$action_ui .= "<br>\n".
&ui_note(&text('index_action_note',
&ui_tag('tt', $auto_update_names)));
}
print &ui_table_row($text{'index_action'}, $action_ui);
print &ui_table_end(); print &ui_table_end();
print &ui_form_end([ [ "save", $text{'save'} ] ]); print &ui_form_end([ [ "save", $text{'save'} ] ]);

View File

@@ -24,6 +24,8 @@ index_action-1=Just notify for security updates
index_action0=Just notify for any updates index_action0=Just notify for any updates
index_action1=Install security updates index_action1=Install security updates
index_action2=Install any updates index_action2=Install any updates
index_action_note=Updates may also be installed by $1; disable external updates if you want Webmin to handle updates exclusively
index_action_disable=Disable external updates
index_err=Failed to fetch package list index_err=Failed to fetch package list
index_refresh=Refresh Available Packages index_refresh=Refresh Available Packages
index_noupdate=No update exists from version $1 index_noupdate=No update exists from version $1

View File

@@ -8,6 +8,16 @@ eval "use WebminCore;";
&foreign_require("cron", "cron-lib.pl"); &foreign_require("cron", "cron-lib.pl");
&foreign_require("webmin", "webmin-lib.pl"); &foreign_require("webmin", "webmin-lib.pl");
# Known OS package auto-update services that can overlap with Webmin's
# scheduled package updates.
@auto_update_services = (
"unattended-upgrades",
"dnf-automatic.timer",
"dnf-automatic-install.timer",
"dnf-automatic-download.timer",
"dnf-automatic-notifyonly.timer",
);
$available_cache_file = &cache_file_path("available.cache"); $available_cache_file = &cache_file_path("available.cache");
$current_cache_file = &cache_file_path("current.cache"); $current_cache_file = &cache_file_path("current.cache");
$updates_cache_file = &cache_file_path("updates.cache"); $updates_cache_file = &cache_file_path("updates.cache");
@@ -19,6 +29,36 @@ $yum_changelog_cache_dir = &cache_file_path("yumchangelog");
$update_progress_dir = "$module_var_directory/progress"; $update_progress_dir = "$module_var_directory/progress";
# list_enabled_auto_update_services()
# Returns known OS-level auto-update services that are currently enabled.
sub list_enabled_auto_update_services
{
return ( ) if (!&foreign_check("init"));
&foreign_require("init");
my @rv;
foreach my $service (@auto_update_services) {
next if (&init::action_status($service) != 2); # not enabled
my $service_name = $service;
$service_name =~ s/\.[^\.]+$//; # nice name
push(@rv, { 'service' => $service, 'name' => $service_name });
}
return @rv;
}
# disable_enabled_auto_update_services()
# Stops, disables and masks known auto-update services that are enabled.
sub disable_enabled_auto_update_services
{
return ( ) if (!&foreign_check("init"));
&foreign_require("init");
my @services = map { $_->{'service'} } &list_enabled_auto_update_services();
my @rv;
foreach my $service (@services) {
push(@rv, $service) if (&init::deactivate_action($service, 0));
}
return @rv;
}
# cache_file_path(name) # cache_file_path(name)
# Returns a path in the /var directory unless the file already exists under # Returns a path in the /var directory unless the file already exists under
# /etc/webmin # /etc/webmin

View File

@@ -51,6 +51,11 @@ else {
$msg = $text{'sched_yes'}; $msg = $text{'sched_yes'};
} }
# Disable auto-update services if requested
if ($in{'disable_auto_updates'}) {
&disable_enabled_auto_update_services();
}
# Tell the user # Tell the user
&ui_print_header(undef, $text{'sched_title'}, ""); &ui_print_header(undef, $text{'sched_title'}, "");
@@ -59,4 +64,3 @@ print "$msg<p>\n";
&webmin_log("sched", undef, $in{'sched_def'} ? 0 : 1); &webmin_log("sched", undef, $in{'sched_def'} ? 0 : 1);
&ui_print_footer("index.cgi?mode=$in{'mode'}&search=". &ui_print_footer("index.cgi?mode=$in{'mode'}&search=".
&urlize($in{'search'}), $text{'index_return'}); &urlize($in{'search'}), $text{'index_return'});

View File

@@ -9,7 +9,7 @@ index_medit=Manage
index_manual=Edit Manually index_manual=Edit Manually
index_anyfile=Edit other PHP configuration file index_anyfile=Edit other PHP configuration file
index_return=configuration files index_return=configuration files
index_pkgs=Manage PHP Packages index_pkgs=Manage PHP Versions
index_pkgsdesc=Install and remove PHP versions from your system's software package repository, so that they can be configured here any used in Virtualmin. index_pkgsdesc=Install and remove PHP versions from your system's software package repository, so that they can be configured here any used in Virtualmin.
file_global=Global PHP configuration file_global=Global PHP configuration

View File

@@ -61,7 +61,7 @@ if (&foreign_installed("package-updates")) {
my @allpkgs = &extend_installable_php_packages(\@newpkgs); my @allpkgs = &extend_installable_php_packages(\@newpkgs);
@allpkgs = sort { $b->{'ver'} cmp $a->{'ver'} } @allpkgs; @allpkgs = sort { $b->{'ver'} cmp $a->{'ver'} } @allpkgs;
print &ui_select("u", undef, print &ui_select("u", undef,
[ map { [ $_->{'name'}, "PHP $_->{'ver'}" ] } @allpkgs ]); [ map { [ $_->{'name'}, $_->{'ver'} ] } @allpkgs ]);
print &ui_hidden( print &ui_hidden(
"redir", &get_webprefix()."/$module_name/list_pkgs.cgi"); "redir", &get_webprefix()."/$module_name/list_pkgs.cgi");
print &ui_hidden("redirdesc", $text{'pkgs_title'}); print &ui_hidden("redirdesc", $text{'pkgs_title'});

View File

@@ -4,3 +4,5 @@ name=PostgreSQL
longdesc=Manage databases, tables and users in your PostgreSQL database server. longdesc=Manage databases, tables and users in your PostgreSQL database server.
readonly=1 readonly=1
cpan=1 cpan=1
rpm_recommends=perl-DBI perl-DBD-Pg
deb_recommends=libdbi-perl libdbd-pg-perl

View File

@@ -6,7 +6,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
-r 'qmail-lib.pl' ? './qmail-lib.pl' : -r 'qmail-lib.pl' ? './qmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParse(); &ReadParse();
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('afile_efile', $in{'file'})); &error(&text('afile_efile', $in{'file'}));
} }

View File

@@ -6,7 +6,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
-r 'qmail-lib.pl' ? './qmail-lib.pl' : -r 'qmail-lib.pl' ? './qmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParse(); &ReadParse();
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('ffile_efile', $in{'file'})); &error(&text('ffile_efile', $in{'file'}));
} }

View File

@@ -6,7 +6,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
-r 'qmail-lib.pl' ? './qmail-lib.pl' : -r 'qmail-lib.pl' ? './qmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParse(); &ReadParse();
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('rfile_efile', $in{'file'})); &error(&text('rfile_efile', $in{'file'}));
} }

View File

@@ -6,7 +6,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
-r 'qmail-lib.pl' ? './qmail-lib.pl' : -r 'qmail-lib.pl' ? './qmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParseMime(); &ReadParseMime();
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('afile_efile', $in{'file'})); &error(&text('afile_efile', $in{'file'}));
} }

View File

@@ -7,7 +7,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParseMime(); &ReadParseMime();
&error_setup($text{'ffile_err'}); &error_setup($text{'ffile_err'});
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('ffile_efile', $in{'file'})); &error(&text('ffile_efile', $in{'file'}));
} }

View File

@@ -6,7 +6,7 @@ require (-r 'sendmail-lib.pl' ? './sendmail-lib.pl' :
-r 'qmail-lib.pl' ? './qmail-lib.pl' : -r 'qmail-lib.pl' ? './qmail-lib.pl' :
'./postfix-lib.pl'); './postfix-lib.pl');
&ReadParseMime(); &ReadParseMime();
if (substr($in{'file'}, 0, length($access{'apath'})) ne $access{'apath'}) { if (!&is_under_directory($access{'apath'}, $in{'file'})) {
&error(&text('rfile_efile', $in{'file'})); &error(&text('rfile_efile', $in{'file'}));
} }
$in{'replies_def'} || $in{'replies'} =~ /^\/\S+/ || $in{'replies_def'} || $in{'replies'} =~ /^\/\S+/ ||

View File

@@ -2114,8 +2114,8 @@ if (!$main::ui_hidden_start_donejs++) {
} }
# Build list of tab titles and names # Build list of tab titles and names
my $tabnames = "[".join(",", map { "\"".&quote_escape($_->[0])."\"" } @$tabs)."]"; my $tabnames = &convert_to_json([map { $_->[0] } @$tabs]);
my $tabtitles = "[".join(",", map { "\"".&quote_escape($_->[1])."\"" } @$tabs)."]"; my $tabtitles = &convert_to_json([map { $_->[1] } @$tabs]);
$rv .= "<script type='text/javascript'>\n"; $rv .= "<script type='text/javascript'>\n";
$rv .= "document.${name}_tabnames = $tabnames;\n"; $rv .= "document.${name}_tabnames = $tabnames;\n";
$rv .= "document.${name}_tabtitles = $tabtitles;\n"; $rv .= "document.${name}_tabtitles = $tabtitles;\n";

View File

@@ -1 +1 @@
2.621 2.630

View File

@@ -47,9 +47,9 @@ print &ui_table_row($text{'sendmail_login'},
&ui_radio("login_def", $user ? 0 : 1, &ui_radio("login_def", $user ? 0 : 1,
[ [ 1, $text{'sendmail_login1'}."<br>" ], [ [ 1, $text{'sendmail_login1'}."<br>" ],
[ 0, $text{'sendmail_login0'} ] ])." ". [ 0, $text{'sendmail_login0'} ] ])." ".
&ui_textbox("login_user", $user, 20)." ". &ui_textbox("login_user", $user, 12)."&nbsp;".
$text{'sendmail_pass'}." ". $text{'sendmail_pass'}."&nbsp;&nbsp;".
&ui_textbox("login_pass", $pass, 20)); &ui_textbox("login_pass", $pass, 12));
# Authentication method # Authentication method
$auth = $mconfig{'smtp_auth'}; $auth = $mconfig{'smtp_auth'};
@@ -62,11 +62,12 @@ print &ui_table_row($text{'sendmail_auth'},
$from = $mconfig{'webmin_from'}; $from = $mconfig{'webmin_from'};
$fromdef = "webmin-noreply\@".&mailboxes::get_from_domain(); $fromdef = "webmin-noreply\@".&mailboxes::get_from_domain();
print &ui_table_row($text{'sendmail_from'}, print &ui_table_row($text{'sendmail_from'},
&ui_opt_textbox("from", $from, 40, &ui_radio_table("from_def", $from ? 0 : 1,
&text('sendmail_fromdef', $fromdef)."<br>", [ [ 1, "", &text('sendmail_fromdef', $fromdef) ],
$text{'sendmail_fromaddr'})." ". [ 0, "", $text{'sendmail_fromaddr'}." ".
$text{'sendmail_name'}." ". &ui_textbox("from", $from, 40)."<br>\n".
&ui_textbox("from_name", $mconfig{'webmin_from_name'}, 30), 3); $text{'sendmail_name'}." ".
&ui_textbox("from_name", $mconfig{'webmin_from_name'}, 30) ] ]), 3);
# Default to address for notifications # Default to address for notifications
$to = $gconfig{'webmin_email_to'}; $to = $gconfig{'webmin_email_to'};

View File

@@ -93,6 +93,19 @@ sub list_secret_keys
return grep { $_->{'secret'} } &list_keys(); return grep { $_->{'secret'} } &list_keys();
} }
# key_matches_id(id, &key)
# Returns 1 if some GnuPG key ID or fingerprint refers to the given key
sub key_matches_id
{
my ($id, $key) = @_;
$id = lc($id);
return 1 if (lc($key->{'key'}) eq $id || lc($key->{'key'}) =~ /\Q$id\E$/);
foreach my $key2 (@{$key->{'key2'}}) {
return 1 if (lc($key2) eq $id || lc($key2) =~ /\Q$id\E$/);
}
return 0;
}
# key_fingerprint(&key) # key_fingerprint(&key)
sub key_fingerprint sub key_fingerprint
{ {
@@ -206,46 +219,66 @@ if ($key) {
$pflag = "--batch --passphrase-file ". $pflag = "--batch --passphrase-file ".
quotemeta(&get_passphrase_file($key)); quotemeta(&get_passphrase_file($key));
} }
my $cmd = "$gpgpath $pflag --output ".quotemeta($dstfile).
" --decrypt ".quotemeta($srcfile);
my ($fh, $fpid) = &proc::pty_process_exec($cmd);
my ($error, $seen_pass, $keyid); my ($error, $seen_pass, $keyid);
$wait_for_debug = 1; my $retry = 0;
while(1) { while(1) {
my $rv = &wait_for($fh, "passphrase:", "key,\\s+ID\\s+(\\S+),", "failed.*\\n", "error.*\\n", "invalid.*\\n", "signal caught.*\\n"); unlink($dstfile);
if ($rv == 0) { my $cmd = "$gpgpath --pinentry-mode loopback $pflag".
# Only needed if caller didn't supply a key with passphrase " --output ".quotemeta($dstfile).
last if ($seen_pass++); " --decrypt ".quotemeta($srcfile);
sleep(1); my ($fh, $fpid) = &proc::pty_process_exec($cmd);
syswrite($fh, "$pass\n", length("$pass\n")); my $rerun = 0;
} $error = $seen_pass = $keyid = undef;
elsif ($rv == 1) { while(1) {
# Only needed if caller didn't supply a key my $rv = &wait_for($fh, "passphrase:", "key,\\s+ID\\s+(\\S+),", "failed.*\\n", "error.*\\n", "invalid.*\\n", "signal caught.*\\n");
$keyid = $matches[1]; if ($rv == 0) {
my $rkey; # Only needed if caller didn't supply a key with passphrase
($rkey) = grep { &indexof($matches[1], @{$_->{'key2'}}) >= 0 || last if ($seen_pass++);
$_->{'key'} eq $matches[1] } sleep(1);
&list_secret_keys(); syswrite($fh, "$pass\n", length("$pass\n"));
if ($rkey && $key) {
# Does discovered key match?
return &text('gnupg_ecryptkey2', "<tt>$keyid</tt>")
if ($rkey->{'key'} ne $key->{'key'});
} }
elsif ($rkey && !$key) { elsif ($rv == 1) {
# Discovered the key to use # Only needed if caller didn't supply a key
$pass = &get_passphrase($rkey); $keyid = $matches[1];
$key = $rkey; my $rkey;
($rkey) = grep { &key_matches_id($matches[1], $_) }
&list_secret_keys();
if ($rkey && $key) {
# Does discovered key match?
return &text('gnupg_ecryptkey2', "<tt>$keyid</tt>")
if (!&key_matches_id($keyid, $key));
}
elsif ($rkey && !$key) {
# Discovered the key to use. Retry with a stored
# passphrase file for newer GnuPG versions
$pass = &get_passphrase($rkey);
$key = $rkey;
if (defined($pass) && !$retry++) {
$pflag = "--batch --passphrase-file ".
quotemeta(
&get_passphrase_file(
$key));
$rerun++;
last;
}
}
}
elsif ($rv > 1) {
$error++;
last;
}
elsif ($rv < 0) {
last;
} }
} }
elsif ($rv > 1) { if ($rerun) {
$error++; kill('TERM', $fpid);
last; close($fh);
} next;
elsif ($rv < 0) {
last;
} }
close($fh);
last;
} }
close($fh);
&reset_environment(); &reset_environment();
unlink($srcfile); unlink($srcfile);
my $dst = &read_file_contents($dstfile); my $dst = &read_file_contents($dstfile);
@@ -283,7 +316,8 @@ if (!defined($pass)) {
return $text{'gnupg_esignpass'}.". ". return $text{'gnupg_esignpass'}.". ".
&text('gnupg_canset', "/gnupg/edit_key.cgi?key=$key->{'key'}")."."; &text('gnupg_canset', "/gnupg/edit_key.cgi?key=$key->{'key'}").".";
} }
my $pflag = "--batch --passphrase-file ".quotemeta(&get_passphrase_file($key)); my $pflag = "--batch --pinentry-mode loopback --passphrase-file ".
quotemeta(&get_passphrase_file($key));
my $cmd; my $cmd;
if ($mode == 0) { if ($mode == 0) {
$cmd = "$gpgpath $pflag --output ".quotemeta($dstfile)." --default-key $key->{'key'} --sign ".quotemeta($srcfile); $cmd = "$gpgpath $pflag --output ".quotemeta($dstfile)." --default-key $key->{'key'} --sign ".quotemeta($srcfile);

View File

@@ -224,24 +224,57 @@ sub message_twofactor_totp
my ($user) = @_; my ($user) = @_;
my $name = &get_display_hostname()." (".$user->{'name'}.")"; my $name = &get_display_hostname()." (".$user->{'name'}.")";
my $str = "otpauth://totp/".$name."?secret=".$user->{'twofactor_id'}; my $str = "otpauth://totp/".$name."?secret=".$user->{'twofactor_id'};
my $url; my $qrcode = &ui_tag('p',
&text('twofactor_qrcode', "<tt>$user->{'twofactor_id'}</tt>"));
if (&can_generate_qr()) { if (&can_generate_qr()) {
my $url;
if (&get_product_name() eq 'usermin') { if (&get_product_name() eq 'usermin') {
$url = "qr.cgi?size=6&str=".&urlize($str); $url = "qr.cgi?size=6";
} }
else { else {
$url = "$gconfig{'webprefix'}/webmin/qr.cgi?". $url = "$gconfig{'webprefix'}/webmin/qr.cgi?size=6";
"size=6&str=".&urlize($str);
} }
my $id = "twofactor_qr_".int(time())."_".int(rand(1000000));
my $img = &ui_tag('img', undef,
{ 'id' => $id, 'border' => 0,
'style' => 'width:210px; height:210px; '.
'border:1px solid #444;',
'alt' => 'QR code' });
my $id_js = &quote_javascript($id);
my $url_js = &quote_javascript($url);
my $str_js = &quote_javascript($str);
return <<EOF;
$qrcode$img
<script>
(function() {
const img = document.getElementById("$id_js"),
body = "str=" + encodeURIComponent("$str_js");
fetch("$url_js", {
method: "POST",
body: body
}).then(function(response) {
if (!response.ok) { return null; }
return response.blob();
}).then(function(blob) {
if (!blob) { return; }
const reader = new FileReader();
reader.onloadend = function() { img.src = reader.result; };
reader.readAsDataURL(blob);
}).catch(function() { });
})();
</script>
<p>
EOF
} }
else { else {
$url = "https://api.qrserver.com/v1/create-qr-code/?". my $url = "https://api.qrserver.com/v1/create-qr-code/?".
"size=200x200&data=".&urlize($str); "size=200x200&data=".&urlize($str);
my $img = &ui_tag('img', undef,
{ 'src' => $url, 'border' => 0, 'alt' => 'QR code' });
return <<EOF;
$qrcode$img<p>
EOF
} }
my $rv;
$rv .= &text('twofactor_qrcode', "<tt>$user->{'twofactor_id'}</tt>")."<p>\n";
$rv .= "<img src='$url' border=0><p>\n";
return $rv;
} }
# validate_twofactor_totp(id, token) # validate_twofactor_totp(id, token)