Compare commits

..

7 Commits

Author SHA1 Message Date
Ilia Ross
925af91ce8 Fix PostgreSQL restricted mode version check 2026-10-16 00:26:46 +02:00
Ilia Ross
e3e6a9b44f Fix PostgreSQL error detection across locales 2026-10-15 21:00:54 +02:00
Ilia Ross
29cf93863d Fix PostgreSQL execution failure reporting 2026-10-15 19:59:39 +02:00
Ilia Ross
b502deaddf Fix to preserve PostgreSQL SQL file error handling 2026-10-15 19:58:30 +02:00
Ilia Ross
0d4c4b7582 Fix unsupported PostgreSQL client handling 2026-10-15 19:55:39 +02:00
Ilia Ross
a272f94d93 Fix safe PostgreSQL dump execution 2026-10-15 18:45:45 +02:00
Ilia Ross
9026459a53 Fix unsafe PostgreSQL command execution 2026-10-15 18:44:33 +02:00
188 changed files with 956 additions and 11757 deletions

View File

@@ -1,26 +1,19 @@
## Changelog
#### 2.680 (October 23, 2026)
* Add an SQLite database browser and editor with schema inspection, table management, row editing and direct SQL execution [forum.virtualmin.com/t/137508](https://forum.virtualmin.com/t/sqlite-database-browser-and-editor/137508/)
#### 2.680 (October, 2026)
* Add color schemes to the Terminal module
* Add per-user Terminal preferences for color scheme, font size, screen reader mode and terminal dimensions
* Add protection against infinite loops resolving recursive symlinks (thanks to Carl Zuzart, Zedden I.T.)
* Add a Cloudmin virtualization host profile to the nftables module
* Add drop-down menu, dialog, breadcrumb and page link widgets to the UI library, built on native `details` and `dialog` elements
* Add confirmation dialogs for buttons and links carrying `data-ui-confirm`, replacing the browser's own prompt
* Update the Framed Theme with many visual and usability improvements
* Update File Manager to use the new UI widgets in the standard theme, dropping legacy templates
* Fix standard theme sortable tables dropping controls from table headings, such as the select-all checkbox in File Manager
* Fix package installation with DNF running `update` instead of `install` when one of several requested packages is already installed, leaving the others uninstalled
* Fix two-factor authentication validation to use the Webmin configuration and data directories passed by the caller
* Fix HTML escaping of module, script and session fields on the Webmin Actions Log details page
* Fix mailbox attachment option injection
* Fix archive extraction bypassing Upload and Download directory restrictions
* Fix path traversal and command injection in parameterized Custom Commands file editors
* Fix MySQL/MariaDB manual editor ACL bypass
* Fix MySQL/MariaDB database backups allowing restricted users to write files or save root-run settings outside their access limits
* Fix MySQL/MariaDB imports to use server-accessible temporary files with restricted permissions, including when systemd isolates temporary directories or Webmin connects through a loopback address
* Fix the logged in users list in Users and Groups module showing nobody when `who` prints ISO dates under a non-C locale or `sshd pts/N` lines from the logind backend [forum.virtualmin.com/t/138162](https://forum.virtualmin.com/t/138162)
* Fix missing SSH logins in the Users and Groups module on Ubuntu 26.04 by using systemd-logind when `utmp` is absent
* Fix mail date parsing of ISO dates like `2026-10-15 02:33` returning the wrong minutes
* Add optional address-family selection to HTTP downloads and socket connections, preserved across redirects [#1300](https://github.com/virtualmin/virtualmin-gpl/issues/1300)
* Update the Authentic theme to the latest version with various improvements:

File diff suppressed because one or more lines are too long

View File

@@ -142,8 +142,6 @@ while(1) {
$rawarg .= $got;
}
print STDERR "fastrpc[$$]: raw $rawarg\n" if ($gconfig{'rpcdebug'});
# Mirror the request's wire format in the response
my $dumper = substr($rawarg, 0, 5) eq '$VAR1' ? 1 : 0;
my $arg = &unserialise_variable($rawarg);
@@ -213,12 +211,6 @@ while(1) {
elsif ($use_ipv6 && vec($rmask, fileno($tsock6), 1)) {
accept(TRANS, $tsock6) || exit;
}
if ($arg->{'xferauth'}) {
my $auth = <TRANS>;
exit if (!defined($auth));
chomp($auth);
exit if ($auth ne $sid);
}
print STDERR "fastrpc[$$]: tcpwrite $file accepted\n" if ($gconfig{'rpcdebug'});
my $buf;
my $err;
@@ -248,9 +240,7 @@ while(1) {
$xfer_kids{$cpid} = 1;
print STDERR "fastrpc[$$]: tcpwrite $file started\n"
if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => [ $file, $tport,
$arg->{'xferauth'} ? $sid : undef ] };
$rv = { 'status' => 1, 'rv' => [ $file, $tport ] };
}
close($tsock);
close($tsock6) if ($use_ipv6);
@@ -305,12 +295,6 @@ while(1) {
elsif (vec($rmask, fileno($tsock6), 1)) {
accept(TRANS, $tsock6) || exit;
}
if ($arg->{'xferauth'}) {
my $auth = <TRANS>;
exit if (!defined($auth));
chomp($auth);
exit if ($auth ne $sid);
}
my $buf;
while(read(FILE, $buf, 1024) > 0) {
print TRANS $buf;
@@ -325,8 +309,7 @@ while(1) {
"started\n"
if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => [ $arg->{'file'}, $tport,
$arg->{'xferauth'} ? $sid : undef ] };
'rv' => [ $arg->{'file'}, $tport ] };
}
close(FILE);
close($tsock);

View File

@@ -40,17 +40,11 @@ else {
}
$| = 1;
# list_disks_partitions([include-cds], [disk-only], [error-ref])
# Returns a list of disk hash refs; disk-only mode omits partition metadata.
# Failure returns an empty list, never an error string. The optional error-ref
# is a scalar reference that receives the reason without changing the return type.
# Example: my @disks = list_disks_partitions(undef, 1, \$error);
# list_disks_partitions([include-cds])
# Returns a structure containing the details of all disks and partitions
sub list_disks_partitions
{
local ($include_cds, $disk_only) = @_;
my $error_ref = $_[2];
$$error_ref = undef if ($disk_only && $error_ref);
if (!$disk_only && scalar(@list_disks_partitions_cache)) {
if (scalar(@list_disks_partitions_cache)) {
return @list_disks_partitions_cache;
}
@@ -128,13 +122,10 @@ else {
}
local (@disks, @devs, $d);
my $discovery_partitions = '';
if (open(PARTS, "</proc/partitions")) {
# The list of all disks can come from the kernel
local $sc = 0;
while(<PARTS>) {
$discovery_partitions .= $_ if ($disk_only);
if (/\d+\s+\d+\s+\d+\s+sd([a-z]+)\s/ ||
/\d+\s+\d+\s+\d+\s+(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)\s+/) {
# New or old style SCSI device
@@ -166,7 +157,7 @@ if (open(PARTS, "</proc/partitions")) {
if (open(MEDIA, "</proc/ide/hd$n/media")) {
local $media = <MEDIA>;
close(MEDIA);
if ($media =~ /^disk/ && !$include_cds) {
if ($media =~ /^disk/ && !$_[0]) {
push(@devs, "/dev/hd$n");
}
}
@@ -222,36 +213,17 @@ if (open(PARTS, "</proc/partitions")) {
@devs = sort { ($b =~ /\/hd[a-z]+$/ ? 1 : 0) <=>
($a =~ /\/hd[a-z]+$/ ? 1 : 0) } @devs;
}
elsif ($disk_only) {
return &disk_discovery_error("Cannot open disk inventory for disk-only discovery", $error_ref);
}
if ($disk_only) {
return &disk_discovery_error("Invalid disk inventory for disk-only discovery", $error_ref)
if ($discovery_partitions !~ /major\s+minor\s+#blocks\s+name/);
return &disk_discovery_error("Disk inventory changed during disk-only discovery", $error_ref)
if ($discovery_partitions ne &read_file_contents('/proc/partitions'));
}
return ( ) if (!@devs); # No disks, ie on Xen
# Skip cd-rom drive, identified from symlink. Don't do this if we can identify
# cds by their media type though
if (!-d "/proc/ide") {
local @cdstat = stat("/dev/cdrom");
if (@cdstat && !$include_cds) {
if (@cdstat && !$_[0]) {
@devs = grep { (stat($_))[1] != $cdstat[1] } @devs;
}
}
my %discovery_identity;
if ($disk_only) {
foreach my $device (@devs) {
my $identity = &disk_discovery_identity($device);
return &disk_discovery_error("Missing device during disk-only discovery: $device", $error_ref)
if (!defined($identity));
$discovery_identity{$device} = $identity;
}
}
# Get Linux disk ID mapping
local %id_map;
local %all_id_map;
@@ -268,246 +240,6 @@ foreach my $id (readdir(IDS)) {
}
closedir(IDS);
# Both paths use the same classification, ID and model logic.
my $set_metadata = sub {
my ($disk) = @_;
if ($disk->{'device'} =~ /\/sd([a-z]+)$/) {
# Old-style SCSI disk
$disk->{'desc'} = &text('select_device', 'SCSI',
uc($1));
local ($dscsi) = grep { $_->{'dev'} eq "sd$1" } @dscsi;
$disk->{'scsi'} = $dscsi ? &indexof($dscsi, @dscsi)
: ord(uc($1))-65;
$disk->{'type'} = 'scsi';
}
elsif ($disk->{'device'} =~ /\/hd([a-z]+)$/) {
# IDE disk
$disk->{'desc'} = &text('select_device', 'IDE', uc($1));
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/xvd([a-z]+)$/) {
# Xen virtual disk
$disk->{'desc'} = &text('select_device', 'Xen', uc($1));
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/mmcblk([0-9]+)$/) {
# SD-card / MMC
$disk->{'desc'} = &text('select_device', 'SD-Card', $1);
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/vd([a-z]+)$/) {
# KVM virtual disk
$disk->{'desc'} = &text('select_device',
'VirtIO', uc($1));
$disk->{'type'} = 'virtio';
}
elsif ($disk->{'device'} =~ /\/(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
# New complete SCSI disk specification
$disk->{'host'} = $2;
$disk->{'bus'} = $3;
$disk->{'target'} = $4;
$disk->{'lun'} = $5;
$disk->{'desc'} = &text('select_scsi',
"$2", "$3", "$4", "$5");
# Work out the SCSI index for this disk
local $j;
if ($dscsi_mode) {
for($j=0; $j<@dscsi; $j++) {
if ($dscsi[$j]->{'host'} == $disk->{'host'} && $dscsi[$j]->{'bus'} == $disk->{'bus'} && $dscsi[$j]->{'target'} == $disk->{'target'} && $dscsi[$j]->{'lnun'} == $disk->{'lun'}) {
$disk->{'scsi'} = $j;
last;
}
}
}
else {
for($j=0; $j<@pscsi; $j++) {
if ($pscsi[$j] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i && $disk->{'host'} == $1 && $disk->{'target'} == $2) {
$disk->{'scsi'} = $j;
last;
}
}
}
$disk->{'type'} = 'scsi';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/(ide\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
# New-style IDE specification
$disk->{'host'} = $2;
$disk->{'bus'} = $3;
$disk->{'target'} = $4;
$disk->{'lun'} = $5;
$disk->{'desc'} = &text('select_newide',
"$2", "$3", "$4", "$5");
$disk->{'type'} = 'ide';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/(rd\/c(\d+)d(\d+))/) {
# Mylex raid device
local ($mc, $md) = ($2, $3);
$disk->{'desc'} = &text('select_mylex', $mc, $md);
open(RD, "</proc/rd/c$mc/current_status");
while(<RD>) {
if (/^Configuring\s+(.*)/i) {
$disk->{'model'} = $1;
}
elsif (/\s+(\S+):\s+([^, ]+)/ &&
$1 eq $disk->{'device'}) {
$disk->{'raid'} = $2;
}
}
close(RD);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(ida\/c(\d+)d(\d+))/) {
# Compaq RAID device
local ($ic, $id) = ($2, $3);
$disk->{'desc'} = &text('select_cpq', $ic, $id);
open(IDA, -d "/proc/driver/array" ? "</proc/driver/array/ida$ic" : "</proc/driver/cpqarray/ida$ic");
while(<IDA>) {
if (/^(\S+):\s+(.*)/ && $1 eq "ida$ic") {
$disk->{'model'} = $2;
}
}
close(IDA);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(cciss\/c(\d+)d(\d+))/) {
# Compaq Smart Array RAID
local ($ic, $id) = ($2, $3);
$disk->{'desc'} = &text('select_smart', $ic, $id);
open(CCI, "</proc/driver/cciss/cciss$ic");
while(<CCI>) {
if (/^\s*(\S+):\s*(.*)/ && $1 eq "cciss$ic") {
$disk->{'model'} = $2;
}
}
close(CCI);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(ataraid\/disc(\d+)\/disc)/) {
# Promise RAID controller
local $dd = $2;
$disk->{'desc'} = &text('select_promise', $dd);
$disk->{'type'} = 'raid';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/nvme(\d+)n(\d+)$/) {
# NVME SSD controller
$disk->{'desc'} = &text('select_nvme', "$1", "$2");
$disk->{'type'} = 'scsi';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
# Work out short name, like sda
local $short;
if (defined($disk->{'host'})) {
$short = &hbt_to_device($disk->{'host'},
$disk->{'bus'},
$disk->{'target'});
}
else {
$short = $disk->{'device'};
$short =~ s/^.*\///g;
}
$disk->{'short'} = $short;
$disk->{'id'} = $id_map{$disk->{'device'}} ||
$id_map{"/dev/$short"};
$disk->{'ids'} = $all_id_map{$disk->{'device'}} ||
$all_id_map{"/dev/$short"};
};
my $enrich_metadata = sub {
local @disks = @_;
local $d;
# Check /proc/ide for IDE disk models
foreach $d (@disks) {
if ($d->{'type'} eq 'ide') {
local $short = $d->{'short'};
$d->{'model'} = &read_file_contents("/proc/ide/$short/model");
$d->{'model'} =~ s/\r|\n//g;
$d->{'media'} = &read_file_contents("/proc/ide/$short/media");
$d->{'media'} =~ s/\r|\n//g;
if ($d->{'short'} =~ /^vd/ && !$d->{'model'}) {
# Fake up model for KVM VirtIO disks
$d->{'model'} = "KVM VirtIO";
}
}
}
# Fill in SCSI information
foreach $d (@disks) {
if ($d->{'type'} eq 'scsi') {
local $s = $d->{'scsi'};
local $sysdir = "/sys/block/$d->{'short'}/device";
if (-d $sysdir) {
# From kernel 2.6.30+ sys directory
$d->{'model'} = &read_file_contents("$sysdir/vendor").
" ".
&read_file_contents("$sysdir/model");
$d->{'model'} =~ s/\r|\n//g;
$d->{'media'} = &read_file_contents("$sysdir/media");
$d->{'media'} =~ s/\r|\n//g;
}
elsif ($dscsi_mode) {
# From other scsi files
$d->{'model'} = "$dscsi[$s]->{'make'} $dscsi[$s]->{'model'}";
$d->{'controller'} = $dscsi[$s]->{'host'};
$d->{'scsiid'} = $dscsi[$s]->{'target'};
}
else {
# From /proc/scsi/scsi lines
if ($pscsi[$s] =~ /Vendor:\s+(\S+).*Model:\s+(.*)\s+Rev:/i) {
$d->{'model'} = "$1 $2";
}
if ($pscsi[$s] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i) {
$d->{'controller'} = int($1);
$d->{'scsiid'} = int($2);
}
}
if ($d->{'model'} =~ /ATA/) {
# Fake SCSI disk, actually IDE
$d->{'scsi'} = 0;
$d->{'desc'} =~ s/SCSI/SATA/g;
foreach my $p (@{$d->{'parts'} || []}) {
$p->{'desc'} =~ s/SCSI/SATA/g;
}
}
}
}
};
if ($disk_only) {
my @inventory = @devs;
foreach my $device (@inventory) {
my $disk = { 'device' => $device, 'prefix' => $device };
$set_metadata->($disk);
push(@disks, $disk);
}
$enrich_metadata->(@disks);
foreach my $disk (@disks) {
my $device = $disk->{'device'};
my $identity = &disk_discovery_identity($device);
return &disk_discovery_error("Missing device during disk-only discovery: $device", $error_ref)
if (!defined($identity));
return &disk_discovery_error("Disk identity changed during disk-only discovery: $device", $error_ref)
if ($discovery_identity{$device} ne $identity);
# An absent controller model must not silently bypass RAID expansion.
return &disk_discovery_error("Missing disk model during disk-only discovery: $device", $error_ref)
if (($disk->{'type'} eq 'scsi' || $disk->{'type'} eq 'raid') &&
$disk->{'model'} !~ /\S/);
}
return &disk_discovery_error("Disk inventory changed during disk-only discovery", $error_ref)
if ($discovery_partitions ne &read_file_contents('/proc/partitions'));
# These are metadata records, not claims of empty partition tables.
# Do not read or populate the full partition cache.
return @disks;
}
# Call fdisk to get partition and geometry information
local $qdevs = join(" ", map { quotemeta($_) } @devs);
local ($disk, $m2);
@@ -565,7 +297,154 @@ while(<FDISK>) {
local @st = stat($disk->{'device'});
next if (@cdstat && $st[1] == $cdstat[1]);
$set_metadata->($disk);
if ($disk->{'device'} =~ /\/sd([a-z]+)$/) {
# Old-style SCSI disk
$disk->{'desc'} = &text('select_device', 'SCSI',
uc($1));
local ($dscsi) = grep { $_->{'dev'} eq "sd$1" } @dscsi;
$disk->{'scsi'} = $dscsi ? &indexof($dscsi, @dscsi)
: ord(uc($1))-65;
$disk->{'type'} = 'scsi';
}
elsif ($disk->{'device'} =~ /\/hd([a-z]+)$/) {
# IDE disk
$disk->{'desc'} = &text('select_device', 'IDE', uc($1));
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/xvd([a-z]+)$/) {
# Xen virtual disk
$disk->{'desc'} = &text('select_device', 'Xen', uc($1));
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/mmcblk([0-9]+)$/) {
# SD-card / MMC
$disk->{'desc'} = &text('select_device', 'SD-Card', $1);
$disk->{'type'} = 'ide';
}
elsif ($disk->{'device'} =~ /\/vd([a-z]+)$/) {
# KVM virtual disk
$disk->{'desc'} = &text('select_device',
'VirtIO', uc($1));
$disk->{'type'} = 'virtio';
}
elsif ($disk->{'device'} =~ /\/(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
# New complete SCSI disk specification
$disk->{'host'} = $2;
$disk->{'bus'} = $3;
$disk->{'target'} = $4;
$disk->{'lun'} = $5;
$disk->{'desc'} = &text('select_scsi',
"$2", "$3", "$4", "$5");
# Work out the SCSI index for this disk
local $j;
if ($dscsi_mode) {
for($j=0; $j<@dscsi; $j++) {
if ($dscsi[$j]->{'host'} == $disk->{'host'} && $dscsi[$j]->{'bus'} == $disk->{'bus'} && $dscsi[$j]->{'target'} == $disk->{'target'} && $dscsi[$j]->{'lnun'} == $disk->{'lun'}) {
$disk->{'scsi'} = $j;
last;
}
}
}
else {
for($j=0; $j<@pscsi; $j++) {
if ($pscsi[$j] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i && $disk->{'host'} == $1 && $disk->{'target'} == $2) {
$disk->{'scsi'} = $j;
last;
}
}
}
$disk->{'type'} = 'scsi';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/(ide\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
# New-style IDE specification
$disk->{'host'} = $2;
$disk->{'bus'} = $3;
$disk->{'target'} = $4;
$disk->{'lun'} = $5;
$disk->{'desc'} = &text('select_newide',
"$2", "$3", "$4", "$5");
$disk->{'type'} = 'ide';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/(rd\/c(\d+)d(\d+))/) {
# Mylex raid device
local ($mc, $md) = ($2, $3);
$disk->{'desc'} = &text('select_mylex', $mc, $md);
open(RD, "</proc/rd/c$mc/current_status");
while(<RD>) {
if (/^Configuring\s+(.*)/i) {
$disk->{'model'} = $1;
}
elsif (/\s+(\S+):\s+([^, ]+)/ &&
$1 eq $disk->{'device'}) {
$disk->{'raid'} = $2;
}
}
close(RD);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(ida\/c(\d+)d(\d+))/) {
# Compaq RAID device
local ($ic, $id) = ($2, $3);
$disk->{'desc'} = &text('select_cpq', $ic, $id);
open(IDA, -d "/proc/driver/array" ? "</proc/driver/array/ida$ic" : "</proc/driver/cpqarray/ida$ic");
while(<IDA>) {
if (/^(\S+):\s+(.*)/ && $1 eq "ida$ic") {
$disk->{'model'} = $2;
}
}
close(IDA);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(cciss\/c(\d+)d(\d+))/) {
# Compaq Smart Array RAID
local ($ic, $id) = ($2, $3);
$disk->{'desc'} = &text('select_smart', $ic, $id);
open(CCI, "</proc/driver/cciss/cciss$ic");
while(<CCI>) {
if (/^\s*(\S+):\s*(.*)/ && $1 eq "cciss$ic") {
$disk->{'model'} = $2;
}
}
close(CCI);
$disk->{'type'} = 'raid';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
elsif ($disk->{'device'} =~ /\/(ataraid\/disc(\d+)\/disc)/) {
# Promise RAID controller
local $dd = $2;
$disk->{'desc'} = &text('select_promise', $dd);
$disk->{'type'} = 'raid';
$disk->{'prefix'} =~ s/disc$/part/g;
}
elsif ($disk->{'device'} =~ /\/nvme(\d+)n(\d+)$/) {
# NVME SSD controller
$disk->{'desc'} = &text('select_nvme', "$1", "$2");
$disk->{'type'} = 'scsi';
$disk->{'prefix'} = $disk->{'device'}.'p';
}
# Work out short name, like sda
local $short;
if (defined($disk->{'host'})) {
$short = &hbt_to_device($disk->{'host'},
$disk->{'bus'},
$disk->{'target'});
}
else {
$short = $disk->{'device'};
$short =~ s/^.*\///g;
}
$disk->{'short'} = $short;
$disk->{'id'} = $id_map{$disk->{'device'}} ||
$id_map{"/dev/$short"};
$disk->{'ids'} = $all_id_map{$disk->{'device'}} ||
$all_id_map{"/dev/$short"};
push(@disks, $disk);
}
@@ -721,40 +600,66 @@ while(<FDISK>) {
}
close(FDISK);
$enrich_metadata->(@disks);
# Check /proc/ide for IDE disk models
foreach $d (@disks) {
if ($d->{'type'} eq 'ide') {
local $short = $d->{'short'};
$d->{'model'} = &read_file_contents("/proc/ide/$short/model");
$d->{'model'} =~ s/\r|\n//g;
$d->{'media'} = &read_file_contents("/proc/ide/$short/media");
$d->{'media'} =~ s/\r|\n//g;
if ($d->{'short'} =~ /^vd/ && !$d->{'model'}) {
# Fake up model for KVM VirtIO disks
$d->{'model'} = "KVM VirtIO";
}
}
}
# Fill in SCSI information
foreach $d (@disks) {
if ($d->{'type'} eq 'scsi') {
local $s = $d->{'scsi'};
local $sysdir = "/sys/block/$d->{'short'}/device";
if (-d $sysdir) {
# From kernel 2.6.30+ sys directory
$d->{'model'} = &read_file_contents("$sysdir/vendor").
" ".
&read_file_contents("$sysdir/model");
$d->{'model'} =~ s/\r|\n//g;
$d->{'media'} = &read_file_contents("$sysdir/media");
$d->{'media'} =~ s/\r|\n//g;
}
elsif ($dscsi_mode) {
# From other scsi files
$d->{'model'} = "$dscsi[$s]->{'make'} $dscsi[$s]->{'model'}";
$d->{'controller'} = $dscsi[$s]->{'host'};
$d->{'scsiid'} = $dscsi[$s]->{'target'};
}
else {
# From /proc/scsi/scsi lines
if ($pscsi[$s] =~ /Vendor:\s+(\S+).*Model:\s+(.*)\s+Rev:/i) {
$d->{'model'} = "$1 $2";
}
if ($pscsi[$s] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i) {
$d->{'controller'} = int($1);
$d->{'scsiid'} = int($2);
}
}
if ($d->{'model'} =~ /ATA/) {
# Fake SCSI disk, actually IDE
$d->{'scsi'} = 0;
$d->{'desc'} =~ s/SCSI/SATA/g;
foreach my $p (@{$d->{'parts'}}) {
$p->{'desc'} =~ s/SCSI/SATA/g;
}
}
}
}
@list_disks_partitions_cache = @disks;
return @disks;
}
# Report disk-only failure without changing the list-returning caller contract.
sub disk_discovery_error
{
my ($message, $error_ref) = @_;
if ($error_ref) {
$$error_ref = $message;
}
else {
warn "$message\n";
}
return ();
}
# Compare device-node and sysfs object identity without opening the device.
# This detects disappearance/replacement during enumeration, not all possible
# changes after this function returns. The next collection always scans again.
sub disk_discovery_identity
{
my ($device) = @_;
my @node = stat($device);
return if (!@node);
my $short = $device;
$short =~ s{^/dev/}{};
$short =~ s{/}{!}g;
my @sys = stat("/sys/class/block/$short");
return join(':', @node[0,1,6], @sys ? @sys[0,1] : ('', ''));
}
# partition_description(device)
# Converts a device path like /dev/hda1 into a human-readable name
sub partition_description

View File

@@ -1,9 +0,0 @@
# Disk-only discovery tests
Run `prove -lr fdisk/t`. Perl::Critic enables lint checks for the added tests.
Fixtures use temporary kernel/device trees and fake partition commands; they
never query real disks. Coverage includes GPT/MBR, fdisk fallback, cache isolation,
fresh inventory and identifiers, disappearance, invalid/empty inventory, and
non-throwing error reporting, controller-qualified sysfs replacement with a retained
device node, and canonical identities through symlinked temporary parents. Physical
RAID and real hotplug need hardware tests.

View File

@@ -1,16 +0,0 @@
use strict;
use warnings;
use Test::More;
use FindBin qw($Bin);
BEGIN {
eval { require Perl::Critic; 1 }
or plan skip_all => 'Perl::Critic not installed';
}
# Apply the existing policy to new tests, not untouched legacy module files.
my $critic = Perl::Critic->new(-profile => "$Bin/../../.perlcriticrc");
for my $file (qw(disk-discovery.t fixtures/disk-discovery.pl disk-discovery-perlcritic.t)) {
my @violations = $critic->critique("$Bin/$file");
is(scalar @violations, 0, "$file perlcritic");
diag join('', @violations) if @violations;
}
done_testing();

View File

@@ -1,223 +0,0 @@
use strict;
use warnings;
use Test::More;
use JSON::PP;
use File::Temp qw(tempdir);
use File::Path qw(make_path remove_tree);
use File::Basename qw(dirname);
use Cwd qw(abs_path);
use IPC::Open3;
use Symbol qw(gensym);
my $here = abs_path(dirname(__FILE__));
my $library = "$here/../fdisk-lib.pl";
my $fixture;
sub write_file {
my ($name, $value) = @_;
make_path(dirname("$fixture/$name"));
open(my $fh, '>', "$fixture/$name") or die $!;
print {$fh} $value;
close($fh) or die $!;
}
sub disk {
my ($name, $vendor, $model) = @_;
write_file("dev/$name", '');
write_file("sys/block/$name/device/vendor", "$vendor\n");
write_file("sys/block/$name/device/model", "$model\n");
make_path("$fixture/sys/class/block/$name");
}
sub setup_fixture {
my ($parent) = @_;
$fixture = abs_path(tempdir(CLEANUP => 1, defined($parent) ? (DIR => $parent) : ()));
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n 8 1 90 sda1\n");
write_file('proc/scsi/scsi', "Attached devices:\n");
disk('sda', 'Example', 'USB NVMe');
disk('sdb', 'ATA', 'second');
make_path("$fixture/dev/disk/by-id");
symlink('../../sda', "$fixture/dev/disk/by-id/old") or die $!;
write_file('tool-output', '');
for my $tool (qw(parted fdisk)) {
write_file("bin/$tool", "#!$^X\n" . <<'TOOL');
use strict;
my $root = $ENV{'FIXTURE_ROOT'};
open(my $calls, '>>', "$root/calls") or die $!;
print {$calls} "query\n";
close($calls);
open(my $data, '<', "$root/tool-output") or die $!;
print while <$data>;
TOOL
chmod(0755, "$fixture/bin/$tool") or die $!;
}
}
sub run_library {
my ($scenario) = @_;
local $ENV{'PATH'} = "$fixture/bin:$ENV{'PATH'}";
local $ENV{'FIXTURE_ROOT'} = $fixture;
local $SIG{'ALRM'} = sub { die "fixture timed out\n"; };
alarm(15);
my $err = gensym();
my $pid = open3(my $in, my $out, $err, $^X, "$here/fixtures/disk-discovery.pl",
$library, $fixture, $scenario || 'disk');
close($in);
my ($stdout, $stderr);
{ local $/; $stdout = <$out>; $stderr = <$err>; }
waitpid($pid, 0);
my $status = $?;
alarm(0);
is($status, 0, 'fixture process succeeds') or diag($stderr);
is($stderr, '', 'no unexpected warnings');
my $result = decode_json($stdout);
is($result->{'exception'}, undef, 'library does not throw an exception');
return $result;
}
sub parted_output {
my ($table) = @_;
my $part = $table eq 'gpt' ? ' 1 1cyl 9cyl 8cyl ext4 name' : ' 1 1cyl 9cyl 8cyl primary ext4';
my $extra = $table eq 'msdos' ? " 2 9cyl 10cyl 1cyl extended\n" : '';
write_file('tool-output', "Disk $fixture/dev/sda: 10cyl\nBIOS cylinder,head,sector geometry: 10,4,8. Each cylinder is 16384b.\nPartition Table: $table\n$part\n$extra");
}
subtest 'disk-only inventory avoids partition commands and cache' => sub {
setup_fixture();
my $r = run_library();
is($r->{'error'}, undef, 'discovery succeeds');
my $d = $r->{'results'}->[0]->[0];
is($d->{'model'}, 'Example USB NVMe', 'model retained');
is_deeply($d->{'ids'}, ['old'], 'stable ID retained');
ok(!exists($d->{'parts'}) && !exists($d->{'table'}) && !exists($d->{'size'}),
'metadata does not claim partition or geometry data');
is_deeply($r->{'cache'}, [], 'full partition cache not populated');
ok(!-e "$fixture/calls", 'no partition tool called');
};
subtest 'full discovery still parses GPT, MBR and extended partitions' => sub {
for my $table (qw(gpt msdos)) {
setup_fixture(); parted_output($table);
my $r = run_library('full');
is($r->{'error'}, undef, 'full discovery succeeds');
my $d = $r->{'results'}->[0]->[0];
is($d->{'table'}, $table, 'partition table retained');
is(scalar(@{$d->{'parts'}}), $table eq 'gpt' ? 1 : 2, 'partitions retained');
is($d->{'model'}, 'Example USB NVMe', 'shared model metadata retained');
ok(-e "$fixture/calls", 'full path queries partition tool');
}
};
subtest 'full fdisk fallback remains available' => sub {
setup_fixture();
write_file('tool-output', "Disk $fixture/dev/sda: 1 GiB, 1073741824 bytes, 2097152 sectors\nGeometry: 4 heads, 8 sectors/track, 10 cylinders\nUnits = cylinders of 32 * 512\nDisklabel type: gpt\n$fixture/dev/sda1 1 9 9 4K Linux filesystem\n");
my $r = run_library('full_fdisk');
is($r->{'error'}, undef, 'fdisk fallback succeeds');
is(scalar(@{$r->{'results'}->[0]->[0]->{'parts'}}), 1, 'fdisk partition retained');
ok(-e "$fixture/calls", 'fdisk command used');
};
subtest 'cache isolation in both call orders' => sub {
for my $scenario (qw(disk_full full_disk)) {
setup_fixture(); parted_output('gpt');
my $r = run_library($scenario);
my ($disk, $full) = @{$r->{'results'}};
($disk, $full) = ($full, $disk) if $scenario eq 'full_disk';
ok(!exists($disk->[0]->{'parts'}), 'disk-only result has no partition claim');
ok(@{$full->[0]->{'parts'}}, 'full result has partitions');
is_deeply($r->{'cache'}, $full, 'full cache contains only full result');
}
};
subtest 'replacement refreshes model and identifiers' => sub {
setup_fixture(); my $r = run_library('refresh');
my ($a, $b) = map { $_->[0] } @{$r->{'results'}};
is($a->{'model'}, 'Example USB NVMe', 'first model');
is($b->{'model'}, 'Example Replacement', 'new model');
is_deeply($b->{'ids'}, ['new'], 'new stable ID');
};
subtest 'addition and removal refresh inventory' => sub {
setup_fixture(); my $r = run_library('remove_add');
is($r->{'results'}->[0]->[0]->{'device'}, "$fixture/dev/sda", 'initial disk');
is($r->{'results'}->[1]->[0]->{'device'}, "$fixture/dev/sdb", 'replacement inventory');
};
subtest 'disappearance during scan fails explicitly' => sub {
setup_fixture(); like(run_library('race')->{'error'}, qr/Missing device/, 'no stale result');
};
subtest 'missing model and missing inventory fail explicitly' => sub {
setup_fixture();
unlink("$fixture/sys/block/sda/device/vendor", "$fixture/sys/block/sda/device/model");
like(run_library()->{'error'}, qr/Missing disk model/, 'missing model fails');
unlink("$fixture/proc/partitions");
like(run_library()->{'error'}, qr/Cannot open disk inventory/, 'missing inventory fails');
};
subtest 'multiple drives and namespaces retain classifications' => sub {
setup_fixture();
my @names = qw(sda sdaa nvme0n1 nvme0n2 mmcblk0 xvda vda hda);
write_file('proc/partitions', "major minor #blocks name\n" . join('', map { "8 0 100 $_\n" } @names));
write_file('proc/ide/hda/media', "disk\n"); write_file('proc/ide/hda/model', "IDE fixture\n");
disk($_, 'Example', 'disk') for @names;
my $r = run_library();
is($r->{'error'}, undef, 'multi-drive discovery succeeds');
is_deeply([sort map { $_->{'short'} } @{$r->{'results'}->[0]}], [sort @names], 'all devices retained');
my ($virtio) = grep { $_->{'short'} eq 'vda' } @{$r->{'results'}->[0]};
is($virtio->{'type'}, 'virtio', 'VirtIO classification preserved');
ok(!-e "$fixture/calls", 'multiple drives need no partition query');
};
subtest 'unpartitioned disk without stable link remains discoverable' => sub {
setup_fixture(); unlink("$fixture/dev/disk/by-id/old");
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n");
my $r = run_library();
is(scalar(@{$r->{'results'}->[0]}), 1, 'disk retained');
is($r->{'results'}->[0]->[0]->{'ids'}, undef, 'no fabricated identifiers');
};
subtest 'legacy controller metadata and SCSI fallback remain available' => sub {
setup_fixture();
my @names = qw(cciss/c0d0 rd/c0d0 ida/c0d0);
write_file('proc/partitions', "major minor #blocks name\n" . join('', map { "8 0 100 $_\n" } @names));
write_file("dev/$_", '') for @names;
write_file('proc/driver/cciss/cciss0', "cciss0: Smart Array\n");
write_file('proc/rd/c0/current_status', "Configuring Mylex\n");
write_file('proc/driver/cpqarray/ida0', "ida0: Compaq\n");
my $r = run_library();
is_deeply([map { $_->{'model'} } @{$r->{'results'}->[0]}], ['Smart Array','Mylex','Compaq'], 'controller models retained');
ok(!grep({ $_->{'type'} ne 'raid' } @{$r->{'results'}->[0]}), 'legacy RAID classification retained');
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n");
remove_tree("$fixture/sys/block/sda/device");
write_file('proc/scsi/scsi', "Attached devices:\nHost: scsi0 Channel: 00 Id: 00 Lun: 00\n Vendor: LSI Model: RAID fixture Rev: 1\n Type: Direct-Access\n");
like(run_library()->{'results'}->[0]->[0]->{'model'}, qr/LSI/, 'proc SCSI fallback retained');
};
subtest 'failure reporting preserves the list-returning contract' => sub {
setup_fixture();
unlink("$fixture/dev/sda");
my $r = run_library('warn');
is_deeply($r->{'results'}, [[]], 'unreported-reference failure returns no partial disks');
is(scalar @{$r->{'warnings'}}, 1, 'caller without error reference receives one warning');
like($r->{'warnings'}->[0], qr/Missing device/, 'warning explains the failure');
setup_fixture();
is(run_library('reset_error')->{'error'}, undef, 'successful disk-only call clears stale error');
write_file('proc/partitions', "invalid inventory\n");
like(run_library()->{'error'}, qr/Invalid disk inventory/, 'invalid header is reported');
write_file('proc/partitions', "major minor #blocks name\n");
$r = run_library();
is_deeply($r->{'results'}, [[]], 'empty valid inventory succeeds');
is($r->{'error'}, undef, 'empty valid inventory is not an error');
};
subtest 'controller-qualified sysfs replacement is detected with a retained device node' => sub {
setup_fixture();
write_file('proc/partitions', "major minor #blocks name\n 104 0 100 cciss/c0d0\n");
write_file('dev/cciss/c0d0', '');
write_file('proc/driver/cciss/cciss0', "cciss0: Smart Array\n");
make_path("$fixture/sys/class/block/cciss!c0d0");
my $stable = run_library('legacy_stable');
is($stable->{'error'}, undef, 'controller sysfs path is accepted');
my $before = [stat("$fixture/dev/cciss/c0d0")];
my $changed = run_library('legacy_replace');
like($changed->{'error'}, qr/Disk identity changed/, 'sysfs replacement is detected');
is_deeply($changed->{'results'}, [[]], 'replacement returns no stale inventory');
my $after = [stat("$fixture/dev/cciss/c0d0")];
is_deeply([@{$after}[0,1,6]], [@{$before}[0,1,6]], 'device-node identity was retained');
};
subtest 'symlinked temporary parents produce canonical fixture identities' => sub {
my $parent = abs_path(tempdir(CLEANUP => 1));
make_path("$parent/real");
symlink("$parent/real", "$parent/alias") or die $!;
setup_fixture("$parent/alias");
is($fixture, abs_path($fixture), 'fixture root is canonical');
my $result = run_library();
is($result->{'error'}, undef, 'discovery succeeds through symlinked parent');
is_deeply($result->{'results'}->[0]->[0]->{'ids'}, ['old'], 'resolved stable IDs match canonical devices');
};
done_testing();

View File

@@ -1,90 +0,0 @@
use strict;
use warnings;
use JSON::PP;
use Cwd qw(abs_path);
use File::Basename qw(dirname);
my ($source, $root, $scenario) = @ARGV;
open(my $fh, '<', $source) or die $!;
local $/; my $code = <$fh>; close($fh); $/ = "\n";
# Keep the actual library functions; replace literal filesystem roots only.
$code = substr($code, index($code, 'sub list_disks_partitions'));
for my $prefix ('/proc/', '/sys/', '/dev/') {
$code =~ s/\Q$prefix\E/$root$prefix/g;
}
our (@reads, $has_parted, @list_disks_partitions_cache, $change);
$has_parted = $scenario !~ /fdisk/;
# Mock the cross-module filesystem helper used by the real library.
{ package mount; ## no critic (Modules::RequireFilenameMatchesPackage)
sub list_fstypes { return ('ext2', 'ext3', 'ext4', 'xfs'); }
}
sub text { return join(' ', @_); }
sub indexof { my ($x, @v) = @_; for (0..$#v) { return $_ if $v[$_] eq $x; } return -1; }
sub resolve_links { return abs_path($_[0]); }
sub simplify_path { my $p = $_[0]; $p =~ s{/dev/disk/by-id/../../}{/dev/}; return $p; }
sub read_file_contents {
my ($p) = @_;
die "Attempt to read device data" if $p =~ m{\Q$root\E/dev/};
push(@reads, $p);
open(my $f, '<', $p) or return '';
local $/; my $s = <$f>; close($f);
if ($change && $p =~ m{/device/model$}) {
unlink("$root/dev/sda");
$change = 0;
}
return $s;
}
{
# Load the real legacy functions, which use Webmin package globals.
no strict 'vars'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
# Webmin does not enable these warnings for optional legacy metadata.
no warnings 'uninitialized';
eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval)
die $@ if $@;
}
if ($scenario eq 'legacy_replace') {
my $identity = \&disk_discovery_identity;
my $calls = 0;
no warnings 'redefine';
*disk_discovery_identity = sub {
if (++$calls == 2) {
my $sys = "$root/sys/class/block/cciss!c0d0";
rename($sys, "$sys.old") or die $!;
mkdir($sys) or die $!;
}
return $identity->(@_);
};
}
my @results;
my $error;
my $exception;
my @warnings;
eval {
if ($scenario eq 'race') { $change = 1; }
if ($scenario eq 'reset_error') { $error = 'previous failure'; }
if ($scenario eq 'disk_full' || $scenario eq 'full_disk') {
my $first = $scenario eq 'disk_full' ? 1 : 0;
push(@results, [list_disks_partitions(undef, $first, \$error)]);
push(@results, [list_disks_partitions(undef, !$first, \$error)]);
}
elsif ($scenario eq 'refresh') {
push(@results, [list_disks_partitions(undef, 1, \$error)]);
open(my $f, '>', "$root/sys/block/sda/device/model") or die $!;
print $f "Replacement\n"; close($f);
unlink("$root/dev/disk/by-id/old");
symlink('../../sda', "$root/dev/disk/by-id/new") or die $!;
push(@results, [list_disks_partitions(undef, 1, \$error)]);
}
elsif ($scenario eq 'remove_add') {
push(@results, [list_disks_partitions(undef, 1, \$error)]);
open(my $f, '>', "$root/proc/partitions") or die $!;
print $f "major minor #blocks name\n 8 16 100 sdb\n"; close($f);
push(@results, [list_disks_partitions(undef, 1, \$error)]);
}
elsif ($scenario eq 'warn') {
local $SIG{__WARN__} = sub { push(@warnings, @_); };
push(@results, [list_disks_partitions(undef, 1)]);
}
else { push(@results, [list_disks_partitions(undef, $scenario =~ /^full/ ? 0 : 1, \$error)]); }
1;
} or $exception = $@;
print JSON::PP->new->canonical->encode({results=>\@results, error=>$error, exception=>$exception, warnings=>\@warnings, reads=>\@reads, cache=>\@list_disks_partitions_cache});

View File

@@ -12,7 +12,7 @@ else {
&can_edit_user($in{'user'}) || &error($text{'poll_ecannot'});
@uinfo = getpwnam($in{'user'});
$file = "$uinfo[7]/.fetchmailrc";
$uheader = &text('poll_foruser', "<tt>".&html_escape($in{'user'})."</tt>");
$uheader = &text('poll_foruser', "<tt>$in{'user'}</tt>");
}
&ui_print_unbuffered_header($uheader, $text{'check_title'}, "");
@@ -54,7 +54,7 @@ else { print "$text{'check_ok'} <p>\n"; }
$config{'config_file'} ? $file : $in{'user'}, $poll);
if (!$fetchmail_config && $config{'view_mode'}) {
&ui_print_footer("edit_user.cgi?user=".&urlize($in{'user'}), $text{'user_return'},
&ui_print_footer("edit_user.cgi?user=$in{'user'}", $text{'user_return'},
"", $text{'index_return'});
}
else {

View File

@@ -12,7 +12,7 @@ else {
&can_edit_user($in{'user'}) || &error($text{'poll_ecannot'});
@uinfo = getpwnam($in{'user'});
$file = "$uinfo[7]/.fetchmailrc";
$uheader = &text('poll_foruser', "<tt>".&html_escape($in{'user'})."</tt>");
$uheader = &text('poll_foruser', "<tt>$in{'user'}</tt>");
}
&ui_print_header($uheader, $text{'global_title'}, "");
@@ -54,7 +54,7 @@ print &ui_table_end();
print &ui_form_end([ [ undef, $text{'save'} ] ]);
if (!$fetchmail_config && $config{'view_mode'}) {
&ui_print_footer("edit_user.cgi?user=".&urlize($in{'user'}), $text{'user_return'},
&ui_print_footer("edit_user.cgi?user=$in{'user'}", $text{'user_return'},
"", $text{'index_return'});
}
else {

View File

@@ -11,7 +11,7 @@ else {
&can_edit_user($in{'user'}) || &error($text{'poll_ecannot'});
@uinfo = getpwnam($in{'user'});
$file = "$uinfo[7]/.fetchmailrc";
$uheader = &text('poll_foruser', "<tt>".&html_escape($in{'user'})."</tt>");
$uheader = &text('poll_foruser', "<tt>$in{'user'}</tt>");
}
if ($in{'new'}) {
@@ -139,7 +139,7 @@ else {
print &ui_form_end(\@buts);
if (!$fetchmail_config && $config{'view_mode'}) {
&ui_print_footer("edit_user.cgi?user=".&urlize($in{'user'}), $text{'user_return'},
&ui_print_footer("edit_user.cgi?user=$in{'user'}", $text{'user_return'},
"", $text{'index_return'});
}
else {

View File

@@ -9,7 +9,7 @@ require './fetchmail-lib.pl';
$file = "$uinfo[7]/.fetchmailrc";
@conf = &parse_config_file("$uinfo[7]/.fetchmailrc");
@conf = grep { $_->{'poll'} } @conf;
&ui_print_header(&text('user_header', "<tt>".&html_escape($in{'user'})."</tt>"), $text{'user_title'}, "");
&ui_print_header(&text('user_header', "<tt>$in{'user'}</tt>"), $text{'user_title'}, "");
print &show_polls(\@conf, $file, $in{'user'});

24
gray-theme/edit_overlay.cgi Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/local/bin/perl
# Show a form for changing the theme overlay
require "gray-theme/gray-theme-lib.pl";
# Get the user and themes
&foreign_require("acl", "acl-lib.pl");
@overlays = &list_virtualmin_theme_overlays();
($user) = grep { $_->{'name'} eq $base_remote_user } &acl::list_users();
$user || &error($text{'overlay_euser'});
$overlay = $current_themes[1];
&ui_print_header(undef, $text{'overlay_title'}, "");
print &ui_form_start("save_overlay.cgi");
print $text{'overlay_desc'},"<p>\n";
print "<b>$text{'overlay_msg'}</b>\n";
print &ui_select("overlay", $overlay,
[ [ "", $text{'overlay_none'} ],
map { [ $_->{'dir'}, $_->{'desc'} ] } @overlays ]),"<br>\n";
print &ui_form_end([ [ undef, $text{'overlay_ok'} ] ]);
&ui_print_footer("right.cgi", $text{'right_return'});

View File

@@ -69,7 +69,7 @@
<g transform="matrix(.3804 0 0 -.3804 -45.383 147.81)">
<g clip-path="url(#clipPath4378)">
<g transform="translate(188.69 382.73)">
<path d="m0 0c40.231-1.537 22.668-21.769 73.743-8.284 43.506 11.486 52.259-59.513 6.705-55.023-31.707 3.126-18.03-51.842-80.448-55.274-34.592 0-62.645 26.551-62.645 59.291 0 32.739 28.053 59.29 62.645 59.29" fill="#fff"/>
<path d="m0 0c40.231-1.537 22.668-21.769 73.743-8.284 43.506 11.486 52.259-59.513 6.705-55.023-31.707 3.126-18.03-51.842-80.448-55.274-34.592 0-62.645 26.551-62.645 59.291 0 32.739 28.053 59.29 62.645 59.29" fill="#e67e38"/>
</g>
</g>
<g clip-path="url(#clipPath4390)">
@@ -80,14 +80,14 @@
<g clip-path="url(#clipPath4436)">
<g clip-path="url(#clipPath4444)">
<g clip-path="url(#clipPath4448)">
<g transform="matrix(1 0 0 -1 142.87 284.15)" fill="#df7935">
<g transform="matrix(1 0 0 -1 142.87 284.15)" fill="#fff">
<path d="m75.456-71.654c-6.912-8.8704-18.432-11.981-27.533-11.981-25.459 0-44.006 16.704-44.006 42.854s18.547 42.854 44.006 42.854c11.174 0 22.694-4.4928 29.491-14.17l-14.976-11.174c-3.6864 5.5296-9.5616 8.7552-16.819 8.7552-13.018 0-23.04-11.059-23.04-26.266s10.022-26.266 23.616-26.266c6.6816 0 11.866 2.4192 15.437 6.6816z"/>
</g>
</g>
</g>
<g clip-path="url(#clipPath4472)">
<g clip-path="url(#clipPath4476)">
<g transform="matrix(1 0 0 -1 241.04 335.48)" fill="#df7935">
<g transform="matrix(1 0 0 -1 241.04 335.48)" fill="#fff">
<path d="m2.7136 0h7.68v-12.39c0-3.2256 0.6144-6.3488 4.608-6.3488 3.5328 0 3.4304 3.7376 3.4304 5.5808v13.158h7.68v-13.158c0-3.072 1.1264-5.5808 4.6592-5.5808 2.5088 0 3.3792 1.9456 3.3792 4.2496v14.49h7.68v-14.694c0-6.0416-2.048-10.803-8.8064-10.803-3.6864 0-6.0928 1.28-7.8848 4.4544-1.3312-3.0208-4.1984-4.4544-7.68-4.4544-3.7376 0-6.2464 2.048-7.2704 3.9936h-0.1024v-3.3792h-7.3728z"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -63,6 +63,25 @@ else {
}
$title = &get_html_title($title);
# Work out if we have a top frame
if ($hasvirt) {
%vconfig = &foreign_config("virtual-server");
}
$upperframe = $vconfig{'theme_topframe'} ||
$gconfig{'theme_topframe'};
$upperrows = $vconfig{'theme_toprows'} ||
$gconfig{'theme_toprows'} || 200;
if ($upperframe =~ /\$LEVEL|\$\{LEVEL/) {
# Sub in user level
$levelnum = &get_virtualmin_user_level();
$level = $levelnum == 0 ? "master" :
$levelnum == 1 ? "reseller" :
$levelnum == 2 ? "domain" :
$levelnum == 3 ? "usermin" :
$levelnum == 4 ? "owner" : "unknown";
$upperframe = &substitute_template($upperframe, { 'level' => $level });
}
# Show frameset
&PrintHeader();
$cols = &get_left_frame_width();
@@ -74,15 +93,15 @@ if ($current_lang_info->{'rtl'} || $current_lang eq "ar") {
$fscols = "*,$cols";
}
# The frameset declares the scheme inherited by its frames. The script keeps
# an automatic scheme stable across reloads and provides the per-tab Alt+T
# toggle and Alt+R content-frame reload. The stylesheet paints the background
# behind frames while they load.
# Page head. The frameset declares the same color scheme as its frames,
# since browsers give a frame the scheme of the page holding it. When no
# scheme is forced, the script keeps it steady across reloads. The
# stylesheet paints the page ground behind a frame that is still loading.
$scheme = &theme_color_scheme();
print "<html".($scheme ? " data-scheme='$scheme'" : "").">\n";
print "<head>\n";
print "<meta name='color-scheme' content='".($scheme || "light dark")."'>\n";
print &theme_scheme_script($scheme);
print &theme_scheme_script() if (!$scheme);
print "<link rel='stylesheet' type='text/css' href='".&get_webprefix().
"/unauthenticated/gray-theme.css?".
&theme_asset_key("gray-theme.css")."'>\n";
@@ -104,6 +123,19 @@ print "<link rel='icon' type='image/png' sizes='32x32' href='$imgdir/favicons/
print "<link rel='icon' type='image/png' sizes='192x192' href='$imgdir/favicons/$prod/favicon-192x192.png'>\n";
print "</head>\n";
# Upper custom frame
if ($upperframe) {
print "<frameset rows='$upperrows,*' border=0>\n";
if ($upperframe =~ /^\//) {
# Local file to serve
print "<frame name=top src='top.cgi' scrolling=auto>\n";
}
else {
# Absolute URL
print "<frame name=top src='$upperframe' scrolling=auto>\n";
}
}
# Left and right frames
print "<frameset cols='$fscols' border=0>\n";
print $frame1,"\n";
@@ -117,6 +149,9 @@ print "</body>\n";
print "</noframes>\n";
# End of the frames and page
if ($upperframe) {
print "</frameset>\n";
}
print "</frameset>\n";
print "</html>\n";

View File

@@ -75,4 +75,13 @@ has_vm2=Cloudmin
has_webmin=Webmin
has_usermin=Usermin
overlay_title=Change Theme
overlay_desc=This page allows you to select a different theme overlay, which can change the colors, icons and layout of the Virtualmin user interface.
overlay_msg=Change theme overlay to:
overlay_ok=Change Now
overlay_none=None - use default colors and icons
overlay_refresh=Refreshing Virtualmin user interface ..
overlay_done=.. done
overlay_euser=Failed to get details of current user!
overlay_webmin=Reloading Webmin configuration ..
__norefs=1

View File

@@ -82,7 +82,7 @@ my %prodnames = ( 'cloudmin' => $text{'has_vm2'},
popup_header($prodnames{$prod});
# The whole menu sits in one panel that floats on the page ground
print "<div class='menu-panel menu-panel-$prod'>\n";
print "<div class='menu-panel'>\n";
if ($mode eq "modules") {
# Only showing Webmin modules

33
gray-theme/save_overlay.cgi Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/local/bin/perl
# Update the theme overlay
require "gray-theme/gray-theme-lib.pl";
&ReadParse();
($gtheme) = split(/\s+/, $gconfig{'theme'});
# Get and modify the user
&foreign_require("acl", "acl-lib.pl");
($user) = grep { $_->{'name'} eq $base_remote_user } &acl::list_users();
$user || &error($text{'overlay_euser'});
if ($in{'overlay'}) {
$user->{'theme'} = $current_theme;
}
elsif ($gtheme eq $current_theme) {
# No need for user-specific theme
delete($user->{'theme'});
}
$user->{'overlay'} = $in{'overlay'};
&acl::modify_user($user->{'name'}, $user);
# Refresh the page
&ui_print_header(undef, $text{'overlay_title'}, "");
print $text{'overlay_webmin'},"<br>\n";
&reload_miniserv();
print $text{'overlay_done'},"<p>\n";
print $text{'overlay_refresh'},"<br>\n";
print &js_redirect("/", "top");
print $text{'overlay_done'},"<p>\n";
&ui_print_footer("right.cgi", $text{'right_return'});

View File

@@ -279,7 +279,7 @@ sub theme_prehead
my $pfx = &get_webprefix();
my $scheme = &theme_color_scheme();
print "<meta name='color-scheme' content='".($scheme || "light dark")."'>\n";
print &theme_scheme_script($scheme);
print &theme_scheme_script() if (!$scheme);
print &theme_frame_script();
print "<link rel='stylesheet' type='text/css' href='$pfx/unauthenticated/".
"gray-theme.css?".&theme_asset_key("gray-theme.css")."'>\n";
@@ -290,157 +290,42 @@ $tconfig{'inbody'} = (@cls ? "class='".join(" ", @cls)."'" : "").
($scheme ? " data-scheme='$scheme'" : "");
}
# theme_scheme_script([forced-scheme])
# Returns the scheme bootstrap and frame-wide keyboard shortcuts. The optional
# forced scheme is the configured default. Alt+T stores a per-tab override
# without changing that setting, and Alt+R reloads only the content frame.
# Scheme changes reach the frameset and both frames. An automatic scheme uses
# its last value before first paint, follows system changes, and is checked
# again after load.
# theme_scheme_script()
# Returns a script that keeps the color scheme steady when none is forced.
# A browser can report the system scheme for a moment before settling on
# its own, and gives a frame the scheme of the page holding it only once
# that page is styled, so a frame could paint dark and then turn light on
# every reload. The script applies the last scheme seen to the root
# element before the first paint and follows later changes. In a top
# window it also writes the scheme into the root style, so the frames
# inherit it, and checks again a second after load if no change came, in
# case the stored scheme was stale.
sub theme_scheme_script
{
my ($forced) = @_;
$forced = $forced && $forced =~ /^(light|dark)$/ ? $1 : "";
my $rv = <<'EOF';
return <<'EOF';
<script type='text/javascript'>
/* initializeGrayTheme(): Apply the scheme and bind frame shortcuts. */
(function() {
var key = 'gray-theme-scheme', overrideKey = key + '-override',
forced = '__GRAY_THEME_FORCED_SCHEME__',
root = document.documentElement,
var key = 'gray-theme-scheme', root = document.documentElement,
mq = window.matchMedia('(prefers-color-scheme: dark)'),
stored = null, changed = false;
try { stored = localStorage.getItem(key); } catch(e) { }
/* valid(value): Return whether value is a supported scheme. */
function valid(v) { return v == 'dark' || v == 'light'; }
/* browserScheme(): Return the scheme selected by the browser. */
function browserScheme() { return mq.matches ? 'dark' : 'light'; }
/* baseScheme(): Return the configured scheme or system preference. */
function baseScheme() { return valid(forced) ? forced : browserScheme(); }
/* getOverride(): Read this tab's override when storage is available. */
function getOverride() {
try { return sessionStorage.getItem(overrideKey); }
catch(e) { return null; }
}
/* rememberOverride(value): Store only a scheme that differs from the base. */
function rememberOverride(v) {
try {
/* Remove an override that now matches the base. */
if (v == baseScheme()) sessionStorage.removeItem(overrideKey);
/* Keep a different choice for later pages in this tab. */
else sessionStorage.setItem(overrideKey, v);
}
catch(e) { }
}
/* apply(value, [remember]): Apply a scheme and optionally cache it. */
function apply(v, remember) {
/* Ignore malformed values from storage or another frame. */
if (!valid(v)) return;
function current() { return mq.matches ? 'dark' : 'light'; }
function apply(v) {
root.setAttribute('data-scheme', v);
/* Update body selectors once the body exists. */
if (document.body) document.body.setAttribute('data-scheme', v);
/* Let newly loaded frames inherit the frameset scheme. */
if (self == top) root.style.colorScheme = v;
/* Do not cache a scheme relayed from another frame. */
if (remember !== false) {
try { localStorage.setItem(key, v); } catch(e) { }
}
try { localStorage.setItem(key, v); } catch(e) { }
}
/* grayThemeApplyScheme(value, [remember]): Apply a relayed scheme. */
window.grayThemeApplyScheme = function(v, remember) {
changed = true;
/* Save a relayed user choice as this tab's override. */
if (remember) rememberOverride(v);
apply(v, false);
};
/* applyEverywhere(value): Apply a scheme across the frameset. */
function applyEverywhere(v) {
changed = true;
rememberOverride(v);
apply(v);
try {
/* Relay a child-frame change to the frameset. */
if (top != window && top.grayThemeApplyScheme)
top.grayThemeApplyScheme(v, true);
}
catch(e) { }
/* syncFrame(name): Apply the scheme to one named frame. */
['left', 'right'].forEach(function(name) {
try {
var frame = top.frames[name];
/* Skip this document and frames that are not ready. */
if (frame && frame != window && frame.grayThemeApplyScheme)
frame.grayThemeApplyScheme(v, true);
}
catch(e) { }
});
}
/* Prefer the tab override, configured scheme, saved scheme, then system. */
var override = getOverride();
apply(valid(override) ? override :
valid(forced) ? forced :
valid(stored) ? stored : browserScheme());
/* syncBodyScheme(): Copy the root scheme to the new body. */
document.addEventListener('DOMContentLoaded', function() {
apply(root.getAttribute('data-scheme'), false);
});
/* onchange(): Follow system changes unless a tab override is active. */
function onchange() {
var override = getOverride(), current = browserScheme();
/* Preserve an override that differs from the system scheme. */
if (valid(override) && override != current) return;
/* Remove an override once the system reaches the same scheme. */
if (valid(override)) rememberOverride(current);
changed = true;
apply(current);
}
/* A configured scheme does not follow system preference changes. */
if (!valid(forced)) {
/* Prefer the current media-query listener API. */
if (mq.addEventListener) mq.addEventListener('change', onchange);
/* Fall back to the older listener API. */
else mq.addListener(onchange);
}
/* handleThemeShortcut(event): Handle Alt+T and Alt+R in any frame. */
document.addEventListener('keydown', function(event) {
/* Ignore other modifiers, repeats, composition and handled events. */
if (event.defaultPrevented || event.repeat || event.isComposing ||
!event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
return;
/* Alt+T toggles the palette throughout the frameset. */
if (event.code == 'KeyT' || event.keyCode == 84) {
event.preventDefault();
event.stopPropagation();
applyEverywhere(root.getAttribute('data-scheme') == 'dark' ?
'light' : 'dark');
}
/* Alt+R reloads content without refreshing navigation. */
else if (event.code == 'KeyR' || event.keyCode == 82) {
var frame = null;
try { frame = top.frames['right']; } catch(e) { }
/* Do not claim the browser shortcut outside the main frameset. */
if (!frame) return;
event.preventDefault();
event.stopPropagation();
try { frame.location.reload(); } catch(e) { }
}
}, true);
/* Recheck an automatic scheme after the top-level page loads. */
if (self == top && !valid(forced))
/* refreshBrowserScheme(): Recheck after all frames load. */
apply(stored == 'dark' || stored == 'light' ? stored : current());
function onchange() { changed = true; apply(current()); }
if (mq.addEventListener) mq.addEventListener('change', onchange);
else mq.addListener(onchange);
if (self == top)
window.addEventListener('load', function() {
/* refreshUnchangedScheme(): Correct untouched automatic state. */
setTimeout(function() {
/* Preserve user changes and tab overrides. */
if (!changed && !valid(getOverride()))
applyEverywhere(browserScheme());
}, 1000);
setTimeout(function() { if (!changed) apply(current()); }, 1000);
});
})();
</script>
EOF
$rv =~ s/__GRAY_THEME_FORCED_SCHEME__/$forced/g;
return $rv;
}
# theme_frame_script()
@@ -519,8 +404,6 @@ return &theme_prehead();
sub theme_ui_table_start
{
my ($heading, $tabletags, $cols, $tds, $rightheading) = @_;
# Expose the column count so CSS can size the form layout.
my $formcols = $cols || 4;
if (! $tabletags =~ /width/) { $tabletages .= " width=100%"; }
if (defined($main::ui_table_cols)) {
# Push on stack, for nested call
@@ -542,11 +425,9 @@ $rv .= "<table class='ui_table' $tabletags>\n";
if (defined($heading) || defined($rightheading)) {
# Heading row, with an optional part at the right
$rv .= "<thead><tr>";
# Add the main heading when present.
if (defined($heading)) {
$rv .= "<td><b>$heading</b></td>"
}
# Put optional actions or context at the right.
if (defined($rightheading)) {
$rv .= "<td align=right>$rightheading</td>";
$colspan++;
@@ -554,8 +435,8 @@ if (defined($heading) || defined($rightheading)) {
$rv .= "</tr></thead>\n";
}
$rv .= "<tbody> <tr class='ui_table_body'> <td colspan=$colspan>".
"<table class='ui_form_rows ui_form_rows_$formcols' width=100%>\n";
$main::ui_table_cols = $formcols;
"<table width=100%>\n";
$main::ui_table_cols = $cols || 4;
$main::ui_table_pos = 0;
$main::ui_table_default_tds = $tds;
return $rv;
@@ -690,10 +571,8 @@ my ($border) = @_;
return $border ? "</div>\n" : "";
}
# theme_ui_columns_start(&headings, [width-percent], [no-border],
# [&td-tags], [title], [sortable], [class])
# Returns HTML for a multi-column table. The sortable flag forces client-side
# sorting, and class adds a table class.
# theme_ui_columns_start(&headings, [width-percent], [noborder], [&tdtags], [title])
# Returns HTML for a multi-column table, with the given headings
sub theme_ui_columns_start
{
my ($heads, $width, $noborder, $tdtags, $title, $sortable, $class) = @_;
@@ -701,12 +580,11 @@ my ($href) = grep { $_ =~ /<a\s+href/i } @$heads;
my $rv;
$theme_ui_columns_row_toggle = 0;
if (!$noborder && !$main::COLUMNS_WRAPPER_OPEN) {
# The outermost bordered table opens the card and a container that
# scrolls wide columns without widening the page.
# The outermost bordered table opens the card
$rv .= "<table class='wrapper' width="
. ($width ? $width : "100")
. "%>\n";
$rv .= "<tr><td><div class='ui_columns_scroll'>\n";
$rv .= "<tr><td>\n";
}
if (!$noborder) {
# Nested bordered tables share that card
@@ -773,15 +651,13 @@ return $rv;
}
# theme_ui_columns_end()
# Ends a table from theme_ui_columns_start. Also closes the scroll container
# and card opened for the outermost bordered table.
# Returns HTML to end a table started by ui_columns_start
sub theme_ui_columns_end
{
my $rv;
$rv = "</tbody> </table>\n";
# Close the shared scroll container with the outermost table.
if ($main::COLUMNS_WRAPPER_OPEN == 1) {
$rv .= "</div></td> </tr> </table>\n";
if ($main::COLUMNS_WRAPPER_OPEN == 1) { # Last wrapper
$rv .= "</td> </tr> </table>\n";
}
$main::COLUMNS_WRAPPER_OPEN--;
return $rv;
@@ -982,21 +858,14 @@ else {
}
# theme_ui_links_row(&links)
# Returns links in a toolbar wrapper. Gray Theme hides the legacy separators
# and styles each link as a button.
# Returns a row of links separated by bars, in a block of its own so that
# the stylesheet can draw it as a toolbar when it sits above a table
sub theme_ui_links_row
{
my ($links) = @_;
# An empty list does not need a toolbar container.
return "" if (!$links || !@$links);
my @button_links = map {
my $link = $_;
# Drop the final period from the button without changing its source text.
$link =~ s/(?<!\.)\.(?=(?:\s*<\/[^>]+>)*\s*<\/a>\s*$)//i;
$link;
} @$links;
return "<div class='ui_links_row'>".
join(" <span class='ui_links_sep'>|</span> ", @button_links).
join(" <span class='ui_links_sep'>|</span> ", @$links).
"</div>\n";
}

15
gray-theme/top.cgi Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/local/bin/perl
# Output contents of top frame file
require "gray-theme/gray-theme-lib.pl";
$hasvirt = &foreign_available("virtual-server");
if ($hasvirt) {
%vconfig = &foreign_config("virtual-server");
}
$upperframe = $vconfig{'theme_topframe'} ||
$gconfig{'theme_topframe'};
&PrintHeader();
print &read_file_contents($upperframe);

View File

@@ -416,19 +416,12 @@ details[open].menu-cat > summary::after {
/* ---- Form controls ---- */
/* Forms and their controls shrink inside the content column. */
form.ui_form {
min-width: 0;
max-width: 100%;
}
/* Text fields, menus and text areas share one height, edge and radius */
input[type="text"], input[type="password"], input[type="email"],
input[type="number"], input[type="search"], input[type="url"],
input[type="tel"], input[type="date"], input[type="time"],
input[type="datetime-local"], input:not([type]), select, textarea {
box-sizing: border-box;
max-width: 100%;
min-height: var(--control-height);
padding: 4px 10px;
border: 1px solid var(--border-color-input);
@@ -449,29 +442,6 @@ select:not([multiple]):not([size]), select[size="1"]:not([multiple]) {
background-position: right 9px center;
background-size: 10px 10px;
}
/* Treat byte values and their unit menu as one compact control. */
.ui_bytesbox {
display: inline-flex;
align-items: stretch;
max-width: 100%;
vertical-align: middle;
}
.ui_bytesbox > input {
flex: 1 1 auto;
min-width: 0;
border-start-end-radius: 0;
border-end-end-radius: 0;
}
.ui_bytesbox > select {
flex: 0 0 auto;
margin-inline-start: -1px;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
.ui_bytesbox > :focus-visible {
position: relative;
z-index: 1;
}
/* List boxes keep their own height */
select[multiple], select[size]:not([size="1"]) {
min-height: 0;
@@ -520,11 +490,6 @@ textarea + input[type="radio"],
button + input[type="radio"] {
margin-left: 10px;
}
/* Keep controls on the line below a text area from touching it. */
textarea + br + input[type="checkbox"],
textarea + br + input[type="radio"] {
margin-top: 8px;
}
input[type="file"] { font: inherit; }
/* Labels of checkboxes inside bold cells stay plain */
.ui_form_value td > b > input + label + tt,
@@ -534,7 +499,8 @@ input[type="file"] { font: inherit; }
/* ---- Buttons ---- */
/* Form buttons use Gray Theme's compact raised surface and small radius. */
/* Every button, from the core classes to plain button tags, is a white
* pill with a gray edge */
input[type="submit"], input[type="button"], input[type="reset"], button,
input.ui_submit, input.ui_button, input.ui_reset {
display: inline-block;
@@ -576,142 +542,6 @@ input[type="reset"]:disabled, button:disabled {
cursor: default;
}
/* UI-lib actions can carry classes used by other themes. Reset the outer
* control so they cannot change Gray Theme's sizing; the inner element draws
* the visible surface. */
:is(button, a, summary).ui_action {
--ui-action-radius: var(--radius-sm);
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: auto;
height: auto;
min-width: 0;
min-height: 0;
margin: 0;
padding: 0;
border: 0;
border-radius: var(--ui-action-radius);
background: none;
box-shadow: none;
color: var(--ui-action-text);
font: inherit;
line-height: inherit;
text-decoration: none;
vertical-align: middle;
white-space: nowrap;
cursor: pointer;
}
/* Gray Theme softens UI-lib's square default to match its form controls,
* while keeping the stronger rounded and pill shape options distinct. */
:is(button, a, summary).ui_action.ui_action_rounded {
--ui-action-radius: var(--radius);
}
:is(button, a, summary).ui_action.ui_action_pill {
--ui-action-radius: 999px;
}
/* Neutral actions sit beside fields, so use the same surface and edge as
* inputs and selects in both palettes. */
:is(button, a, summary).ui_action.ui_action_neutral {
--ui-action-text: var(--text-color);
--ui-action-bg: var(--bg-color-input);
--ui-action-border: var(--border-color-input);
--ui-action-hover-text: var(--text-color);
--ui-action-hover-bg: var(--bg-color-hover);
--ui-action-hover-border: var(--text-color-disabled);
--ui-action-active-text: var(--text-color);
--ui-action-active-bg: var(--bg-color-switch);
--ui-action-active-border: var(--border-color-darker);
}
/* The inner element draws the action; the outer element handles semantics,
* keyboard focus and shape. */
:is(button, a, summary).ui_action > .ui_action_content {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
gap: 5px;
padding: 2px 10px;
border: 1px solid var(--ui-action-border);
border-radius: inherit;
background: var(--ui-action-bg);
color: inherit;
font: inherit;
font-size: 0.9em;
font-weight: 600;
line-height: 1.5;
}
/* Compact and large actions retain the shared component's size variants. */
:is(button, a, summary).ui_action.ui_action_small > .ui_action_content {
padding: 0 8px 0 7px;
font-size: 0.8em;
line-height: 1.25;
/* Let the border round separately at fractional browser zoom levels. */
box-sizing: content-box;
min-height: 1lh;
}
/* Center compact labels by their cap height without shrinking the button. */
:is(button, a, summary).ui_action.ui_action_small > .ui_action_content > span {
text-box: trim-both cap alphabetic;
}
:is(button, a, summary).ui_action.ui_action_large > .ui_action_content {
min-height: var(--control-height);
padding: 4px 12px;
font-size: 1em;
}
/* Interaction colors belong to the visible inner surface. */
:is(button, a, summary).ui_action:not(:disabled):not([aria-disabled="true"]):hover {
border: 0;
background: none;
color: var(--ui-action-hover-text);
text-decoration: none;
}
:is(button, a, summary).ui_action:not(:disabled):not([aria-disabled="true"]):hover > .ui_action_content {
background: var(--ui-action-hover-bg);
border-color: var(--ui-action-hover-border);
}
:is(button, a, summary).ui_action:not(:disabled):not([aria-disabled="true"]):active {
border: 0;
background: none;
color: var(--ui-action-active-text);
}
:is(button, a, summary).ui_action:not(:disabled):not([aria-disabled="true"]):active > .ui_action_content {
background: var(--ui-action-active-bg);
border-color: var(--ui-action-active-border);
box-shadow: inset 0 0 0 1px var(--ui-action-active-border);
}
/* Put the keyboard outline around the painted surface, not its wrapper. */
:is(button, a, summary).ui_action:focus-visible {
outline: 0;
box-shadow: none;
}
:is(button, a, summary).ui_action:focus-visible > .ui_action_content {
outline: 2px solid var(--ui-action-focus-ring);
outline-offset: 2px;
}
/* Disabled actions keep their semantic state and use disabled palette
* variables on the visible surface. */
:is(button, a, summary).ui_action:is(:disabled, [aria-disabled="true"]) {
border: 0;
background: none;
color: var(--ui-action-disabled-text);
opacity: 0.55;
cursor: not-allowed;
}
:is(button, a, summary).ui_action:is(:disabled, [aria-disabled="true"]) > .ui_action_content {
background: var(--ui-action-disabled-bg);
border-color: var(--ui-action-disabled-border);
}
/* Legacy forms can print action buttons directly instead of using a
* button-row wrapper. Keep consecutive controls from touching. */
form.ui_form > :is(input[type="submit"], input[type="button"],
input[type="reset"], button) +
:is(input[type="submit"], input[type="button"], input[type="reset"], button) {
margin-inline-start: 6px;
}
/* The first button of a form is the primary action, unless it deletes */
.ui_form_end_buttons td:first-child > input.ui_submit:not([name*="delete"]):not([name*="remove"]),
.ui_form_end_side_left .ui_form_end_buttons td:first-child > input.ui_submit,
@@ -913,7 +743,7 @@ table.header > tbody > tr > td {
color: var(--text-color);
font-size: 12.5px;
font-weight: 500;
line-height: 1.333;
line-height: 1.2;
white-space: nowrap;
text-decoration: none;
}
@@ -1105,14 +935,6 @@ table.header:has(+ .ui_post_header) {
/* Forms with a heading, tables of rows, collapsible sections, tab boxes
* and icon grids all sit in the same card */
/* Give modern widgets the theme's card spacing. Add the same gap after a
* widget group followed by legacy page content. */
body .ui_page { --ui-gap: var(--card-gap); }
.ui_page:has(~ :not(script, style, link, dialog, [hidden], .ui_hidden,
input[type="hidden"])) {
margin-bottom: var(--card-gap);
}
table.shrinkwrapper, table.wrapper, div.wrapper,
details.ui_hidden_table_start, details.section, div.ui_tabs_box {
width: 100%;
@@ -1126,29 +948,9 @@ div.wrapper, details.ui_hidden_table_start, details.section,
div.ui_tabs_box { box-sizing: border-box; }
table.shrinkwrapper > tbody > tr > td,
table.wrapper > tbody > tr > td { padding: 0; }
/* Data-table cards stay within the page. Only their columns scroll when
* their natural width is wider than the available content area. */
table.wrapper:has(> tbody > tr > td > .ui_columns_scroll) {
table-layout: fixed;
}
.ui_columns_scroll {
width: 100%;
max-width: 100%;
overflow-x: auto;
}
/* Keep form tables inside their cards regardless of control width. Column
* count classes give labels useful proportions in multi-field rows. */
table.shrinkwrapper > tbody > tr > td > .ui_table,
table.ui_form_rows { table-layout: fixed; }
table.ui_form_rows_2 > tbody > tr.ui_form_pair > td.ui_form_label:not([width]) {
width: 20%;
}
:is(table.ui_form_rows_3, table.ui_form_rows_4) > tbody > tr.ui_form_pair > td.ui_form_label:not([width]) {
width: 15%;
}
:is(table.ui_form_rows_5, table.ui_form_rows_6) > tbody > tr.ui_form_pair > td.ui_form_label:not([width]) {
width: 10%;
}
/* A fixed layout keeps the label and value columns steady whatever the
* values hold */
table.shrinkwrapper > tbody > tr > td > .ui_table { table-layout: fixed; }
/* ---- Forms with a heading (ui_table_start) ---- */
/* Markup: table.shrinkwrapper > td > table.ui_table with the heading in
@@ -1305,32 +1107,9 @@ details.ui_hidden_table_start > summary span.rightheading {
font-weight: 400;
}
details.ui_hidden_table_start > table {
display: block;
box-sizing: border-box;
width: 100%;
max-width: 100%;
padding: 8px 14px 10px;
overflow-x: auto;
}
details.ui_hidden_table_start > table > tbody {
display: table;
width: 100%;
}
/* Make a collapsible form the only horizontal scroll area. Nested column
* tables keep their natural width without adding another scrollbar. */
details.ui_hidden_table_start table.wrapper:has(.ui_columns_scroll) {
width: max-content;
min-width: 100%;
table-layout: auto;
}
details.ui_hidden_table_start .ui_columns_scroll {
width: auto;
max-width: none;
overflow-x: visible;
}
details.ui_hidden_table_start table.ui_columns {
width: max-content;
min-width: 100%;
}
.section-body { padding: 10px 16px 14px; }
/* Text above a dashboard block's content */
@@ -1548,14 +1327,6 @@ form.ui_form > table.wrapper + table.wrapper:has(> tbody > tr > td > table.ui_gr
box-shadow: var(--shadow);
}
/* A legacy card form may print its action buttons directly after the
* table. Leave the normal card gap before another form begins; the core
* may put its form-end script between the two forms. */
form.ui_form:has(> :is(table.wrapper, table.shrinkwrapper)):has(> .ui_submit):has(
+ form.ui_form, + script + form.ui_form) {
margin-bottom: var(--card-gap);
}
/* A small form of controls before another form or a table, such as the
* search box over a list of web apps or the file menu over a config
* editor, is one row of controls with the card gap under it, so it
@@ -1694,11 +1465,6 @@ td:not(:hover) .ui_link_help { visibility: hidden; }
position: relative;
vertical-align: top;
}
/* Card actions align help bubbles through ui-lib, but this theme still
* needs the bubble to anchor its generated tooltip. */
.ui_card_actions .ui_help[data-tooltip][aria-label] {
position: relative;
}
[data-tooltip][aria-label]::after {
content: "";
position: absolute;
@@ -1714,8 +1480,6 @@ td:not(:hover) .ui_link_help { visibility: hidden; }
pointer-events: none;
opacity: 0;
visibility: hidden;
/* Keep a tooltip near the top above the sticky page header. */
z-index: 10;
}
[data-tooltip][aria-label]::before {
content: attr(aria-label);
@@ -1732,13 +1496,11 @@ td:not(:hover) .ui_link_help { visibility: hidden; }
font-weight: normal;
text-align: center;
text-shadow: none;
/* Wrap between words, breaking one only when it is too long. */
overflow-wrap: anywhere;
word-break: break-all;
transform: translateX(50%);
pointer-events: none;
opacity: 0;
visibility: hidden;
z-index: 10;
}
[data-tooltip][aria-label]:hover::before,
[data-tooltip][aria-label]:hover::after {
@@ -1746,12 +1508,6 @@ td:not(:hover) .ui_link_help { visibility: hidden; }
opacity: 1;
transition-duration: 0.5s;
}
/* Opacity gives the tooltip trigger its own stacking context. Raise that
* context while the tooltip is visible, because raising its children alone
* cannot place them above the sticky header. */
[data-tooltip][aria-label]:hover {
z-index: 10;
}
/* ---- Inline disclosures (ui_details) ---- */
@@ -1841,12 +1597,10 @@ body.left-frame {
position: sticky;
top: 0;
z-index: 2;
padding-bottom: 12px;
background: var(--bg-color-frame);
border-bottom: 1px solid var(--border-color-frame);
}
/* Leave room below the product switch only when it is present. Without
* one, the top block ends at its single divider. */
.menu-top:has(> .mode) { padding-bottom: 12px; }
/* A bar bouncing along the top edge of the panel while the right frame
* loads a page; the menu script sets the class on the root */
.menu-loader {
@@ -1870,17 +1624,12 @@ html.loading-right .menu-loader { display: block; }
background: var(--accent-color);
animation: menu-bounce 0.9s ease-in-out infinite alternate;
}
/* Match Authentic's crimson progress bar for Cloudmin navigation. */
.menu-panel-cloudmin .menu-loader::before { background: #dc143c; }
@keyframes menu-bounce { to { left: 65%; } }
/* The brand block: the logo has as much room above it as below it, and
* the logo files are trimmed to their drawing, so every product sits the
* same way. The logo links to the system information page. */
.menu-brand {
padding: 14px 16px 14px;
}
/* Separate the brand details from the product switch when it exists. */
.menu-top:has(> .mode) .menu-brand {
border-bottom: 1px solid var(--border-color-frame);
}
.menu-brand-name {
@@ -2007,29 +1756,6 @@ html.loading-right .menu-loader { display: block; }
color: var(--text-color-inverse);
text-decoration: none;
}
/* Cloudmin's creation action uses a quiet orange tint and outline. Normal
* theme text stays readable without making the button dominate the menu. */
.menu-panel-cloudmin .menu-create {
background: rgba(223, 121, 53, 0.14);
box-shadow: inset 0 0 0 1px rgba(223, 121, 53, 0.42);
color: var(--text-color);
}
.menu-panel-cloudmin .menu-create:hover {
background: rgba(223, 121, 53, 0.22);
color: var(--text-color);
}
.menu-panel-cloudmin .menu-create svg { color: #df7935; }
/* Virtualmin uses the same quiet treatment with the theme's blue palette. */
.menu-panel-virtualmin .menu-create {
background: var(--element-highlight-bg-color);
box-shadow: inset 0 0 0 1px var(--accent-color-soft);
color: var(--text-color);
}
.menu-panel-virtualmin .menu-create:hover {
box-shadow: inset 0 0 0 2px var(--border-color-info);
color: var(--text-color);
}
.menu-panel-virtualmin .menu-create svg { color: var(--accent-color-hover); }
/* Forms in the menu: a caps label over a field that fills the panel */
.menu-form { margin: 0 8px 12px; }
.menu-label {
@@ -2117,9 +1843,6 @@ details.menu-cat[open] { margin-bottom: 4px; }
border: 0;
border-top: 1px solid var(--border-color-frame);
}
/* A leading separator follows hidden format metadata in modern Cloudmin,
* so it has no visible section above it to divide. */
.leftmenu > .menu-divider:first-child { display: none; }
.menu-text, .menu-html {
padding: 4px 12px;
font-size: 12px;

View File

@@ -478,6 +478,4 @@ defcert_error=Default $1 bundled SSL certificate is being used. It is highly adv
main_error_details=Error details
exempli_gratia=e.g.
__norefs=1

View File

@@ -150,7 +150,7 @@ $cmd_unpacked .= " | tac" if ($journal_tac);
my $view_title = $in{'idx'} =~ /^journal/ ?
$text{'view_titlejournal'} : $text{'view_title'};
&ui_print_header("<tt>".&html_escape($file || $cmd_unpacked)."</tt>",
&html_escape($in{'linktitle'}) || $view_title, "", undef,
$in{'linktitle'} || $view_title, "", undef,
!$no_navlinks && $skip_index,
($no_navlinks || $skip_index) ? 1 : undef,
0, $help_link);

View File

@@ -6,8 +6,7 @@ require './lpadmin-lib.pl';
&ReadParse();
print "Refresh: $config{'queue_refresh'}\r\n"
if ($config{'queue_refresh'});
&ui_print_header(&text('jobs_on', "<tt>".&html_escape($in{'name'})."</tt>"),
$text{'jobs_title'}, "");
&ui_print_header(&text('jobs_on', "<tt>$in{'name'}</tt>"), $text{'jobs_title'}, "");
@jobs = &get_jobs($in{'name'});
if (@jobs) {
@@ -34,9 +33,8 @@ if (@jobs) {
push(@cols, &html_escape($j->{'id'}));
if ($j->{'printfile'} && &can_edit_jobs($in{'name'}, $ju)) {
push(@cols,
"<a href='view_job.cgi?name=".&urlize($in{'name'}).
"&id=".&urlize($j->{'id'})."'>".
&nice_size($j->{'size'})."</a>");
"<a href='view_job.cgi?name=$in{'name'}".
"&id=$j->{'id'}'>".&nice_size($j->{'size'})."</a>");
$printfile++;
}
else {
@@ -52,8 +50,7 @@ if (@jobs) {
push(@cols, $j->{'when'});
}
if ($j->{'file'}) {
# Job titles and filenames can be supplied by print clients.
push(@cols, &html_escape($j->{'file'}));
push(@cols, $j->{'file'});
}
if (&can_edit_jobs($in{'name'}, $ju) &&
$access{'cancel'}) {
@@ -77,8 +74,7 @@ if (@jobs) {
}
}
else {
print "<b>",&text('jobs_none', "<tt>".&html_escape($in{'name'})."</tt>"),
"</b><p>\n";
print "<b>",&text('jobs_none', "<tt>$in{'name'}</tt>"),"</b><p>\n";
}
# Test print button

View File

@@ -5,12 +5,12 @@
require './lpadmin-lib.pl';
&ReadParse();
$access{'test'} || &error($text{'test_ecannot'});
&ui_print_header(&text('jobs_on', "<tt>".&html_escape($in{'name'})."</tt>"),
&ui_print_header(&text('jobs_on', "<tt>$in{'name'}</tt>"),
$text{'test_title'}, "");
print "<form action=test_print.cgi method=post enctype=multipart/form-data>\n";
print &ui_hidden("name", $in{'name'});
print &text('test_desc', "<tt>".&html_escape($in{'name'})."</tt>"),"<p>\n";
print "<input type=hidden name=name value='$in{'name'}'>\n";
print &text('test_desc', "<tt>$in{'name'}</tt>"),"<p>\n";
print "<input type=radio name=mode value=0 checked> $text{'test_0'}<br>\n";
print "<input type=radio name=mode value=1> $text{'test_1'}<br>\n";

View File

@@ -6,7 +6,7 @@ require './lpadmin-lib.pl';
&foreign_require("proc", "proc-lib.pl");
&ReadParseMime();
$access{'test'} || &error($text{'test_ecannot'});
&ui_print_header(&text('jobs_on', "<tt>".&html_escape($in{'name'})."</tt>"),
&ui_print_header(&text('jobs_on', "<tt>$in{'name'}</tt>"),
$text{'test_title'}, "");
if ($in{'mode'} == 0) {

View File

@@ -1019,15 +1019,14 @@ elsif ($sm) {
if ($ssl == 2) {
# Request a TLS upgrade with STARTTLS
my $rv = &smtp_command($h, "starttls\r\n", 1);
if ($rv =~ /^220(?:[ -]|$)/) {
if ($rv =~ /^2\d+/) {
# Enable TLS and repeat the greeting on the secure connection
&switch_smtp_to_ssl($h);
&smtp_command($h, $helocmd, 0);
}
else {
# Never send credentials or mail without the requested TLS.
&close_http_connection($h);
&error("SMTP STARTTLS failed : ".&html_escape($rv));
# Continue without TLS if STARTTLS is unavailable
$ssl = 0;
}
}

View File

@@ -68,41 +68,40 @@ if ($in{'scale'}) {
else {
# Just output the attachment
print "X-no-links: 1\n";
print "X-Content-Type-Options: nosniff\n";
$fn = $attach->{'filename'} ? &decode_mimewords($attach->{'filename'})
: "attachment";
$fn =~ s/[\r\n\0"\\\/]//g;
$fn ||= "attachment";
my $type = lc($attach->{'type'} || 'application/octet-stream');
my $download = $in{'save'};
if ($download) {
# Downloads must keep the original bytes without rendering them.
$type = 'application/octet-stream';
@download = split(/\t+/, $config{'download'});
if ($attach->{'type'} =~ /^image\/svg(\+xml)?/i ||
$in{'type'} =~ /^image\/svg(\+xml)?/i ||
$fn =~ /\.svgz?$/i) {
# SVG can execute scripts when served from the Webmin origin.
print "Content-Disposition: Attachment; filename=\"$fn\"\n"
if ($in{'save'});
print "Content-type: text/plain\n\n";
print $attach->{'data'};
}
elsif ($in{'type'} || $type =~ /(?:\/|\+)xml\z/ ||
$type eq 'image/svg' || $type eq 'message/delivery-status' ||
$fn =~ /\.svgz?$/i) {
# Render XML/SVG and the UI's plain-text override as inert text.
$type = 'text/plain';
elsif ($in{'type'}) {
# Display as a specific MIME type
print "Content-type: $in{'type'}\n\n";
print $attach->{'data'};
}
else {
# Auto-detect type
if ($in{'save'}) {
# Force download
print "Content-Disposition: Attachment; filename=\"$fn\"\n";
}
if ($attach->{'type'} eq 'message/delivery-status') {
print "Content-type: text/plain\n\n";
}
else {
print "Content-type: $attach->{'type'}\n\n";
}
}
elsif ($type eq 'text/html') {
# Keep HTML previews isolated even if the text filters miss markup.
print "Content-Security-Policy: sandbox; script-src 'none'; ".
"object-src 'none'; frame-src 'none'; base-uri 'none'; ".
"form-action 'none'\n";
}
elsif ($type =~ /^text\//) {
# Other text formats must not be interpreted as active content.
$type = 'text/plain';
}
elsif ($type !~ /\Aimage\/(?:gif|jpeg|png|webp|bmp|avif|x-icon|vnd\.microsoft\.icon)\z/) {
# Only known raster images may be served inline without a sandbox.
$type = 'application/octet-stream';
$download = 1;
}
print "Content-Disposition: Attachment; filename=\"$fn\"\n" if ($download);
print "Content-type: $type\n\n";
if ($type eq 'text/html') {
if ($attach->{'type'} =~ /^(text\/html|image\/svg|application\/xhtml)/i
&& !$in{'save'}) {
print &safe_urls(&filter_javascript($attach->{'data'}));
}
else {

View File

@@ -351,8 +351,6 @@ sform_and=Find messages matching all criteria below ..
sform_or=Find messages matches any criteria below ..
sform_neg0=contains
sform_neg1=doesn't contain
sform_larger=larger than
sform_smaller=smaller than
sform_ok=Search Now
sform_folder2=Search in folder(s)
sform_all=&lt;All folders&gt;

View File

@@ -11,27 +11,6 @@ require './mailboxes-lib.pl';
&ui_print_header(undef, $text{'sform_title'}, "", undef, 0, 0, undef,
&folder_link($in{'user'}, $folder));
# Escape localized labels before inserting them into JavaScript strings.
my $sform_larger_js = &quote_javascript($text{'sform_larger'});
my $sform_smaller_js = &quote_javascript($text{'sform_smaller'});
my $sform_neg0_js = &quote_javascript($text{'sform_neg0'});
my $sform_neg1_js = &quote_javascript($text{'sform_neg1'});
print <<EOF;
<script>
function set_neg(mode, negfield)
{
if (mode == "size") {
negfield.options[0].text = "$sform_larger_js";
negfield.options[1].text = "$sform_smaller_js";
}
else {
negfield.options[0].text = "$sform_neg0_js";
negfield.options[1].text = "$sform_neg1_js";
}
}
</script>
EOF
# Start of form
print &ui_form_start("mail_search.cgi");
print &ui_hidden("user", $in{'user'});
@@ -54,8 +33,7 @@ for($i=0; $i<=4; $i++) {
map { [ $_, $_ eq 'all' ? $text{'sform_allmsg'}
: $text{"sform_".$_} ] }
( 'from', 'subject', 'to', 'cc', 'date',
'body', 'headers', 'all', 'size') ],
1, 0, 0, undef, "onChange='set_neg(value, form.neg_$i)'"));
'body', 'headers', 'all', 'size') ]));
push(@cols, &ui_select("neg_$i", 0,
[ [ 0, $text{'sform_neg0'} ],

View File

@@ -1565,23 +1565,8 @@ if (&get_type($full) eq "internal/cgi" && $validated != 4) {
$cgiheader{lc($1)} = $2;
push(@cgiheader, [ $1, $2 ]);
}
# A CGI can reject a request before producing normal
# output. Use its explicit status instead of the default
# success code.
my $cgi_status = $cgiheader{'status'} =~
/\A([1-5][0-9][0-9])\s+([^\r\n]+)\z/;
if ($cgi_status) {
($ok_code, $ok_message) = ($1, $2);
}
else {
# Invalid control headers must not affect
# redirect logging.
delete($cgiheader{'status'});
}
if ($cgiheader{"location"}) {
&write_data("HTTP/1.0 ".($cgi_status
? "$ok_code $ok_message"
: "302 Moved Temporarily")."\r\n");
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_keep_alive(0);
@@ -1603,7 +1588,6 @@ if (&get_type($full) eq "internal/cgi" && $validated != 4) {
&write_keep_alive(0);
}
foreach $h (@cgiheader) {
next if (lc($h->[0]) eq 'status');
&write_data("$h->[0]: $h->[1]\r\n");
}
&write_data("\r\n");
@@ -1687,9 +1671,7 @@ else {
# log the request
&log_request($loghost, $authuser, $reqline,
$logged_code ? $logged_code :
$cgiheader{"location"} && !$cgiheader{"status"}
? "302"
: $ok_code, &byte_count());
$cgiheader{"location"} ? "302" : $ok_code, &byte_count());
return $rv;
}
@@ -2401,31 +2383,18 @@ $miniserv::page_capture_out .= $buf
if ($miniserv::page_capture);
}
# READ(handle, buffer, length, [offset])
# Read buffered POST data or SOCK, replacing the buffer tail like Perl read.
# Send back already read data while we have it, then read from SOCK
sub READ
{
my $r = shift;
my $bufref = \$_[0];
my $len = $_[1];
my $offset = $_[2] || 0;
die "Negative length" if ($len < 0);
$$bufref = '' if (!defined($$bufref));
# Resolve offsets before consuming input, and pad beyond the old buffer end.
$offset += length($$bufref) if ($offset < 0);
die "Offset outside string" if ($offset < 0);
$$bufref .= "\0" x ($offset - length($$bufref))
if ($offset > length($$bufref));
# A zero-length read must not wait for network input.
if (!$len) {
substr($$bufref, $offset) = '';
return 0;
}
my $offset = $_[2];
if ($postpos < length($postinput)) {
# Reading from already fetched array
my $left = length($postinput) - $postpos;
my $canread = $len > $left ? $left : $len;
substr($$bufref, $offset) =
substr($$bufref, $offset, $canread) =
substr($postinput, $postpos, $canread);
$postpos += $canread;
return $canread;
@@ -2437,7 +2406,7 @@ else {
# End of socket
shutdown(SOCK, 0);
}
substr($$bufref, $offset) = $data;
substr($$bufref, $offset, length($data)) = $data;
return length($data);
}
}
@@ -2517,22 +2486,9 @@ foreach $d (@_) {
}
}
if ($doneheaders) {
# Match forked CGI status handling for in-process
# scripts.
my $cgi_status = $cgiheader{'status'} =~
/\A([1-5][0-9][0-9])\s+([^\r\n]+)\z/;
if ($cgi_status) {
($ok_code, $ok_message) = ($1, $2);
}
else {
# Invalid control headers must not affect
# redirect logging.
delete($cgiheader{'status'});
}
if ($cgiheader{"location"}) {
&write_data("HTTP/1.0 ".($cgi_status
? "$ok_code $ok_message"
: "302 Moved Temporarily")."\r\n");
&write_data(
"HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_keep_alive(0);
@@ -2547,7 +2503,6 @@ foreach $d (@_) {
&write_keep_alive(0);
}
foreach $h (@cgiheader) {
next if (lc($h->[0]) eq 'status');
&write_data("$h->[0]: $h->[1]\r\n");
}
&write_data("\r\n");
@@ -2585,9 +2540,7 @@ if ($doing_cgi_eval && $$ == $main_process_id) {
close(SOCK);
close($PASSINw); close($PASSOUTw);
&log_request($loghost, $authuser, $reqline,
$cgiheader{"location"} && !$cgiheader{"status"}
? "302"
: $ok_code, &byte_count());
$cgiheader{"location"} ? "302" : $ok_code, &byte_count());
}
}

View File

@@ -1 +1 @@
acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 hardware-info heartbeat hpuxexports htaccess-htpasswd idmapd inetd init systemd inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam sqlite squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones
acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 hardware-info heartbeat hpuxexports htaccess-htpasswd idmapd inetd init systemd inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones

View File

@@ -23,30 +23,16 @@ if ($in{'dest'} == 1 && $in{'all'}) {
if ($in{'dest'} == 1 && ($in{'sched'} || $in{'save'})) {
&error($text{'backup_edownloadsave'});
}
# Apply the path restriction to both immediate and saved backups.
if (!$in{'dest'} && !&is_under_directory($access{'bpath'}, $in{'file'})) {
&error($text{'backup_epath'}."<br>".
&text('backup_eunder', "<tt>$access{'bpath'}</tt>"));
}
# Dump options can create files directly and bypass output restrictions.
if ($in{'parameters'} &&
($access{'bpath'} ne '/' || $access{'buser'} ne 'root')) {
&error($text{'backup_eparameters'});
}
# Validate fields required to run a backup now or from a schedule.
if (!$in{'save'} || $in{'sched'}) {
if ($in{'all'}) {
# All-database backups write one file per database into a directory.
-d $in{'file'} || -d &date_subs($in{'file'}) || $in{'mkdir'} ||
&error($text{'backup_efile2'});
}
else {
# A single-database file backup needs an absolute destination.
$in{'dest'} || $in{'file'} =~ /^\/\S+$/ ||
&error($text{'backup_efile'});
}
# Check optional filters, character sets, and compression programs.
$in{'where_def'} || $in{'where'} || &error($text{'backup_ewhere'});
$in{'charset_def'} || $in{'charset'} =~ /^\S+$/ ||
&error($text{'backup_echarset'});
@@ -54,8 +40,11 @@ if (!$in{'save'} || $in{'sched'}) {
$in{'compress'} == 2 ? "bzip2" : undef;
!$ccmd || &has_command($ccmd) ||
&error(&text('backup_eccmd', "<tt>$ccmd</tt>"));
if (!$in{'dest'} && !&is_under_directory($access{'bpath'}, $in{'file'})) {
&error($text{'backup_epath'}."<br>".
&text('backup_eunder', "<tt>$access{'bpath'}</tt>"));
}
if (!$in{'all'} && !$in{'tables_def'}) {
# A custom table selection must include at least one table.
@tables = split(/\0/, $in{'tables'});
@tables || &error($text{'backup_etables'});
}
@@ -64,10 +53,6 @@ if (!$in{'save'} || $in{'sched'}) {
push(@compat, split(/\0/, $in{'options'}));
$cron = !$module_info{'usermin'} && $access{'buser'} eq 'root' &&
!$access{'user'} && &foreign_installed("cron");
# Only root-backup schedulers may save shared settings.
if (($in{'save'} || $in{'sched'}) && !$cron) {
&error($text{'backup_esave'});
}
$commands = &can_run_backup_commands($cron);
$cmode = 0;
if ($commands) {
@@ -84,22 +69,12 @@ if ($cron) {
&foreign_require("cron");
@jobs = &cron::list_cron_jobs();
if ($in{'all'}) {
($job) = grep { $_->{'command'} eq "$cron_cmd --all" } @jobs;
}
else {
($job) = grep {
$_->{'command'} eq $cron_cmd." ".$in{'db'} ||
$_->{'command'} eq $cron_cmd." ".quotemeta($in{'db'}) }
@jobs;
}
$cmd = $in{'all'} ? "$cron_cmd --all" : "$cron_cmd $in{'db'}";
($job) = grep { $_->{'command'} eq $cmd } @jobs;
$oldjob = $job;
$cmd = $in{'all'} ? "$cron_cmd --all"
: $cron_cmd." ".quotemeta($in{'db'});
$job ||= { 'user' => 'root',
$job ||= { 'command' => $cmd,
'user' => 'root',
'active' => 1 };
# Replace legacy unquoted commands when updating existing jobs.
$job->{'command'} = $cmd;
&cron::parse_times_input($job, \%in);
}
@@ -243,8 +218,8 @@ if (!$in{'save'}) {
}
elsif (!$in{'dest'}) {
@st = stat($file);
print &text('backup_done', "<tt>".&html_escape($db)."</tt>",
"<tt>".&html_escape($file)."</tt>", int($st[7])),"<p>\n";
print &text('backup_done', "<tt>$db</tt>",
"<tt>$file</tt>", int($st[7])),"<p>\n";
}
&execute_after($db, STDOUT, 1, $file, $in{'all'} ? undef : $db,
$access{'buser'})
@@ -302,7 +277,7 @@ if (!$in{'dest'}) {
}
else {
&ui_print_footer(
"/$module_name/edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}
}

View File

@@ -22,7 +22,7 @@ $access{'buser'} || &error($text{'dbase_ecannot'});
if (!-x $cmd) {
print &text('backup_edump', "<tt>$cmd</tt>",
"../config.cgi?$module_name"),"<p>\n";
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'});
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'});
exit;
}
@@ -33,7 +33,7 @@ if ($in{'all'}) {
print "$text{'backup_desc3'}\n";
}
else {
print &text('backup_desc', "<tt>".&html_escape($in{'db'})."</tt>"),"\n";
print &text('backup_desc', "<tt>$in{'db'}</tt>"),"\n";
}
if ($cron) {
print "$text{'backup_desc2'}\n";
@@ -42,7 +42,7 @@ print "<p>\n";
%c = $module_info{'usermin'} ? %userconfig : %config;
$download = $in{'all'} ? 'database' : $in{'db'};
print &ui_form_start("backup_db.cgi/".&urlize($download).".sql", "post");
print &ui_form_start("backup_db.cgi/$download.sql", "post");
print &ui_hidden("db", $in{'db'});
print &ui_hidden("all", $in{'all'});
print &ui_hidden_table_start($text{'backup_header1'}, "width=100%", 2, "main",
@@ -152,12 +152,10 @@ $q = $c{'backup_quick_'.$in{'db'}};
print &ui_table_row($text{'backup_quick'},
&ui_yesno_radio("quick", $q ? 1 : 0));
# Show custom dump options only to unrestricted root backup users.
if ($access{'bpath'} eq '/' && $access{'buser'} eq 'root') {
$b = $c{'backup_parameters_'.$in{'db'}};
print &ui_table_row($text{'backup_parameters'},
&ui_textbox("parameters", $b, 60));
}
# Allow user to specify custom backup parameters
$b = $c{'backup_parameters_'.$in{'db'}};
print &ui_table_row($text{'backup_parameters'},
&ui_textbox("parameters", $b, 60));
if ($commands) {
# Show before/after commands
@@ -201,15 +199,8 @@ if ($cron) {
# Show cron time
&foreign_require("cron", "cron-lib.pl");
@jobs = &cron::list_cron_jobs();
if ($in{'all'}) {
($job) = grep { $_->{'command'} eq "$cron_cmd --all" } @jobs;
}
else {
($job) = grep {
$_->{'command'} eq $cron_cmd." ".$in{'db'} ||
$_->{'command'} eq $cron_cmd." ".quotemeta($in{'db'}) }
@jobs;
}
$cmd = $in{'all'} ? "$cron_cmd --all" : "$cron_cmd $in{'db'}";
($job) = grep { $_->{'command'} eq $cmd } @jobs;
print &ui_table_row($text{'backup_sched'},
&ui_radio("sched", $job ? 1 : 0,
@@ -237,7 +228,7 @@ if ($in{'all'}) {
&ui_print_footer("", $text{'index_return'});
}
else {
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -64,16 +64,15 @@ foreach $r (@{$rv->{'data'}}) {
# All done .. tell the user
if ($in{'dest'}) {
&close_tempfile(OUT);
$desc = &text('table_header', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $text{'csv_title'}, "", "csv");
@st = stat($in{'file'});
print &text('csv_done', "<tt>".&html_escape($in{'file'})."</tt>",
print &text('csv_done', "<tt>$in{'file'}</tt>",
&nice_size($st[7])),"<p>\n";
&ui_print_footer(
"@{[&get_webprefix()]}/$module_name/edit_dbase.cgi?db=".&urlize($in{'db'}),
"@{[&get_webprefix()]}/$module_name/edit_dbase.cgi?db=$in{'db'}",
$text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -6,11 +6,10 @@ require './mysql-lib.pl';
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
$access{'edonly'} && &error($text{'dbase_ecannot'});
$desc = &text('table_header', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $text{'csv_title'}, "", "csv");
print &ui_form_start("csv.cgi/".&urlize($in{'table'}).".csv", "post");
print &ui_form_start("csv.cgi/$in{'table'}.csv", "post");
print &ui_hidden("db", $in{'db'}),"\n";
print &ui_hidden("table", $in{'table'}),"\n";
print &ui_table_start($text{'csv_header'}, undef, 2);
@@ -46,5 +45,5 @@ print &ui_table_row($text{'csv_cols'},
print &ui_table_end();
print &ui_form_end([ [ "ok", $text{'csv_ok'} ] ]);
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -21,7 +21,7 @@ elsif ($in{'empty'}) {
&execute_sql_logged($in{'db'}, "drop table ".&quotestr($t));
}
&webmin_log("delete", "db", $in{'db'});
&redirect("edit_dbase.cgi?db=".&urlize($in{'db'}));
&redirect("edit_dbase.cgi?db=$in{'db'}");
}
else {
# Ask the user if he is sure..
@@ -35,7 +35,7 @@ else {
if (!$access{'delete'}) {
# Offer to drop tables only
$msg = &text('ddrop_rusure2', "<tt>".&html_escape($in{'db'})."</tt>", scalar(@tables), $rows);
$msg = &text('ddrop_rusure2', "<tt>$in{'db'}</tt>", scalar(@tables), $rows);
$msg .= " ".$text{'ddrop_mysql'} if ($in{'db'} eq $master_db);
print &ui_confirmation_form(
"drop_dbase.cgi", $msg,
@@ -45,7 +45,7 @@ else {
}
else {
# Offer to drop DB or tables
$msg = &text('ddrop_rusure', "<tt>".&html_escape($in{'db'})."</tt>", scalar(@tables), $rows);
$msg = &text('ddrop_rusure', "<tt>$in{'db'}</tt>", scalar(@tables), $rows);
$msg .= " ".$text{'ddrop_mysql'} if ($in{'db'} eq $master_db);
print &ui_confirmation_form(
"drop_dbase.cgi", $msg,
@@ -56,7 +56,7 @@ else {
);
}
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -11,7 +11,7 @@ if ($in{'confirm'}) {
&error_setup($text{'tdrop_err'});
&execute_sql_logged($in{'db'}, "drop table ".&quotestr($in{'table'}));
&webmin_log("delete", "table", $in{'table'}, \%in);
&redirect("edit_dbase.cgi?db=".&urlize($in{'db'}));
&redirect("edit_dbase.cgi?db=$in{'db'}");
}
else {
# Ask the user if he is sure..
@@ -21,19 +21,16 @@ else {
"select count(*) from ".&quotestr($in{'table'}));
$rows = $d->{'data'}->[0]->[0];
print "<center>\n";
print "<b>",&text('tdrop_rusure',
"<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>", $rows),"</b><p>\n";
print &ui_form_start("drop_table.cgi");
print &ui_hidden("db", $in{'db'});
print &ui_hidden("table", $in{'table'});
print &ui_form_end([ [ "confirm", $text{'tdrop_ok'} ] ]);
print "</center>\n";
&ui_print_footer(
"edit_table.cgi?db=".&urlize($in{'db'})."&table=".&urlize($in{'table'}),
print "<center><b>",&text('tdrop_rusure', "<tt>$in{'table'}</tt>",
"<tt>$in{'db'}</tt>", $rows),"</b><p>\n";
print "<form action=drop_table.cgi>\n";
print "<input type=hidden name=db value='$in{'db'}'>\n";
print "<input type=hidden name=table value='$in{'table'}'>\n";
print "<input type=submit name=confirm value='$text{'tdrop_ok'}'>\n";
print "</form></center>\n";
&ui_print_footer("edit_table.cgi?db=$in{'db'}&table=$in{'table'}",
$text{'table_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -27,7 +27,7 @@ if ($in{'confirm'}) {
&execute_sql_logged($in{'db'}, "drop table ".&quotestr($t));
}
&webmin_log("delete", "tables", scalar(@tables), \%in);
&redirect("edit_dbase.cgi?db=".&urlize($in{'db'}));
&redirect("edit_dbase.cgi?db=$in{'db'}");
}
else {
# Ask the user if he is sure..
@@ -42,16 +42,16 @@ else {
$msg = defined($rows) ? 'tdrops_rusure' : 'tdrops_rusure2';
print "<center><b>", &text($msg, scalar(@tables),
"<tt>".&html_escape($in{'db'})."</tt>", $rows),"</b><p>\n";
"<tt>$in{'db'}</tt>", $rows),"</b><p>\n";
print "<form action=drop_tables.cgi>\n";
print &ui_hidden("db", $in{'db'}),"\n";
print "<input type=hidden name=db value='$in{'db'}'>\n";
print "<input type=submit name=confirm value='$text{'tdrops_ok'}'>\n";
foreach $t (@tables) {
print &ui_hidden("d", $t),"\n";
}
print "</form></center>\n";
&ui_print_footer(
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -47,8 +47,8 @@ else {
print &ui_table_start($text{'cpriv_header'}, undef, 2);
# Apply to DB and table
print &ui_table_row($text{'cpriv_db'}, "<tt>".&html_escape($d)."</tt>");
print &ui_table_row($text{'cpriv_table'}, "<tt>".&html_escape($t)."</tt>");
print &ui_table_row($text{'cpriv_db'}, "<tt>$d</tt>");
print &ui_table_row($text{'cpriv_table'}, "<tt>$t</tt>");
# Table field
print &ui_table_row($text{'cpriv_field'},

View File

@@ -6,7 +6,7 @@ require './mysql-lib.pl';
&ReadParse();
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
@titles = grep { &can_edit_db($_) } &list_databases();
$desc = "<tt>".&html_escape($in{'db'})."</tt>";
$desc = "<tt>$in{'db'}</tt>";
if (@titles == 1 && $module_info{'usermin'}) {
# Single-DB mode
&ui_print_header($desc, $text{'dbase_title'}, "", "edit_dbase", 1, 1);
@@ -30,9 +30,8 @@ if ($in{'search'}) {
@views = grep { /\Q$in{'search'}\E/i } @views;
print "<table width=100%><tr>\n";
print "<td> <b>",&text('dbase_showing',
"<tt>".&html_escape($in{'search'})."</tt>"),"</b></td>\n";
print "<td align=right><a href='edit_dbase.cgi?db=",
&urlize($in{'db'}),"'>",
"<tt>$in{'search'}</tt>"),"</b></td>\n";
print "<td align=right><a href='edit_dbase.cgi?db=$in{'db'}'>",
"$text{'view_searchreset'}</a></td>\n";
print "</tr></table>\n";
}
@@ -83,11 +82,11 @@ elsif (@titles || @indexes) {
( map { "images/index.gif" } @indexes ),
( map { "images/view.gif" } @views ),
);
@links = ( ( map { "edit_table.cgi?db=".&urlize($in{'db'})."&table=".&urlize($_) }
@links = ( ( map { "edit_table.cgi?db=$in{'db'}&table=".&urlize($_) }
@titles ),
( map { "edit_index.cgi?db=".&urlize($in{'db'})."&index=".&urlize($_) }
( map { "edit_index.cgi?db=$in{'db'}&index=".&urlize($_) }
@indexes ),
( map { "edit_view.cgi?db=".&urlize($in{'db'})."&view=".&urlize($_) }
( map { "edit_view.cgi?db=$in{'db'}&view=".&urlize($_) }
@views ),
);
@descs = ( ( map { "" } @titles ),

View File

@@ -6,8 +6,7 @@ require './mysql-lib.pl';
&ReadParse();
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
$access{'edonly'} && &error($text{'dbase_ecannot'});
$desc = &text('field_in', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('field_in', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
if ($in{'type'}) {
# Creating a new field
&ui_print_header($desc, $text{'field_title1'}, "", "create_field");
@@ -42,7 +41,7 @@ if ($type =~ /^(\S+)\((.*)\)(.*)/) {
print &ui_hidden("type", $type);
if ($in{'type'}) {
# New field .. just show chosen type
$tsel = &html_escape($type);
$tsel = $type;
}
else {
# Existing field .. allow type change
@@ -155,9 +154,8 @@ else {
@desc > 1 ? ( [ 'delete', $text{'delete'} ] ): ( ) ]);
}
&ui_print_footer("edit_table.cgi?db=".&urlize($in{'db'}).
"&table=".&urlize($in{'table'}),
&ui_print_footer("edit_table.cgi?db=$in{'db'}&table=".&urlize($in{'table'}),
$text{'table_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -16,8 +16,7 @@ else {
# Creating an index
$table = $in{'table'};
}
$desc = &text('table_header', "<tt>".&html_escape($table)."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$table</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $in{'index'} ? $text{'index_title2'}
: $text{'index_title1'}, "");
@@ -57,5 +56,5 @@ else {
print &ui_form_end([ [ "create", $text{'create'} ] ]);
}
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -6,12 +6,10 @@ require './mysql-lib.pl';
&ReadParse();
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
if ($access{'edonly'}) {
&redirect("view_table.cgi?db=".&urlize($in{'db'}).
"&table=".&urlize($in{'table'}));
&redirect("view_table.cgi?db=$in{'db'}&table=".&urlize($in{'table'}));
exit;
}
$desc = &text('table_header', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $text{'table_title'}, "", "edit_table");
print &ui_form_start("delete_fields.cgi", "post");
@@ -31,7 +29,7 @@ print &ui_columns_start([ "",
$i = 0;
foreach $r (@desc) {
local @cols;
push(@cols, "<a href='edit_field.cgi?db=".&urlize($in{'db'})."&table=".
push(@cols, "<a href='edit_field.cgi?db=$in{'db'}&table=".
&urlize($in{'table'})."&".
"idx=$i'>".&html_escape($r->{'field'})."</a>");
push(@cols, &html_escape($r->{'type'}));
@@ -98,6 +96,6 @@ print "</td>\n";
print "</tr></table>\n";
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -30,7 +30,7 @@ else {
print &ui_table_start($text{'tpriv_header'}, undef, 2);
# Apply to DB
print &ui_table_row($text{'tpriv_db'}, &html_escape($in{'db'} || $u->[1]));
print &ui_table_row($text{'tpriv_db'}, $in{'db'} || $u->[1]);
# Apply to table
print &ui_table_row($text{'tpriv_table'},

View File

@@ -11,7 +11,7 @@ if ($in{'view'}) {
# Editing a view
$str = &view_structure($in{'db'}, $in{'view'});
}
$desc = "<tt>".&html_escape($in{'db'})."</tt>";
$desc = "<tt>$in{'db'}</tt>";
&ui_print_header($desc, $in{'view'} ? $text{'view_title2'}
: $text{'view_title1'}, "");
@@ -22,7 +22,7 @@ print &ui_table_start($text{'view_header1'}, undef, 2);
# View name
print &ui_table_row($text{'view_name'},
$in{'view'} ? "<tt>".&html_escape($str->{'name'})."</tt>" :
$in{'view'} ? "<tt>$str->{'name'}</tt>" :
&ui_textbox("name", $str->{'name'}, 20));
# Selection query
@@ -65,6 +65,6 @@ else {
print &ui_form_end([ [ "create", $text{'create'} ] ]);
}
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -203,7 +203,7 @@ print &ui_tag('pre', &html_escape($detail),
if ($in{'clear'}) {
# Delete the history file
&unlink_file($commands_file.".".$in{'db'});
&redirect("exec_form.cgi?db=".&urlize($in{'db'}));
&redirect("exec_form.cgi?db=$in{'db'}");
}
else {
# Run some SQL
@@ -262,7 +262,7 @@ else {
if (@data) {
print &text('exec_out',
"<tt>".&html_escape($cmd)."</tt>"),"<p>\n";
print &ui_columns_start([ map { &html_escape($_) } @{$d->{'titles'}} ]);
print &ui_columns_start($d->{'titles'});
foreach $r (@data) {
print &ui_columns_row([
map { &html_escape($_) } @$r ]);
@@ -293,8 +293,8 @@ else {
}
&webmin_log("exec", undef, $in{'db'}, \%in);
&ui_print_footer("exec_form.cgi?db=".&urlize($in{'db'}), $text{'exec_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("exec_form.cgi?db=$in{'db'}", $text{'exec_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}

View File

@@ -31,7 +31,7 @@ else {
&mysql_command_unix_user()) ||
&error($text{'exec_efile'});
&ui_print_header(undef, $text{'exec_title'}, "");
print &text('exec_fileout', "<tt>".&html_escape($in{'file'})."</tt>"),"<p>\n";
print &text('exec_fileout', "<tt>$in{'file'}</tt>"),"<p>\n";
$need_unlink = 1;
}
@@ -102,7 +102,7 @@ if (!$ex) {
'file' => $in{'file'} });
unlink($file) if ($need_unlink);
&ui_print_footer("exec_form.cgi?db=".&urlize($in{'db'})."&mode=file", $text{'exec_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("exec_form.cgi?db=$in{'db'}&mode=file", $text{'exec_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -31,7 +31,7 @@ $csel = &ui_select("charset", $sql_charset,
# Form for executing an SQL command
print &ui_tabs_start_tab("mode", "exec");
print &text('exec_header', "<tt>".&html_escape($in{'db'})."</tt>"),"<p>\n";
print &text('exec_header', "<tt>$in{'db'}</tt>"),"<p>\n";
print &ui_form_start("exec.cgi", "form-data");
print &ui_hidden("db", $in{'db'});
print &ui_textarea("cmd", undef, 10, 70),"<br>\n";
@@ -58,7 +58,7 @@ print &ui_tabs_end_tab();
# Form for executing commands from a file
print &ui_tabs_start_tab("mode", "file");
print &text('exec_header2', "<tt>".&html_escape($in{'db'})."</tt>"),"<br>\n";
print &text('exec_header2', "<tt>$in{'db'}</tt>"),"<br>\n";
print $text{'exec_header2a'},"<p>\n";
print &ui_form_start("exec_file.cgi", "form-data");
print &ui_hidden("db", $in{'db'});
@@ -72,7 +72,7 @@ print &ui_tabs_end_tab();
# Form for loading a CSV or tab-separated file
print &ui_tabs_start_tab("mode", "import");
print &text('exec_header3', "<tt>".&html_escape($in{'db'})."</tt>"),"<br>",
print &text('exec_header3', "<tt>$in{'db'}</tt>"),"<br>",
$text{'exec_header4'},"<p>\n";
print &ui_form_start("import.cgi", "form-data");
@@ -110,7 +110,7 @@ print &ui_tabs_end_tab();
print &ui_tabs_end(1);
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -2,7 +2,6 @@
# import.cgi
# Import data from a text file
use Fcntl qw(O_WRONLY O_CREAT O_EXCL);
require './mysql-lib.pl';
&ReadParseMime();
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
@@ -10,24 +9,13 @@ $access{'edonly'} && &error($text{'dbase_ecannot'});
&error_setup($text{'import_err'});
$sql_charset = $in{'charset'};
# Use server-visible storage, not a /tmp hidden by the database service.
# If the server's directories are not on this host, keep the usual temp path.
my $import_tmpdir;
if ($< == 0 && &supports_users() && &is_mysql_local()) {
$import_tmpdir = &mysql_import_tempdir($in{'db'});
}
if ($in{'mode'}) {
# From uploaded file
$in{'upload'} || &error($text{'import_eupload'});
$in{'upload_filename'} =~ /([^\/\\\s]+)$/ ||
&error($text{'import_eupload'});
$file = &mysql_import_tempname($import_tmpdir, $1);
# Create a new upload file. Keep it owner-only when the server will be
# granted access below; otherwise the umask applies as before.
sysopen(TEMP, $file, O_WRONLY | O_CREAT | O_EXCL,
$import_tmpdir ? 0600 : 0666) ||
&error(&text('efileopen', &html_escape($file), &html_escape("$!")));
$file = &transname($1);
open(TEMP, ">$file");
print TEMP $in{'upload'};
close(TEMP);
&set_mysql_command_file_permissions($file) ||
@@ -38,18 +26,18 @@ if ($in{'mode'}) {
}
else {
# From local file
$file = &mysql_import_tempname($import_tmpdir);
$file = &transname();
&copy_file_under_global_acl($in{'file'}, $file, undef,
&mysql_command_unix_user()) ||
&error($text{'import_efile'});
$need_unlink = 1;
&ui_print_header(undef, $text{'import_title'}, "");
print &text('import_fileout', "<tt>".&html_escape($in{'file'})."</tt>"),"<p>\n";
print &text('import_fileout', "<tt>$in{'file'}</tt>"),"<p>\n";
}
# Build the import command
if ($in{'table'}) {
$nfile = &mysql_import_tempname($import_tmpdir, "$in{'table'}.txt");
$nfile = &transname("$in{'table'}.txt");
&copy_source_dest($file, $nfile);
&set_mysql_command_file_permissions($nfile) ||
&error($text{'import_efile'});
@@ -57,11 +45,6 @@ if ($in{'table'}) {
$file = $nfile;
$need_unlink = 1;
}
# A server-side import needs the local database server to read the file
if ($< == 0 && &supports_users() && &is_mysql_local()) {
&set_mysql_import_file_permissions($in{'db'}, $file) ||
&error($text{'import_eperms'});
}
$delete = $in{'delete'} ? "-d" : "";
$ignore = $in{'ignore'} ? "-i" : "";
if ($in{'format'} == 0) {
@@ -89,5 +72,5 @@ print "</pre>\n";
'file' => $in{'file'} });
unlink($file) if ($need_unlink);
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'});
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'});

View File

@@ -136,7 +136,7 @@ else {
if (@titles == 1 && $access{'dbs'} ne '*' && !$access{'perms'} &&
!$access{'stop'} && !$access{'create'} && $access{'noconfig'}) {
# Only one DB, so go direct to it!
&redirect("edit_dbase.cgi?db=".&urlize($titles[0]));
&redirect("edit_dbase.cgi?db=$titles[0]");
exit;
}
@@ -162,7 +162,7 @@ else {
# DB is running .. list databases
@icons = map { "images/db.gif" } @titles;
@links = map { "edit_dbase.cgi?db=".&urlize($_) } @titles;
@links = map { "edit_dbase.cgi?db=$_" } @titles;
$can_create = $access{'create'} == 1 ||
$access{'create'} == 2 && @titles < $access{'max'};

View File

@@ -653,8 +653,6 @@ backup_efile=Missing or invalid backup filename
backup_efile2=Missing or invalid backup directory
backup_epath=Invalid backup path
backup_eunder=Backups can only be created under $1
backup_eparameters=Custom backup parameters require unrestricted root backup access
backup_esave=You do not have permission to save backup settings or schedules
backup_ewhere=Missing <tt>where</tt> clause
backup_echarset=Missing character set
backup_ebackup=<tt>mysqldump</tt> failed : $1
@@ -708,7 +706,6 @@ import_uploadout=Importing data from uploaded file ..
import_fileout=Importing data from file $1 ..
import_eupload=Missing uploaded import file
import_efile=Missing import filename
import_eperms=Failed to set permissions on the temporary import file
adv_title=Advanced Search
adv_and=Find records matching all criteria ..

View File

@@ -28,9 +28,9 @@ if (@procs) {
foreach $r (@procs) {
print &ui_checked_columns_row([
$r->[0],
&ui_link("edit_user.cgi?user=".&urlize($r->[1]),&html_escape($r->[1])),
&html_escape($r->[2]),
&ui_link("edit_dbase.cgi?db=".&urlize($r->[3]),&html_escape($r->[3])),
&ui_link("edit_user.cgi?user=$r->[1]",$r->[1]),
$r->[2],
&ui_link("edit_dbase.cgi?db=$r->[3]",$r->[3]),
$r->[4],
&nice_time($r->[5]),
&html_escape($r->[7])

View File

@@ -716,12 +716,10 @@ return &quotestr($rv);
}
# quotestr(string)
# Quotes a single SQL identifier, including any embedded backticks.
sub quotestr
{
my ($rv) = @_;
if (&supports_quoting()) {
$rv =~ s/`/``/g;
return "`$rv`";
}
else {
@@ -1185,70 +1183,6 @@ return 0 if (!@uinfo);
return &set_ownership_permissions($user, undef, 0600, $file);
}
# mysql_import_tempdir(database)
# Creates a per-import directory in the local server's permitted file directory,
# or its data directory when no file directory is configured. Called as root.
sub mysql_import_tempdir
{
my ($db) = @_;
my $d = &execute_sql_safe($db,
"show variables where Variable_name in ('secure_file_priv', 'datadir')");
my %vars = map { @$_ } @{$d->{'data'}};
my $dir = $vars{'secure_file_priv'};
$dir = $vars{'datadir'} if (!$dir || $dir eq 'NULL');
return if (!$dir || $dir !~ m{^/} || !-d $dir);
# A unique directory preserves the table filename without concurrent collisions.
# Its name uses the server's temporary-table prefix, so MariaDB does not list
# it as a database or fail dumps and checks while an import is running.
require File::Temp;
my $tmp = eval { File::Temp::tempdir('#sql-webmin-import-XXXXXX', DIR => $dir) };
return if (!$tmp);
push(@main::temporary_files, $tmp);
# The client and daemon can traverse it; CSV contents remain owner/group-only.
return if (!chmod(0755, $tmp));
return $tmp;
}
# mysql_import_tempname(directory, [filename])
# Keeps Webmin's filename handling, using the per-import directory when provided.
sub mysql_import_tempname
{
my ($dir, $filename) = @_;
# The per-import directory is already registered for cleanup.
my $file = $dir ? &tempname($filename) : &transname($filename);
$file =~ s{^.*/}{$dir/} if ($dir);
return $file;
}
# set_mysql_import_file_permissions(database, file)
# Called as root for a local server. Share the temporary file with the daemon's
# group, keeping its owner so the import-command user can still read it.
sub set_mysql_import_file_permissions
{
my ($db, $file) = @_;
# Do not change a symlink's target or a directory's permissions.
my @st = lstat($file);
return 0 if (!@st || !-f _);
my $d = &execute_sql_safe($db, "show variables like 'pid_file'");
my $pidfile = $d->{'data'}->[0]->[1];
my $pid = $pidfile ? &read_file_contents($pidfile) : undef;
# Leave permissions alone if the local daemon cannot be identified.
return 1 if (!defined($pid) || $pid !~ /\A([0-9]+)\s*\z/);
$pid = $1;
return 1 if (!&foreign_check("proc"));
&foreign_require("proc", "proc-lib.pl");
my %proc = &proc::process_info($pid);
return 1 if (!$proc{'args'} ||
$proc{'args'} !~ m{\A(?:\S*/)?(?:mysqld|mariadbd)(?:\s|\z)});
# The socket can have a different group, so use the running process instead.
my $group = $proc{'_group'};
$group = (getpwnam($proc{'user'}))[3] if (!defined($group) || $group eq '');
return 1 if (!defined($group));
return &set_ownership_permissions($st[4], $group, 0640, $file);
}
# execute_sql_file(database, file, [user, pass], [unix-user])
# Executes some file of SQL commands, and returns the exit status and output
sub execute_sql_file
@@ -1783,7 +1717,7 @@ sub view_structure
{
local ($db, $view) = @_;
local $info = { 'name' => $view };
local $d = &execute_sql($db, "show create view ".&quotestr($view));
local $d = &execute_sql($db, "show create view $view");
local $c = $d->{'data'}->[0]->[1];
if ($c =~ /algorithm\s*=\s*(\S+)/i) {
$info->{'algorithm'} = lc($1);
@@ -1800,8 +1734,7 @@ if ($c =~ /sql\s+security\s+(\S+)/i) {
if ($c =~ s/\s+with\s+(cascaded|local)\s+check\s+option//i) {
$info->{'check'} = lc($1);
}
# Quoted view names can contain spaces and escaped backticks.
if ($c =~ /view\s+(`(?:``|[^`])*`|\S+)\s+as\s+(.*)/i) {
if ($c =~ /view\s+(`\S+`|\S+)\s+as\s+(.*)/i) {
$info->{'query'} = $2;
}
return $info;
@@ -1967,30 +1900,25 @@ return $two eq "\037\213" ? 1 :
$two eq "BZ" ? 3 : 0;
}
# backup_database(db, dest-file, compress-mode, drop-flag, [where-clause],
# [charset], &compatible, &only-tables, [run-as-user],
# single-transaction-flag, quick-flag, [force-flag],
# [parameters])
# Writes a database backup to a file, with optional compression. Returns undef
# on success, or an error message on failure.
# backup_database(db, dest-file, compress-mode, drop-flag, where-clause,
# charset, &compatible, &only-tables, run-as-user,
# single-transaction-flag, quick-flag, force-flag, parameters)
# Backs up a database to the given file, optionally with compression. Returns
# undef on success, or an error message on failure.
sub backup_database
{
my ($db, $file, $compress, $drop, $where, $charset, $compatible,
$tables, $user, $single, $quick, $force, $parameters) = @_;
my $writer;
if ($compress == 0) {
# Write an uncompressed SQL dump.
$writer = "cat >".quotemeta($file);
}
elsif ($compress == 1) {
# Compress the dump with gzip while writing it.
$writer = "gzip -c >".quotemeta($file);
}
elsif ($compress == 2) {
# Compress the dump with bzip2 while writing it.
$writer = "bzip2 -c >".quotemeta($file);
}
# Build dump-client options for the requested backup behavior.
my $dropsql = $drop ? "--add-drop-table" : "";
my $singlesql = $single ? "--single-transaction" : "";
my $forcesql = $force ? "--force" : "";
@@ -2000,15 +1928,13 @@ my $parameterssql = $parameters ?
my $wheresql = $where ? "--where=".quotemeta($in{'where'}) : "";
my $charsetsql = $charset ?
"--default-character-set=".quotemeta($charset) : "";
# Quote compatibility values and table names as single shell arguments.
my $compatiblesql = @$compatible ?
"--compatible=".quotemeta(join(",", @$compatible)) : "";
"--compatible=".join(",", @$compatible) : "";
my $quotingsql = &supports_quoting() ? "--quote-names" : "";
my $routinessql = &supports_routines() ? "--routines" : "";
my $tablessql = join(" ", map { quotemeta($_) } @$tables);
my $eventssql = &supports_mysqldump_events() ? "--events" : "";
my $gtidsql = "";
# Check whether the client and server need explicit GTID suppression.
if (&supports_mysqldump_setgtid() &&
$config{'mysqldump'} !~ /--set-gtid-purged/) {
eval {
@@ -2019,7 +1945,7 @@ if (&supports_mysqldump_setgtid() &&
if (@{$d->{'data'}} && uc($d->{'data'}->[0]->[1]) eq 'ON' &&
$variant eq 'mysql' &&
&compare_version_numbers($ver, "5.6") >= 0) {
# Omit GTID_PURGED statements when backing up a GTID server.
# Add flag to support GTIDs
$gtidsql = "--set-gtid-purged=OFF";
}
};
@@ -2029,15 +1955,12 @@ if ($user && $user ne "root") {
$writer = &command_as_user($user, 0, $writer);
}
&set_authstr_env();
# Prevent database and table names from being parsed as dump options.
my $cmd = "$config{'mysqldump'} $authstr $dropsql $singlesql $forcesql $quicksql $parameterssql $wheresql $charsetsql $compatiblesql $quotingsql $routinessql $eventssql $gtidsql -- ".quotemeta($db)." $tablessql | $writer";
my $cmd = "$config{'mysqldump'} $authstr $dropsql $singlesql $forcesql $quicksql $parameterssql $wheresql $charsetsql $compatiblesql $quotingsql $routinessql ".quotemeta($db)." $tablessql $eventssql $gtidsql | $writer";
if (&shell_is_bash()) {
# Propagate dump-client failures through the output pipeline.
$cmd = "set -o pipefail ; $cmd";
}
my $out = &backquote_logged("($cmd) 2>&1");
if ($? || !-s $file || $out =~ /Aborted\s+connection|max_allowed_packet/i) {
# Return errors for command failures, empty output, or partial dumps.
return $out;
}
return undef;
@@ -2050,8 +1973,8 @@ sub delete_database_backup_job
my ($db) = @_;
&foreign_require("cron");
my @jobs = &cron::list_cron_jobs();
my ($job) = grep { $_->{'command'} eq $cron_cmd." ".$db ||
$_->{'command'} eq $cron_cmd." ".quotemeta($db) } @jobs;
my $cmd = "$cron_cmd $db";
my ($job) = grep { $_->{'command'} eq $cmd } @jobs;
if ($job) {
&lock_file(&cron::cron_file($job));
&cron::delete_cron_job($job);

View File

@@ -8,14 +8,14 @@ $access{'perms'} == 1 || &error($text{'perms_ecannot'});
$mode = &mysql_login_type($mysql_login || 'root');
if ($mode eq 'socket') {
print &ui_alert_box(&text('root_socket', &html_escape($mysql_login)), 'warn');
print &ui_alert_box(&text('root_socket', $mysql_login), 'warn');
}
else {
print &ui_form_start("save_root.cgi", "post");
print &ui_table_start($text{'root_header'}, undef, 2);
print &ui_table_row($text{'root_user'},
$mysql_login ? "<tt>".&html_escape($mysql_login)."</tt>"
$mysql_login ? "<tt>$mysql_login</tt>"
: "<label>$text{'root_auto'}</label>");
print &ui_table_row($text{'root_pass'},
$mysql_pass ? "<tt>".&ui_text_mask($mysql_pass)."</tt>"

View File

@@ -11,8 +11,7 @@ else {
&can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'});
@str = &table_structure($in{'db'}, $in{'table'});
$desc = &text('table_header', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $text{'adv_title'}, "");
print &ui_form_start("view_table.cgi", "post");
@@ -36,14 +35,13 @@ print "</table>\n";
print &ui_form_end([ [ "advanced", $text{'adv_ok'} ] ]);
if ($access{'edonly'}) {
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}),$text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}",$text{'dbase_return'},
"", $text{'index_return'});
}
else {
&ui_print_footer("edit_table.cgi?db=".&urlize($in{'db'}).
"&table=".&urlize($in{'table'}),
&ui_print_footer("edit_table.cgi?db=$in{'db'}&table=$in{'table'}",
$text{'table_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
"", $text{'index_return'});
}

View File

@@ -25,7 +25,7 @@ elsif ($in{'exec'}) {
}
$access{'edonly'} && &error($text{'dbase_ecannot'});
$desc = "<tt>".&html_escape($in{'db'})."</tt>";
$desc = "<tt>$in{'db'}</tt>";
&ui_print_header($desc, $text{'table_title2'}, "", "table_form");
print &ui_form_start("create_table.cgi", "post");
@@ -67,6 +67,6 @@ print &ui_table_row(undef, &show_table_form($in{"fields"} || 4), 2);
print &ui_table_end();
print &ui_form_end([ [ "create", $text{'create'} ] ]);
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});

View File

@@ -38,7 +38,7 @@ elsif ($in{'advanced'}) {
@adv).")";
$searchhids .= &ui_hidden("and", $in{'and'})."\n".
&ui_hidden("advanced", 1)."\n";
$searchargs .= "&and=".&urlize($in{'and'}).
$searchargs .= "&and=".$in{'and'}.
"&advanced=1";
}
}
@@ -71,7 +71,7 @@ if ($in{'sortfield'}) {
$sorthids = &ui_hidden("sortfield", $in{'sortfield'})."\n".
&ui_hidden("sortdir", $in{'sortdir'})."\n";
$sortargs = "&sortfield=".&urlize($in{'sortfield'}).
"&sortdir=".&urlize($in{'sortdir'});
"&sortdir=$in{'sortdir'}";
return ($sort, $sorthids, $sortargs);
}
else {

View File

@@ -104,7 +104,7 @@ if ($in{'delete'}) {
$count++;
}
&webmin_log("delete", "data", $count, \%in);
&redirect("view_table.cgi?db=".&urlize($in{'db'})."&".
&redirect("view_table.cgi?db=$in{'db'}&".
"table=".&urlize($in{'table'})."&start=$in{'start'}".
$searchargs.$sortargs);
}
@@ -154,7 +154,7 @@ elsif ($in{'save'}) {
$count++;
}
&webmin_log("modify", "data", $count, \%in);
&redirect("view_table.cgi?db=".&urlize($in{'db'})."&".
&redirect("view_table.cgi?db=$in{'db'}&".
"table=".&urlize($in{'table'})."&start=$in{'start'}".
$searchargs.$sortargs);
}
@@ -168,7 +168,7 @@ elsif ($in{'savenew'}) {
}
&execute_sql_logged($in{'db'}, "insert into ".&quote_table($in{'table'}).
" values (".join(" , ", map { "?" } @set).")", @set);
&redirect("view_table.cgi?db=".&urlize($in{'db'})."&".
&redirect("view_table.cgi?db=$in{'db'}&".
"table=".&urlize($in{'table'})."&start=$in{'start'}".
$searchargs.$sortargs);
&webmin_log("create", "data", undef, \%in);
@@ -177,8 +177,7 @@ elsif ($in{'cancel'} || $in{'new'}) {
undef($in{'row'});
}
$desc = &text('table_header', "<tt>".&html_escape($in{'table'})."</tt>",
"<tt>".&html_escape($in{'db'})."</tt>");
$desc = &text('table_header', "<tt>$in{'table'}</tt>", "<tt>$in{'db'}</tt>");
&ui_print_header($desc, $text{'view_title'}, "");
if ($in{'start'} || $total > $displayconfig{'perpage'}) {
@@ -186,7 +185,7 @@ if ($in{'start'} || $total > $displayconfig{'perpage'}) {
if ($in{'start'}) {
printf "<a href='view_table.cgi?db=%s&table=%s&start=%s%s%s'>".
"<img src=../images/left.gif border=0 align=middle></a>\n",
&urlize($in{'db'}), &urlize($in{'table'}),
$in{'db'}, $in{'table'},
$in{'start'} - $displayconfig{'perpage'},
$searchargs, $sortargs;
}
@@ -196,7 +195,7 @@ if ($in{'start'} || $total > $displayconfig{'perpage'}) {
if ($in{'start'}+$displayconfig{'perpage'} < $total) {
printf "<a href='view_table.cgi?db=%s&table=%s&start=%s%s%s'>".
"<img src=../images/right.gif border=0 align=middle></a> ",
&urlize($in{'db'}), &urlize($in{'table'}),
$in{'db'}, $in{'table'},
$in{'start'} + $displayconfig{'perpage'},
$searchargs, $sortargs;
}
@@ -209,23 +208,23 @@ if ($in{'field'}) {
# Show details of simple search
my $msg = $in{'match'} == 2 || $in{'match'} == 3 ?
'view_searchheadnot' : 'view_searchhead';
print "<tr> <td><b>",&text($msg, "<tt>".&html_escape($in{'for'})."</tt>",
"<tt>".&html_escape($in{'field'})."</tt>"),"</b></td>\n";
print "<td align=right><a href='view_table.cgi?db=",&urlize($in{'db'}),
"&table=",&urlize($in{'table'}),"$sortargs'>$text{'view_searchreset'}</a></td> </tr>\n";
print "<tr> <td><b>",&text($msg, "<tt>$in{'for'}</tt>",
"<tt>$in{'field'}</tt>"),"</b></td>\n";
print "<td align=right><a href='view_table.cgi?db=$in{'db'}&",
"table=$in{'table'}$sortargs'>$text{'view_searchreset'}</a></td> </tr>\n";
}
elsif ($in{'advanced'}) {
# Show details of advanced search
print "<tr> <td><b>",&text('view_searchhead2', $advcount),"</b></td>\n";
print "<td align=right><a href='view_table.cgi?db=",&urlize($in{'db'}),
"&table=",&urlize($in{'table'}),"$sortargs'>$text{'view_searchreset'}</a></td> </tr>\n";
print "<td align=right><a href='view_table.cgi?db=$in{'db'}&",
"table=$in{'table'}$sortargs'>$text{'view_searchreset'}</a></td> </tr>\n";
}
if ($in{'sortfield'}) {
# Show current sort order
print "<tr> <td><b>",&text($in{'sortdir'} ? 'view_sorthead2' : 'view_sorthead1',
"<tt>".&html_escape($in{'sortfield'})."</tt>"),"</b></td>\n";
print "<td align=right><a href='view_table.cgi?db=",&urlize($in{'db'}),
"&table=",&urlize($in{'table'}),"$searchargs'>$text{'view_sortreset'}</a></td> </tr>\n";
"<tt>$in{'sortfield'}</tt>"),"</b></td>\n";
print "<td align=right><a href='view_table.cgi?db=$in{'db'}&",
"table=$in{'table'}$searchargs'>$text{'view_sortreset'}</a></td> </tr>\n";
}
print "</table>\n";
@@ -282,8 +281,7 @@ if ($total || $in{'new'}) {
for($j=0; $j<@str; $j++) {
local $nm = $i == $realrows ? $j :
"${i}_$str[$j]->{'field'}";
$et .= "<tr $cb> <td><b>".&html_escape($str[$j]->{'field'}).
"</b></td> <td>\n";
$et .= "<tr $cb> <td><b>$str[$j]->{'field'}</b></td> <td>\n";
if ($displayconfig{'blob_mode'} &&
&is_blob($str[$j]) && $d[$j]) {
# Show as keep/upload inputs
@@ -396,8 +394,7 @@ if ($total || $in{'new'}) {
push(@cols, &ui_link(
"@{[&get_webprefix()]}/".
"$module_name/download.cgi?".
"db=".&urlize($in{'db'}).
"&table=".&urlize($in{'table'}).
"db=$in{'db'}&table=$in{'table'}".
"&start=$in{'start'}".
$searchargs.$sortargs.
"&row=$i&col=$j",
@@ -492,14 +489,14 @@ if (!$in{'field'} && $total > $displayconfig{'perpage'}) {
}
if ($access{'edonly'}) {
&ui_print_footer("edit_dbase.cgi?db=".&urlize($in{'db'}),$text{'dbase_return'},
&ui_print_footer("edit_dbase.cgi?db=$in{'db'}",$text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}
else {
&ui_print_footer("edit_table.cgi?db=".&urlize($in{'db'})."&table=".
&ui_print_footer("edit_table.cgi?db=$in{'db'}&table=".
&urlize($in{'table'}),
$text{'table_return'},
"edit_dbase.cgi?db=".&urlize($in{'db'}), $text{'dbase_return'},
"edit_dbase.cgi?db=$in{'db'}", $text{'dbase_return'},
&get_databases_return_link($in{'db'}), $text{'index_return'});
}
@@ -512,9 +509,8 @@ local $dir = $in{'sortfield'} eq $field ? !$in{'sortdir'} : 0;
local $img = $in{'sortfield'} eq $field && $dir ? "sortascgrey.gif" :
$in{'sortfield'} eq $field && !$dir ? "sortdescgrey.gif" :
$dir ? "sortasc.gif" : "sortdesc.gif";
return "<a href='view_table.cgi?db=".&urlize($in{'db'})."&table=".
&urlize($in{'table'})."&start=$in{'start'}&sortfield=".
&urlize($field)."&sortdir=$dir$searchargs'>".
"<b>".&html_escape($field)."</b><img valign=middle src=../images/$img border=0>";
return "<a href='view_table.cgi?db=$in{'db'}&table=".
&urlize($in{'table'})."&start=$in{'start'}&sortfield=$field&sortdir=$dir$searchargs'>".
"<b>$field</b><img valign=middle src=../images/$img border=0>";
}

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'dirs_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'dirs_title'}, "");
print &ui_form_start("save_dirs.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'disable_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'disable_title'}, "");
print &ui_form_start("save_disable.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'errors_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'errors_title'}, "");
print &ui_form_start("save_errors.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'limits_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'limits_title'}, "");
print &ui_form_start("save_limits.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -7,8 +7,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'manual_ecannot'});
$access{'manual'} || &error($text{'manual_ecannot'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'manual_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'manual_title'}, "");
print $text{'manual_desc'},"<p>\n";
print &ui_form_start("save_manual.cgi", "form-data");

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'misc_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'misc_title'}, "");
# Build list of timezones
my %tzlist;

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'session_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'session_title'}, "");
print &ui_form_start("save_session.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -6,8 +6,7 @@ require './phpini-lib.pl';
&can_php_config($in{'file'}) || &error($text{'list_ecannot'});
$conf = &get_config_as_user($in{'file'});
&ui_print_header("<tt>".&html_escape($in{'file'})."</tt>",
$text{'vars_title'}, "");
&ui_print_header("<tt>$in{'file'}</tt>", $text{'vars_title'}, "");
print &ui_form_start("save_vars.cgi", "post");
print &ui_hidden("file", $in{'file'}),"\n";

View File

@@ -15,22 +15,17 @@ $mail || &error($text{'mailq_egone'});
&parse_mail($mail);
$attach = $mail->{'attach'}->[$in{'attach'}];
my $type = lc($attach->{'type'} || 'application/octet-stream');
print "X-no-links: 1\n";
print "X-Content-Type-Options: nosniff\n";
if ($type =~ /^text\// || $type =~ /(?:\/|\+)xml\z/ ||
$type eq 'image/svg' || $type eq 'message/delivery-status') {
# Display text and XML as plain text, never as active documents.
if ($attach->{'type'} eq 'message/delivery-status') {
print "Content-type: text/plain\n\n";
}
elsif ($type =~ /\Aimage\/(?:gif|jpeg|png|webp|bmp|avif|x-icon|vnd\.microsoft\.icon)\z/) {
# Only known raster image types may be rendered inline.
print "Content-type: $type\n\n";
else {
print "Content-type: $attach->{'type'}\n\n";
}
if ($attach->{'type'} =~ /^(text\/html|image\/svg|application\/xhtml)/i) {
print &filter_javascript($attach->{'data'});
}
else {
# Download other formats without trusting the sender's MIME type.
print "Content-Disposition: Attachment\n";
print "Content-type: application/octet-stream\n\n";
print $attach->{'data'};
}
print $attach->{'data'};

View File

@@ -10,7 +10,7 @@ our (%access, %in, %text);
$access{'smtpd'} || &error($text{'smtpd_ecannot'});
&ReadParse();
&ui_print_header(undef, &html_escape($in{'title'}), "");
&ui_print_header(undef, $in{'title'}, "");
if (&get_current_value($in{'name'}) eq "")
{

View File

@@ -159,6 +159,7 @@ exec_tabimport=Import text file
exec_return=execute SQL form
exec_ecompress=The SQL file is compressed, but the program $1 needed to un-compress it is not installed.
exec_ecompress2=Un-compression failed : $1
exec_erestrict=The psql client is too old to safely execute SQL files. Update the PostgreSQL client and try again.
stop_err=Failed to stop database server
stop_epidfile=Failed to open PID file $1

View File

@@ -373,10 +373,7 @@ else {
}
elsif ($sql =~ /^\s*\\/ ) {
$break_f = 1 ;
if ($sql !~ /^\s*\\copy\s+/ &&
$sql !~ /^\s*\\i\s+/) {
&error ( &text ( 'r_command', ) ) ;
}
&error ( &text ( 'r_command', ) ) ;
}
if (@params) {
@@ -405,7 +402,7 @@ else {
&host_port_flags().
(!&supports_pgpass() ? " -u" :
" -U ".quotemeta($postgres_login)).
" -c ".quotemeta($sql).
" -c ".&quote_path($sql).
" ".quotemeta($_[0]);
if ($postgres_sameunix && defined(getpwnam($postgres_login))) {
$cmd = &command_as_user($postgres_login, 0, $cmd);
@@ -832,6 +829,22 @@ return 1 if (!$user);
return &set_ownership_permissions($user, undef, 0600, $file);
}
# supports_psql_restrict()
# Returns 1 if psql supports restricted mode.
sub supports_psql_restrict
{
local $out = &backquote_command(
&quote_path($config{'psql'})." -V 2>&1 <$null_file");
local ($version) = $out =~ /psql.*?\s(\d+(?:\.\d+)*)/i;
return 0 if (!defined($version));
local ($major) = split(/\./, $version);
local %minimum = ( 13 => "13.22", 14 => "14.19", 15 => "15.14",
16 => "16.10", 17 => "17.6" );
return &compare_version_numbers($version, "18") >= 0 ||
$minimum{$major} &&
&compare_version_numbers($version, $minimum{$major}) >= 0;
}
# execute_sql_file(database, file, [user, pass], [unix-user])
# Executes some file of SQL statements, and returns the exit status and output
sub execute_sql_file
@@ -844,7 +857,59 @@ if (!defined($user)) {
$user = $postgres_login;
$pass = $postgres_pass;
}
local $cmd = &quote_path($config{'psql'})." -f ".&quote_path($file).
&supports_psql_restrict() || return (1, $text{'exec_erestrict'});
# Copy a plain PostgreSQL dump without its outer restriction wrapper.
local (*SQLFILE, *SAFEFILE);
local ($line, $dump_key);
open(SQLFILE, "<$file") || return (1, $text{'exec_efile'});
while(defined($line = <SQLFILE>)) {
if ($line =~ /^\s*\\restrict\s+([A-Za-z0-9]+)\s*$/) {
$dump_key = $1;
last;
}
elsif ($line !~ /^\s*(?:--.*)?$/) {
last;
}
}
local $safe_file;
if ($dump_key) {
$safe_file = &transname();
&open_tempfile(SAFEFILE, ">$safe_file");
local ($last_line, $last_pos);
while(defined($line = <SQLFILE>)) {
if ($line =~ /\S/) {
$last_line = $line;
$last_pos = tell(SAFEFILE);
}
&print_tempfile(SAFEFILE, $line);
}
&close_tempfile(SAFEFILE);
if (!defined($last_line) ||
$last_line !~ /^\s*\\unrestrict\s+\Q$dump_key\E\s*$/ ||
!truncate($safe_file, $last_pos)) {
unlink($safe_file);
undef($safe_file);
}
}
close(SQLFILE);
if ($safe_file) {
local $fileuser = &postgresql_command_unix_user();
$fileuser ||= $unixuser if ($unixuser && $unixuser ne 'root' && $< == 0);
if ($fileuser &&
!&set_ownership_permissions($fileuser, undef, 0600, $safe_file)) {
unlink($safe_file);
return (1, $text{'exec_efile'});
}
$file = $safe_file;
}
# Restrict psql commands while executing the user-supplied file.
local $restrict_key = &generate_miniserv_websocket_token();
local $cmd = &quote_path($config{'psql'}).
" -X -c ".
quotemeta("\\restrict $restrict_key").
" -f ".&quote_path($file).
&host_port_flags().
(&supports_pgpass() ? " -U $user" : " -u").
" ".$db;
@@ -855,8 +920,11 @@ elsif ($unixuser && $unixuser ne 'root' && $< == 0) {
$cmd = &command_as_user($unixuser, 0, $cmd);
}
$cmd = &command_with_login($cmd, $user, $pass);
local $ENV{'LC_ALL'} = 'C';
local $out = &backquote_logged("$cmd 2>&1");
return ($out =~ /ERROR/i ? 1 : 0, $out);
local $ex = $? ? 1 : 0;
unlink($safe_file) if ($safe_file);
return ($ex || $out =~ /ERROR|backslash commands are restricted|\\unrestrict:\s+wrong key/i ? 1 : 0, $out);
}
# split_table(&titles, &checkboxes, &links, &col1, &col2, ...)

View File

@@ -9,22 +9,12 @@ $mail = &read_mail_file($in{'file'});
&parse_mail($mail);
$attach = $mail->{'attach'}->[$in{'attach'}];
my $type = lc($attach->{'type'} || 'application/octet-stream');
print "X-no-links: 1\n";
print "X-Content-Type-Options: nosniff\n";
if ($type =~ /^text\// || $type =~ /(?:\/|\+)xml\z/ ||
$type eq 'image/svg' || $type eq 'message/delivery-status') {
# Display text and XML as plain text, never as active documents.
print "Content-type: text/plain\n\n";
}
elsif ($type =~ /\Aimage\/(?:gif|jpeg|png|webp|bmp|avif|x-icon|vnd\.microsoft\.icon)\z/) {
# Only known raster image types may be rendered inline.
print "Content-type: $type\n\n";
print "Content-type: $attach->{'type'}\n\n";
if ($attach->{'type'} =~ /^(text\/html|image\/svg|application\/xhtml)/i) {
print &filter_javascript($attach->{'data'});
}
else {
# Download other formats without trusting the sender's MIME type.
print "Content-Disposition: Attachment\n";
print "Content-type: application/octet-stream\n\n";
print $attach->{'data'};
}
print $attach->{'data'};

View File

@@ -27,22 +27,17 @@ foreach $s (@sub) {
}
$attach = $mail->{'attach'}->[$in{'attach'}];
my $type = lc($attach->{'type'} || 'application/octet-stream');
print "X-no-links: 1\n";
print "X-Content-Type-Options: nosniff\n";
if ($type =~ /^text\// || $type =~ /(?:\/|\+)xml\z/ ||
$type eq 'image/svg' || $type eq 'message/delivery-status') {
# Display text and XML as plain text, never as active documents.
if ($attach->{'type'} eq 'message/delivery-status') {
print "Content-type: text/plain\n\n";
}
elsif ($type =~ /\Aimage\/(?:gif|jpeg|png|webp|bmp|avif|x-icon|vnd\.microsoft\.icon)\z/) {
# Only known raster image types may be rendered inline.
print "Content-type: $type\n\n";
else {
print "Content-type: $attach->{'type'}\n\n";
}
if ($attach->{'type'} =~ /^(text\/html|image\/svg|application\/xhtml)/i) {
print &filter_javascript($attach->{'data'});
}
else {
# Download other formats without trusting the sender's MIME type.
print "Content-Disposition: Attachment\n";
print "Content-type: application/octet-stream\n\n";
print $attach->{'data'};
}
print $attach->{'data'};

View File

@@ -10,9 +10,7 @@ require './servers-lib.pl';
our (%in);
&ReadParse();
my $enc = &encode_base64($in{'user'}.":".$in{'pass'});
$in{'id'} =~ s/\r|\n|\0//g;
$in{'id'} =~ /\A[0-9]+\z/ || &error("Invalid server ID");
$enc =~ s/\r|\n|\0//g;
$enc =~ s/\r|\n//g;
print "Set-Cookie: $in{'id'}=$enc; path=/";
if (uc($ENV{'HTTPS'}) eq 'ON') {
print "; secure";

View File

@@ -9,8 +9,6 @@ no warnings 'uninitialized';
require './servers-lib.pl';
&ReadParse();
our (%in);
$in{'id'} =~ s/\r|\n|\0//g;
$in{'id'} =~ /\A[0-9]+\z/ || &error("Invalid server ID");
print "Set-Cookie: $in{'id'}=; path=/";
if (uc($ENV{'HTTPS'}) eq 'ON') {
print "; secure";

View File

@@ -6,8 +6,7 @@ require './software-lib.pl';
&ReadParse();
&ui_print_header(undef, $text{'list_title'}, "", "list_pack");
print &ui_subheading(&text('list_files',
"<tt>".&html_escape($in{'package'})."</tt>"));
print &ui_subheading(&text('list_files', "<tt>$in{'package'}</tt>"));
print &ui_columns_start([
$text{'list_path'},
$text{'list_owner'},

View File

@@ -14,7 +14,6 @@ local($i, $list); $i = 0;
$list = @_ ? join(' ', map { quotemeta($_) } @_) : "-a";
%packages = ( );
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -q $list --queryformat \"%{NAME}\\n%{VERSION}-%{RELEASE}\\n%{EPOCH}\\n%{GROUP}\\n%{ARCH}\\n%{SUMMARY}\\n\\n\"", 1, 1);
while(my $name = <RPM>) {
next if ($name =~ /is\s+not\s+installed$/);
@@ -51,7 +50,6 @@ sub package_info
local(@rv, @tmp, $d);
local $n = $_[1] ? "$_[0]-$_[1]" : $_[0];
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -q $n --queryformat \"%{NAME}\\n%{GROUP}\\n%{ARCH}\\n%{VERSION}-%{RELEASE}\\n%{VENDOR}\\n%{INSTALLTIME}\\n%{URL}\\n\" 2>/dev/null", 1, 1);
@tmp = <RPM>;
chop(@tmp);
@@ -59,7 +57,6 @@ local $ex = close(RPM);
&reset_environment();
if (!@tmp || $tmp[0] =~ /not\s+installed/) { return (); }
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -q $n --queryformat \"%{DESCRIPTION}\"", 1, 1);
while(<RPM>) { $d .= $_; }
close(RPM);
@@ -80,7 +77,6 @@ my $query_format =
'%{INSTALLTIME}\n%{DESCRIPTION}';
&clean_environment();
&clean_language();
&open_execute_command(*RPM, "rpm -q --whatprovides $vpkg --queryformat ".
"\"$query_format\" 2>/dev/null", 1, 1);
my @tmp = <RPM>;
@@ -117,14 +113,12 @@ else {
# just a normal file .. check if it is an RPM and not an SRPM
local $qm = quotemeta($_[0]);
&clean_environment();
&clean_language();
$out = &backquote_command("rpm -q -p $qm 2>&1", 1);
&reset_environment();
if ($out =~ /does not appear|No such file|with major numbers|not an rpm/i || $?) {
return 0;
}
&clean_environment();
&clean_language();
&open_execute_command(OUT, "rpm -q -p -l $qm 2>&1", 1, 1);
&reset_environment();
while(<OUT>) {
@@ -145,7 +139,6 @@ local $qm = quotemeta($_[0]);
if (-d $real) {
local @rv;
&clean_environment();
&clean_language();
&open_execute_command(RPM, "cd $qm ; rpm -q -p *.rpm --queryformat \"%{NAME} %{SUMMARY}\\n\" 2>&1", 1, 1);
while(<RPM>) {
chop;
@@ -158,7 +151,6 @@ if (-d $real) {
elsif ($_[0] =~ /\*|\?/) {
local @rv;
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -q -p $qm --queryformat \"%{NAME} %{SUMMARY}\\n\" 2>&1", 1);
while(<RPM>) {
chop;
@@ -171,7 +163,6 @@ elsif ($_[0] =~ /\*|\?/) {
else {
local($out);
&clean_environment();
&clean_language();
$out = &backquote_command("rpm -q -p $qm --queryformat \"%{NAME} %{SUMMARY}\\n\" 2>&1", 1);
&reset_environment();
$out =~ s/warning:.*\n//;
@@ -232,7 +223,6 @@ if (-d $real) {
# Find the package in the directory
local ($f, $out);
&clean_environment();
&clean_language();
opendir(DIR, $real);
while($f = readdir(DIR)) {
next if ($f !~ /\.rpm$/);
@@ -254,7 +244,6 @@ elsif ($file =~ /\*|\?/) {
# XXX won't work when translation is in effect
local ($f, $out);
&clean_environment();
&clean_language();
foreach $f (glob($real)) {
my $qf = quotemeta($f);
$out = &backquote_command("rpm -q -p $qf --queryformat \"%{NAME}\\n\" 2>&1", 1);
@@ -270,7 +259,6 @@ elsif ($file =~ /\*|\?/) {
}
local $temp = &transname();
&clean_environment();
&clean_language();
local $rv = &system_logged("rpm -i $opts ".quotemeta($file)." >$temp 2>&1");
local $out = &backquote_command("cat $temp");
&reset_environment();
@@ -310,7 +298,6 @@ local $temp = &transname();
local $qm = quotemeta($file);
$qm =~ s/\\([*?])/$1/g if ($file =~ /[\*\?]/);
&clean_environment();
&clean_language();
local $rv = &system_logged("rpm -i $opts $qm >".quotemeta($temp)." 2>&1");
local $out = &backquote_command("cat ".quotemeta($temp));
&reset_environment();
@@ -331,7 +318,6 @@ local($i, $_, @w, %errs, $epath); $i = 0;
local $n = $_[1] ? "$_[0]-$_[1]" : $_[0];
local $qn = quotemeta($n);
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -V $qn", 1, 1);
while(<RPM>) {
/^(.{8}) (.) (.*)$/;
@@ -374,7 +360,6 @@ local ($pkg, $version) = @_;
local $qn = quotemeta($version ? "$pkg-$version" : $pkg);
local @rv;
&clean_environment();
&clean_language();
&open_execute_command(RPM, "rpm -q -l $qn", 1, 1);
while(<RPM>) {
s/\r|\n//g;
@@ -395,13 +380,11 @@ local($pkg, @w, $_, @pkgs, @vers);
undef(%file);
local $qm = quotemeta($_[0]);
&clean_environment();
&clean_language();
$pkg = &backquote_command("rpm -q -f $qm --queryformat \"%{NAME}\\n\" 2>&1", 1);
&reset_environment();
if ($pkg =~ /not owned/ || $?) { return 0; }
&clean_environment();
&clean_language();
@pkgs = split(/\n/, $pkg);
$pkg = &backquote_command("rpm -q -f $qm --queryformat \"%{VERSION}-%{RELEASE}\\n\" 2>&1", 1);
@vers = split(/\n/, $pkg);
@@ -450,7 +433,6 @@ $opts .= $_[1]->{'noscripts'} ? "--noscripts " : "";
local $n = $_[2] ? "$_[0]-$_[2]" : $_[0];
local $qm = quotemeta($n);
&clean_environment();
&clean_language();
local $out = &backquote_logged("rpm -e $opts $qm 2>&1");
&reset_environment();
if ($? || $out =~ /error:/) { return "<pre>$out</pre>"; }
@@ -475,7 +457,6 @@ for($i=0; $i<@{$_[0]}; $i++) {
}
}
&clean_environment();
&clean_language();
local $out = &backquote_logged("$cmd 2>&1");
&reset_environment();
if ($? || $out =~ /error:/) { return "<pre>$out</pre>"; }
@@ -488,7 +469,6 @@ sub package_dependencies
{
my ($pkg, $ver) = @_;
&clean_environment();
&clean_language();
my $out = &backquote_command(
"rpm -qR ".quotemeta($pkg.($ver ? "-$ver" : ""))." 2>/dev/null");
&reset_environment();

View File

@@ -61,10 +61,7 @@ return &get_dnf_version() >= 5 ? '--setopt=disable_excludes=*' :
}
# update_system_install([packages], [&in], [no-force], [flags])
# Installs or updates space-separated packages with YUM or DNF. The package
# list defaults to form input, the input hash can select a repository, and
# flags are passed to the transaction. No-force is kept for compatibility but
# has no effect.
# Installs or updates packages with YUM or DNF.
sub update_system_install
{
local $update = $_[0] || $in{'update'};
@@ -79,30 +76,15 @@ $qflags = &trim(join(" ", map { quotemeta($_) } split(/ /, $runflags)))
if ($runflags);
$update =~ s/\.\*/\*/g;
local $enable;
# Add the repository selected on the install form.
if ($in->{'enablerepo'}) {
$enable = "enablerepo=".quotemeta($in->{'enablerepo'});
}
local (@rv, @newpacks);
# Split installed packages by architecture and record missing packages.
# If there are multiple architectures to update for a package, split them out
local @updates = split(/\s+/, $update);
local (@names, @installed, @missing);
foreach my $u (@updates) {
my $installed;
my @package_names = &append_architectures($u, \$installed);
push(@names, @package_names);
if ($installed) {
# Queue installed architectures for update.
push(@installed, @package_names);
}
else {
# Queue missing package specifications for installation.
push(@missing, @package_names);
}
}
local @names = map { &append_architectures($_) } split(/\s+/, $update);
if (@names == 1) {
# Keep the original specification when no architecture split was needed.
@names = ( $update );
}
$update = join(" ", map { quotemeta($_) } @names);
@@ -128,39 +110,30 @@ if ($versionlock_update) {
}
}
# DNF 4 installs and upgrades a mixed package set in one transaction. DNF 5
# needs separate actions because install does not upgrade packages by name.
my @actions;
if ($yum_command =~ /dnf$/ && @installed && @missing &&
&get_dnf_version() >= 5) {
@actions = ( [ 'update', \@installed ], [ 'install', \@missing ] );
# Use update for installed DNF packages and install for everything else.
local $cmd;
if ($yum_command =~ /dnf$/) {
local @pinfo = &package_info($updates[0]);
if ($pinfo[0]) {
$cmd = "update";
}
else {
$cmd = "install";
}
}
else {
# Use one action for YUM and non-mixed DNF sets.
my $cmd = $yum_command =~ /dnf$/ && @installed && !@missing ?
'update' : 'install';
@actions = ( [ $cmd, \@names ] );
$cmd = "install";
}
# Join actions so DNF 5 installs only after its update succeeds.
my (@uicmds, @fullcmds);
foreach my $action (@actions) {
my ($cmd, $packages) = @$action;
my $uicmd = "$yum_command $enable -y $cmd ".join(" ", @$packages);
$uicmd .= " $runflags" if ($runflags);
push(@uicmds, $uicmd);
my $fullcmd = "$yum_command $enable -y $cmd ".
join(" ", map { quotemeta($_) } @$packages);
$fullcmd .= " $qflags" if ($qflags);
push(@fullcmds, $fullcmd);
}
my $uicmd = join(" && ", @uicmds);
my $fullcmd = join(" && ", @fullcmds);
# Work out the command to run, which may enable some repos
my $uicmd = "$yum_command $enable -y $cmd ".join(" ", @names);
$uicmd .= " $runflags" if ($runflags);
my $fullcmd = "$yum_command $enable -y $cmd $update";
$fullcmd .= " $qflags" if ($qflags);
foreach my $u (@updates) {
my $repo = &update_system_repo($u);
if ($repo) {
# Install a required repository package first.
$fullcmd = "$yum_command -y install $repo ; $fullcmd";
$fullcmd = "$yum_command -y $cmd $repo ; $fullcmd";
}
}
@@ -168,7 +141,7 @@ print &text('yum_install', "<tt>".&html_escape($uicmd)."</tt>"),"\n";
print "<pre data-installer>";
&additional_log('exec', undef, $fullcmd);
$SIG{'TERM'} = 'ignore'; # Installing webmin itself may kill this script
&open_execute_command(CMD, "($fullcmd) </dev/null", 2);
&open_execute_command(CMD, "$fullcmd </dev/null", 2);
while(<CMD>) {
s/\r|\n//g;
if (/^\[(update|install|deps):\s+(\S+)\s+/) {
@@ -247,25 +220,21 @@ else {
}
}
# append_architectures(package, [installed])
# Returns installed package names with their architectures. Sets installed
# when any are found, or returns the original specification when none are.
# append_architectures(package)
# Given a package name, if it has multiple architectures return the name with
# each appended
sub append_architectures
{
my ($name, $installed) = @_;
my ($name) = @_;
local %packages;
my $n = &list_packages($name);
$$installed = $n ? 1 : 0 if ($installed);
# Keep an unresolved specification for installation.
return ( $name ) if (!$n);
my @rv;
for(my $i=0; $i<$n; $i++) {
if ($packages{$i,'arch'}) {
# Include the installed architecture.
push(@rv, $packages{$i,'name'}.".".$packages{$i,'arch'});
}
else {
# Keep names with no architecture metadata.
push(@rv, $packages{$i,'name'});
}
}

View File

@@ -1,75 +0,0 @@
require 'sqlite-lib.pl';
# sub acl_security_form(&options)
# Outputs the SQLite module access-control fields from the saved options.
sub acl_security_form
{
my ($o) = @_;
# Show the stored entries as they are, so a $HOME token stays a token
# rather than becoming the editing administrator's own home.
my $paths = join("\n", grep { length($_) }
split(/\t+|\r?\n/, $o->{'paths'} // ''));
# Render the path and capability fields.
print &ui_table_row($text{'acl_paths'},
&ui_textarea("paths", $paths, 8, 70, undef, undef,
"style='width: 100%'").&ui_help($text{'acl_paths_desc'}), 3);
print &ui_table_row($text{'acl_write'},
&ui_yesno_radio("write", $o->{'write'}), 3);
print &ui_table_row($text{'acl_create'},
&ui_yesno_radio("create", $o->{'create'}), 3);
print &ui_table_row($text{'acl_sql'},
&ui_yesno_radio("sql", $o->{'sql'}), 3);
# Choose which Unix user does the file work: root, the Webmin user's account,
# or a named one. An older ACL shows what the module derives for it.
my $as = &sqlite_work_as_user($o);
my $mode = !defined($as) ? 0 : $o->{'work_as_user'} ? 2 : 1;
print &ui_table_row($text{'acl_work_as'},
&ui_radio_table("user_mode", $mode, [
[ 0, $text{'acl_root'} ],
[ 1, $text{'acl_same'} ],
[ 2, $text{'acl_user'},
&ui_user_textbox("acl_user", $o->{'work_as_user'}) ] ]), 3);
}
# sub acl_security_save(&options, &input)
# Validates the submitted fields and stores them in the options hash.
sub acl_security_save
{
my ($o, $in) = @_;
my @paths = grep { /\S/ } split(/\r?\n/, $in->{'paths'});
@paths || &error($text{'acl_epaths'});
my @resolved;
# Resolve each root so later boundary checks use canonical paths.
foreach my $path (@paths) {
$path =~ s/^\s+|\s+$//g;
# $HOME is resolved per user at request time, so it is kept as is.
if ($path eq '$HOME') {
push(@resolved, $path);
next;
}
$path =~ /^\// && $path !~ /[\0\t\r\n]/ ||
&error(&text('acl_epath', &html_escape($path)));
my $real = &simplify_path(&resolve_links($path));
defined($real) && -d $real ||
&error(&text('acl_epath', &html_escape($path)));
push(@resolved, $real);
}
# Save unique roots and Boolean capabilities.
$o->{'paths'} = join("\t", &unique(@resolved));
$o->{'write'} = $in->{'write'} ? 1 : 0;
$o->{'create'} = $in->{'create'} ? 1 : 0;
$o->{'sql'} = $in->{'sql'} ? 1 : 0;
# A named Unix user must exist; the other two modes name no user.
if ($in->{'user_mode'} == 2) {
defined(getpwnam($in->{'acl_user'})) || &error($text{'acl_euser'});
$o->{'work_as_root'} = 0;
$o->{'work_as_user'} = $in->{'acl_user'};
}
else {
# The root and matching-user modes do not store a named account.
$o->{'work_as_root'} = $in->{'user_mode'} == 0 ? 1 : 0;
$o->{'work_as_user'} = '';
}
}

View File

@@ -1,68 +0,0 @@
#!/usr/local/bin/perl
# Displays the form for adding a column to a table, or for renaming one.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in);
require './sqlite-lib.pl';
&ReadParse();
# Structure changes need a writable table; views cannot be altered.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
my $dbh = &sqlite_connect($path, 0);
my $object = &sqlite_object($dbh, $in{'table'});
$object && $object->{'type'} eq 'table' || &error($text{'table_missing'});
my $columns = &sqlite_table_columns($dbh, $object->{'name'});
$dbh->disconnect();
my $existing;
# Renaming needs the column to still exist and the library to support it.
if ($in{'column'} ne '') {
($existing) = grep { $_->{'name'} eq $in{'column'} } @$columns;
$existing || &error($text{'column_emissing'});
&sqlite_supports('rename_column') ||
&error($text{'column_eunsupported'});
}
my $desc = &ui_tag('tt', &html_escape($object->{'name'})).
$text{'table_in'}.' '.&ui_tag('tt', &html_escape($path));
&ui_print_header($desc,
$text{$existing ? 'column_title_rename' : 'column_title_add'}, '',
'column');
print &ui_form_start('save_column.cgi', 'post');
print &ui_hidden('db', $path);
print &ui_hidden('table', $object->{'name'});
print &ui_hidden('mode', $existing ? 'rename' : 'add');
print &ui_table_start($text{$existing ? 'column_header_rename' :
'column_header_add'}, 'width=100%', 2);
# A rename shows the current name and asks for the new one only.
if ($existing) {
print &ui_hidden('column', $existing->{'name'});
print &ui_table_row($text{'column_current'},
&ui_tag('tt', &html_escape($existing->{'name'})).' '.
&ui_tag('small', &html_escape($existing->{'type'} ||
$text{'edit_untyped'})));
print &ui_table_row($text{'column_newname'},
&ui_textbox('name', $existing->{'name'}, 40));
}
else {
# A new column takes a name, a type, a NULL rule and a default.
print &ui_table_row($text{'column_name'},
&ui_textbox('name', $in{'name'}, 40));
print &ui_table_row($text{'column_type'},
&ui_select('type', $in{'type'} || 'TEXT',
[ map { [ $_ ] } &sqlite_column_types() ]));
print &ui_table_row($text{'column_null'},
&ui_yesno_radio('null', defined($in{'null'}) ? $in{'null'} : 1));
print &ui_table_row($text{'column_default'},
&ui_textbox('default', $in{'default'}, 30).' '.
&ui_help($text{'column_default_help'}));
}
print &ui_table_end();
print &ui_form_end([ [ undef,
$text{$existing ? 'column_rename' : 'column_add'} ] ]);
&ui_print_footer('table.cgi?db='.&urlize($path).'&table='.
&urlize($object->{'name'}).'&view=structure', $text{'column_return'});

View File

@@ -1,7 +0,0 @@
recent=3
perpage=50
max_text=1000
busy_timeout=5000
max_query_rows=1000
max_result_bytes=16777216
confirm_delete=1

View File

@@ -1,7 +0,0 @@
recent=Number of recent databases to list for each user,0,5
perpage=Rows per page,0,5
max_text=Maximum characters to display from each text value,3,Unlimited
busy_timeout=Locked database wait time in milliseconds,0,5
max_query_rows=Maximum SQL query rows to display,0,5
max_result_bytes=Maximum result data kept per request,0,12,,bytes
confirm_delete=Confirm before deleting rows?,1,1-Yes,0-No

View File

@@ -1,8 +0,0 @@
require 'sqlite-lib.pl';
# sub cpan_recommended()
# Returns the Perl modules that provide SQLite database access.
sub cpan_recommended
{
return ( "DBI", "DBD::SQLite" );
}

View File

@@ -1,77 +0,0 @@
#!/usr/local/bin/perl
# Creates a table from the table form, or shows the form again with more
# column rows.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in);
require './sqlite-lib.pl';
&ReadParse();
&error_setup($text{'create_err'});
# Recheck write access before anything else.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
# Keep the row count the form carried between one and fifty.
my $count = int($in{'fields'} || 4);
$count = 1 if ($count < 1);
$count = 50 if ($count > 50);
# Show the form again with more rows, keeping what was typed so far.
if ($in{'more'}) {
$count += 4;
$count = 50 if ($count > 50);
&ui_print_header(&ui_tag('tt', &html_escape($path)),
$text{'create_title'}, '', 'create');
print &ui_page_start();
print &sqlite_table_form($path, \%in, $count);
print &ui_page_end();
&ui_print_footer('database.cgi?db='.&urlize($path),
$text{'database_return'}, '', $text{'index_return'});
exit;
}
# Collect the rows that were filled in, skipping blank ones.
my @columns;
for(my $i = 0; $i < $count; $i++) {
my $name = $in{'name_'.$i};
$name =~ s/^\s+|\s+$//g;
next if ($name eq '');
push(@columns, {
'name' => $name,
'type' => $in{'type_'.$i},
'null' => $in{'null_'.$i} ? 1 : 0,
'key' => $in{'key_'.$i},
'default' => $in{'default_'.$i},
});
}
my $name = $in{'name'};
$name =~ s/^\s+|\s+$//g;
my $sql = eval { &sqlite_create_table_sql($name, \@columns) };
# Report a definition problem as a form error.
if ($@) {
my $message = $@;
chomp($message);
&error($message);
}
my $dbh = &sqlite_connect($path, 1);
# Refuse to replace an existing table or view.
if (&sqlite_object($dbh, $name)) {
$dbh->disconnect();
&error(&text('create_eexists', &html_escape($name)));
}
eval { $dbh->do($sql); };
# Pass SQLite's own objection through the normal error page.
if ($@) {
my $message = $@;
$dbh->disconnect();
&error(&html_escape($message));
}
$dbh->disconnect();
&sqlite_adopt_ownership($path);
&webmin_log('create', 'table', $name, { 'db' => $path });
&redirect('table.cgi?db='.&urlize($path).'&table='.&urlize($name).
'&view=structure');

View File

@@ -1,133 +0,0 @@
#!/usr/local/bin/perl
# Lists the tables and views in an SQLite database.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in, %access);
require './sqlite-lib.pl';
&ReadParse();
# Read objects and summary metrics through a read-only connection.
my $path = &sqlite_database_path($in{'db'});
my $dbh = &sqlite_connect($path, 0);
my $objects = &sqlite_objects($dbh);
my ($journal) = $dbh->selectrow_array('PRAGMA journal_mode');
my ($page_count) = $dbh->selectrow_array('PRAGMA page_count');
my ($page_size) = $dbh->selectrow_array('PRAGMA page_size');
my ($user_version) = $dbh->selectrow_array('PRAGMA user_version');
$dbh->disconnect();
# Put a database that opened successfully at the top of the recent list.
&sqlite_remember_recent($path,
scalar(grep { $_->{'type'} eq 'table' } @$objects));
# Show database metrics above the object list.
&ui_print_header(&ui_tag('tt', &html_escape($path)),
$text{'database_title'}, '', 'database');
print &ui_page_start();
print &ui_tag('style',
'.sqlite_database_stats .ui_stat_value { font-size: 1.48em; }',
{ 'type' => 'text/css' });
my $wal = lc($journal) eq 'wal';
my $writable = &sqlite_can_write($path);
my $sql_writable = &sqlite_can_sql_write($path);
my $database_writable = $writable || $sql_writable;
print &ui_grid([
&ui_card({
'title' => $text{'database_size'},
'body' => &ui_stat({
'value_html' => &nice_size($page_count * $page_size),
'icon' => 'hard-drive',
}),
}),
&ui_card({
'title' => $text{'database_journal'},
'actions' => $wal ? undef : &ui_help($text{'database_wal'}),
'body' => &ui_stat({
'value' => uc($journal),
'icon' => 'database',
'state' => $wal ? 'success' : 'info',
}),
'state' => $wal ? 'success' : 'info',
}),
&ui_card({
'title' => $text{'database_version'},
'body' => &ui_stat({
'value' => int($user_version),
'icon' => 'tag',
}),
}),
&ui_card({
'title' => $text{'database_access'},
'body' => &ui_stat({
'value' => $database_writable ? $text{'database_readwrite'} :
$text{'database_readonly'},
'icon' => $database_writable ? 'edit' : 'lock',
'state' => $database_writable ? 'success' : 'neutral',
}),
'state' => $database_writable ? 'success' : undef,
}),
], { 'min' => '210px', 'gap' => '12px',
'class' => 'sqlite_database_stats' });
# Structured write access offers table creation above a populated list or in
# the empty state.
my $create_url = 'table_form.cgi?db='.&urlize($path);
# List user tables and views when present.
if (@$objects) {
# Offer the table form above the list to users who can write.
if ($writable) {
print &ui_form_start('table_form.cgi');
print &ui_hidden('db', $path);
print &ui_form_end([ [ undef, $text{'database_create_button'} ] ]);
}
print &ui_columns_start([
$text{'database_name'}, $text{'database_type'},
$text{'database_definition'}, $text{'database_action'} ],
100, 0, undef, undef, 1);
foreach my $object (@$objects) {
my $link = 'table.cgi?db='.&urlize($path).
'&table='.&urlize($object->{'name'});
my $sql = $object->{'sql'} || '';
$sql =~ s/\s+/ /g;
# Keep large schema definitions from dominating the object list.
$sql = substr($sql, 0, 160).$text{'value_more'}
if (length($sql) > 160);
print &ui_columns_row([
&ui_tag('a', &html_escape($object->{'name'}), {
'href' => $link,
'style' => 'display:inline-block;white-space:nowrap;',
}),
&html_escape($text{'database_'.$object->{'type'}}),
&ui_tag('tt', &html_escape($sql), {
'style' => 'white-space:normal;overflow-wrap:anywhere;',
}),
&ui_tag('a', $text{'database_browse'}, { 'href' => $link }),
]);
}
print &ui_columns_end();
}
else {
# Show guidance instead of an empty object table: offer the table form
# with structured write access, or point SQL-only users to that page.
print &ui_empty_state({
'title' => $text{'database_empty'},
'desc' => $writable ? $text{'database_empty_create'} :
$sql_writable ? $text{'database_empty_sql'} : undef,
'icon' => 'database',
'actions' => $writable ?
&ui_action_button($text{'database_create_button'}, 'success', {
'href' => $create_url, 'icon' => 'plus' }) : undef,
});
}
# Direct SQL is visible only when explicitly enabled in the module ACL.
if ($access{'sql'}) {
print &ui_form_start('exec_form.cgi');
print &ui_hidden('db', $path);
print &ui_form_end([ [ undef, $text{'database_sql'} ] ]);
}
print &ui_page_end();
&ui_print_footer('', $text{'index_return'});

View File

@@ -1,6 +0,0 @@
paths=/
write=1
create=1
sql=1
work_as_root=1
work_as_user=

View File

@@ -1,66 +0,0 @@
#!/usr/local/bin/perl
# Confirms and deletes selected SQLite table rows.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %config, %in);
require './sqlite-lib.pl';
&ReadParse();
&error_setup($text{'delete_err'});
# Validate the path and require at least one row token.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
my @tokens = split(/\0/, $in{'row'});
@tokens || &error($text{'delete_none'});
# Ask for confirmation when configured.
if (!$in{'confirm'} && $config{'confirm_delete'}) {
&ui_print_header(undef, $text{'delete_title'}, '', 'delete_rows');
print &ui_confirmation_form('delete_rows.cgi',
&text('delete_confirm', scalar(@tokens)),
[ [ 'db', $path ], [ 'table', $in{'table'} ],
[ 'start', int($in{'start'} || 0) ],
map { [ 'row', $_ ] } @tokens ],
[ [ 'confirm', $text{'delete_ok'} ] ]);
&ui_print_footer('table.cgi?db='.&urlize($path).'&table='.
&urlize($in{'table'}), $text{'table_return'});
exit;
}
# Reload the schema before decoding row identities.
my $dbh = &sqlite_connect($path, 1);
my $object = &sqlite_object($dbh, $in{'table'});
$object && $object->{'type'} eq 'table' || &error($text{'table_missing'});
my $columns = &sqlite_table_columns($dbh, $object->{'name'});
my $identity = &sqlite_identity($dbh, $object, $columns);
$identity || &error($text{'table_noidentity'});
my $count = $identity->{'rowid'} ? 1 : scalar(@{$identity->{'columns'}});
# Decode and delete all selected rows in one transaction.
eval {
$dbh->begin_work();
foreach my $token (@tokens) {
my @keys = &sqlite_decode_key($token, $count);
my ($where, @params) = &sqlite_identity_where($identity, \@keys);
$dbh->do('DELETE FROM '.
&sqlite_quote_identifier($object->{'name'}).
' WHERE '.$where, undef, @params);
}
$dbh->commit();
};
# Roll back all deletions if any token or delete fails.
if ($@) {
my $error = $@;
eval { $dbh->rollback(); };
$dbh->disconnect();
&error(&html_escape($error));
}
$dbh->disconnect();
&sqlite_adopt_ownership($path);
&webmin_log('delete', 'rows', scalar(@tokens),
{ 'db' => $path, 'table' => $object->{'name'} });
&redirect('table.cgi?db='.&urlize($path).'&table='.
&urlize($object->{'name'}).'&start='.int($in{'start'} || 0));

View File

@@ -1,58 +0,0 @@
#!/usr/local/bin/perl
# Confirms and drops one column from a table.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in);
require './sqlite-lib.pl';
&ReadParse();
&error_setup($text{'dropcol_err'});
# Recheck write access, library support and the column itself.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
&sqlite_supports('drop_column') || &error($text{'column_eunsupported'});
my $dbh = &sqlite_connect($path, 0);
my $object = &sqlite_object($dbh, $in{'table'});
$object && $object->{'type'} eq 'table' || &error($text{'table_missing'});
my ($column) = grep { $_->{'name'} eq $in{'column'} }
@{&sqlite_table_columns($dbh, $object->{'name'})};
$column || &error($text{'column_emissing'});
my $quoted_table = &sqlite_quote_identifier($object->{'name'});
my $structure = 'table.cgi?db='.&urlize($path).'&table='.
&urlize($object->{'name'}).'&view=structure';
# Always ask first, since the dropped values cannot be recovered.
if (!$in{'confirm'}) {
my ($rows) = $dbh->selectrow_array('SELECT count(*) FROM '.$quoted_table);
$dbh->disconnect();
&ui_print_header(undef, $text{'dropcol_title'}, '', 'drop_column');
print &ui_confirmation_form('drop_column.cgi',
&text('dropcol_confirm',
&ui_tag('tt', &html_escape($column->{'name'})),
&ui_tag('tt', &html_escape($object->{'name'})), $rows),
[ [ 'db', $path ], [ 'table', $object->{'name'} ],
[ 'column', $column->{'name'} ] ],
[ [ 'confirm', $text{'dropcol_ok'} ] ]);
&ui_print_footer($structure, $text{'column_return'});
exit;
}
$dbh->disconnect();
$dbh = &sqlite_connect($path, 1);
eval { $dbh->do('ALTER TABLE '.$quoted_table.' DROP COLUMN '.
&sqlite_quote_identifier($column->{'name'})); };
# SQLite refuses to drop key, unique, indexed and referenced columns, and
# says so in its own words.
if ($@) {
my $message = $@;
$dbh->disconnect();
&error(&html_escape($message));
}
$dbh->disconnect();
&sqlite_adopt_ownership($path);
&webmin_log('drop', 'column', $column->{'name'},
{ 'db' => $path, 'table' => $object->{'name'} });
&redirect($structure);

View File

@@ -1,62 +0,0 @@
#!/usr/local/bin/perl
# Confirms and drops one table or view.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in);
require './sqlite-lib.pl';
&ReadParse();
&error_setup($text{'drop_err'});
# Recheck write access and find the object to drop.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
my $dbh = &sqlite_connect($path, 0);
my $object = &sqlite_object($dbh, $in{'table'});
$object || &error($text{'table_missing'});
my $is_view = $object->{'type'} eq 'view';
my $quoted = &sqlite_quote_identifier($object->{'name'});
# Always ask first, since a dropped table or view cannot be recovered here.
if (!$in{'confirm'}) {
my $message;
# Say how many rows a table holds; a view holds none of its own.
if ($is_view) {
$message = &text('drop_confirm_view',
&ui_tag('tt', &html_escape($object->{'name'})));
}
else {
# Count the table's rows for the warning.
my ($rows) = $dbh->selectrow_array('SELECT count(*) FROM '.$quoted);
$message = &text('drop_confirm',
&ui_tag('tt', &html_escape($object->{'name'})), $rows);
}
$dbh->disconnect();
&ui_print_header(undef,
$text{$is_view ? 'table_drop_view' : 'table_drop'}, '',
'drop_table');
print &ui_confirmation_form('drop_table.cgi', $message,
[ [ 'db', $path ], [ 'table', $object->{'name'} ] ],
[ [ 'confirm', $text{$is_view ? 'table_drop_view' : 'table_drop'} ] ]);
&ui_print_footer('table.cgi?db='.&urlize($path).'&table='.
&urlize($object->{'name'}), $text{'table_return'});
exit;
}
$dbh->disconnect();
# Drop through a read-write connection.
$dbh = &sqlite_connect($path, 1);
eval { $dbh->do(($is_view ? 'DROP VIEW ' : 'DROP TABLE ').$quoted); };
# Report SQLite's own objection through the normal error page.
if ($@) {
my $message = $@;
$dbh->disconnect();
&error(&html_escape($message));
}
$dbh->disconnect();
&sqlite_adopt_ownership($path);
&webmin_log('drop', $is_view ? 'view' : 'table', $object->{'name'},
{ 'db' => $path });
&redirect('database.cgi?db='.&urlize($path));

View File

@@ -1,133 +0,0 @@
#!/usr/local/bin/perl
# Displays a form for adding or editing a table row.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in);
require './sqlite-lib.pl';
&ReadParse();
# Require a writable table with a stable row identity.
my $path = &sqlite_database_path($in{'db'});
&sqlite_can_write($path) || &error($text{'error_readonly'});
my $dbh = &sqlite_connect($path, 0);
my $object = &sqlite_object($dbh, $in{'table'});
$object && $object->{'type'} eq 'table' || &error($text{'table_missing'});
my $columns = &sqlite_table_columns($dbh, $object->{'name'});
my $identity = &sqlite_identity($dbh, $object, $columns);
$identity || &error($text{'table_noidentity'});
my @values = map { undef } @$columns;
my @storage_types = map { undef } @$columns;
my @storage_lengths = map { undef } @$columns;
# Load one existing row. New rows start with undefined values.
if (!$in{'new'}) {
my $count = $identity->{'rowid'} ? 1 : scalar(@{$identity->{'columns'}});
my @keys = &sqlite_decode_key($in{'key'}, $count);
my ($where, @params) = &sqlite_identity_where($identity, \@keys);
my ($value_selects, $type_selects, $length_selects) =
&sqlite_column_selects($columns);
# Measure the row before loading its full values. Run both queries in one
# read transaction so they use the same snapshot.
my @quoted = map { &sqlite_quote_identifier($_->{'name'}) } @$columns;
my $row_size = &sqlite_sum_sql(64 + 64 * 3 * scalar(@$columns),
&sqlite_bytes_sql(\@quoted));
my $from = ' FROM '.&sqlite_quote_identifier($object->{'name'}).
' WHERE '.$where.' LIMIT 2 OFFSET 0';
my $loaded = eval {
$dbh->do('BEGIN DEFERRED');
# Fetch only the row size and storage metadata for the size check.
my $metadata_sql = 'SELECT '.join(', ', $row_size,
@$type_selects, @$length_selects).$from;
my $metadata_rows = &sqlite_select_rows_bounded($dbh,
$metadata_sql, @params);
@$metadata_rows == 1 || die $text{'edit_missing'}."\n";
my @metadata = @{$metadata_rows->[0]};
my $measured_size = shift(@metadata);
# Reject an oversized row before SQLite evaluates its full values.
die &text('error_resultlarge', &sqlite_result_bytes_limit())."\n"
if ($measured_size > &sqlite_result_bytes_limit());
# Split the remaining metadata into type and length lists.
@storage_types = splice(@metadata, 0, scalar(@$columns));
@storage_lengths = @metadata;
undef($metadata_rows);
# Read full values from the same snapshot after the size check passes.
my $value_sql = 'SELECT '.join(', ', @$value_selects).$from;
my $value_rows = &sqlite_select_rows_bounded($dbh,
$value_sql, @params);
@$value_rows == 1 || die $text{'edit_missing'}."\n";
@values = @{$value_rows->[0]};
$dbh->do('COMMIT');
1;
};
# On failure, roll back the snapshot and close the handle before reporting it.
if (!$loaded) {
my $error = $@ || $text{'error_unknown'};
eval { $dbh->do('ROLLBACK'); };
$dbh->disconnect();
&error(&html_escape($error));
}
}
$dbh->disconnect();
# Carry the database, table and row identity into the save request.
&ui_print_header(undef, $in{'new'} ? $text{'edit_title_new'} :
$text{'edit_title'}, '', 'edit_row');
print &ui_form_start('save_row.cgi', 'post');
print &ui_hidden('db', $path);
print &ui_hidden('table', $object->{'name'});
print &ui_hidden('new', $in{'new'} ? 1 : 0);
# Existing rows must carry their opaque identity into the update request.
print &ui_hidden('key', $in{'key'}) if (!$in{'new'});
print &ui_hidden('start', int($in{'start'} || 0));
print &ui_table_start($text{'edit_header'}, undef, 3);
for(my $i = 0; $i < @$columns; $i++) {
my $column = $columns->[$i];
my $value = $values[$i];
my $details = &html_escape($column->{'type'} || $text{'edit_untyped'});
# Mark declared NOT NULL columns as required context for the user.
$details .= ', '.$text{'edit_required'} if ($column->{'notnull'});
# Mark generated columns before rendering them as read-only values.
$details .= ', '.$text{'table_generated'} if ($column->{'hidden'} == 2 ||
$column->{'hidden'} == 3);
my $input;
# Generated columns are displayed but never submitted for modification.
if ($column->{'hidden'} == 2 || $column->{'hidden'} == 3) {
$input = &sqlite_display_value($value, $storage_types[$i],
$storage_lengths[$i]);
}
elsif (!$in{'new'} && ((defined($storage_types[$i]) &&
$storage_types[$i] eq 'blob') ||
&sqlite_value_is_binary($value))) {
# Preserve binary data because the form cannot edit it.
my $length = defined($storage_lengths[$i]) ?
$storage_lengths[$i] : length($value);
$input = &text('edit_blob', $length).
&ui_hidden('keep_'.$i, 1);
}
else {
# Text and NULL values use the normal editable controls.
$input = &ui_textarea('value_'.$i, defined($value) ? $value : '',
3, 60, undef, undef, "style='width: 100%'");
$input .= &ui_tag('br').&ui_checkbox('null_'.$i, 1,
$text{'edit_null'}, !defined($value) && !$in{'new'});
# Let SQLite supply declared defaults and integer primary keys
# on insert.
if ($in{'new'} && (defined($column->{'dflt_value'}) ||
$column->{'pk'} && $column->{'type'} =~ /^INTEGER$/i)) {
$input .= ' '.&ui_checkbox('default_'.$i, 1,
$text{'edit_default'}, 1);
}
}
# Use the full-width row layout in themes that support it.
print &ui_table_row(&html_escape($column->{'name'}).&ui_tag('br').
&ui_tag('small', $details), $input, 2, undef, undef, 1);
}
print &ui_table_end();
print &ui_form_end([ [ undef, $in{'new'} ? $text{'edit_create'} :
$text{'save'} ] ]);
&ui_print_footer('table.cgi?db='.&urlize($path).'&table='.
&urlize($object->{'name'}), $text{'table_return'});

View File

@@ -1,72 +0,0 @@
#!/usr/local/bin/perl
# Executes one SQL statement against an SQLite database.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %config, %in, %access);
require './sqlite-lib.pl';
&ReadParse();
&error_setup($text{'exec_err'});
$access{'sql'} || &error($text{'exec_denied'});
$in{'sql'} =~ /\S/ || &error($text{'exec_empty'});
# Match the connection mode to write access and install the SQL sandbox.
my $path = &sqlite_database_path($in{'db'});
my $write = &sqlite_can_sql_write($path);
my $dbh = &sqlite_connect($path, $write ? 1 : 0, 0, $write ? 1 : 0);
&sqlite_install_authorizer($dbh);
my $limit = int($config{'max_query_rows'} || 1000);
# Use one row as the minimum result limit.
$limit = 1 if ($limit < 1);
# Cap SQL results at 100,000 rows.
$limit = 100000 if ($limit > 100000);
# Execute one statement and preserve any DBI exception.
my $result = eval {
&sqlite_query($dbh, $in{'sql'}, $limit);
};
# Release the database handle before reporting a failed statement.
if ($@) {
my $error = $@;
$dbh->disconnect();
&error(&html_escape($error));
}
$dbh->disconnect();
&sqlite_adopt_ownership($path);
&ui_print_header(undef, $text{'exec_title'}, '', 'exec');
print &ui_tag('p', &text('exec_result',
&ui_tag('tt', &html_escape($in{'sql'}))));
# Statements with named columns render a result table.
if (@{$result->{'columns'}}) {
print &ui_columns_start([ map { &html_escape($_) }
@{$result->{'columns'}} ], 100, 0, undef, undef, 1);
foreach my $row (@{$result->{'rows'}}) {
print &ui_columns_row([ map { &sqlite_display_value($_) } @$row ]);
}
print &ui_columns_end();
# Report whether the row limit or byte limit shortened the output.
if ($result->{'truncated_by'} eq 'bytes') {
# Report the byte limit that stopped result collection.
print &ui_alert_box(&text('exec_truncated_bytes',
&sqlite_result_bytes_limit()), 'warn');
}
elsif ($result->{'truncated_by'} eq 'rows') {
# Report the row limit that stopped result collection.
print &ui_alert_box(&text('exec_truncated', $limit), 'warn');
}
}
else {
# Statements without result columns report their affected-row count.
print &ui_alert_box(&text('exec_affected',
$result->{'affected'} < 0 ? 0 : $result->{'affected'}),
'success', undef, undef, '');
}
# Log the statement, then offer to run another one.
&webmin_log('exec', 'sql', $path, { 'sql' => $in{'sql'} });
print &ui_form_start('exec_form.cgi');
print &ui_hidden('db', $path);
print &ui_form_end([ [ undef, $text{'exec_again'} ] ]);
&ui_print_footer('database.cgi?db='.&urlize($path),
$text{'database_return'});

View File

@@ -1,24 +0,0 @@
#!/usr/local/bin/perl
# Displays the SQLite SQL command form.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%text, %in, %access);
require './sqlite-lib.pl';
&ReadParse();
$access{'sql'} || &error($text{'exec_denied'});
# Preserve the validated database path while the user writes a statement.
my $path = &sqlite_database_path($in{'db'});
&ui_print_header(&ui_tag('tt', &html_escape($path)),
$text{'exec_title'}, '', 'exec');
print &ui_tag('p', $text{'exec_desc'});
print &ui_form_start('exec.cgi', 'post');
print &ui_hidden('db', $path);
print &ui_textarea('sql', $in{'sql'}, 14, 90, undef, undef,
"style='width: 100%'");
print &ui_form_end([ [ undef, $text{'exec_run'} ] ]);
&ui_print_footer('database.cgi?db='.&urlize($path),
$text{'database_return'});

View File

@@ -1,15 +0,0 @@
#!/usr/local/bin/perl
# Removes one database from the current user's recently opened list.
use strict;
use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
our (%in);
require './sqlite-lib.pl';
&ReadParse();
# The path only names an entry in this user's own list, which stores the
# resolved path, so it is matched as given and needs no ACL check.
&sqlite_forget_recent($in{'db'}) if ($in{'db'} ne '');
&redirect('');

View File

@@ -1,34 +0,0 @@
<header>SQLite module access control</header>
These settings control which database files a Webmin user can open and what
the user can do with them. They do not override the filesystem permissions
of the selected Unix user.
<p>For <b>Allowed database directories</b>, enter one absolute directory per
line. The module allows files in those directories and their subdirectories.
Use <tt>$HOME</tt> for the selected Unix user's home directory. Paths outside
the listed directories are rejected. In root mode, <tt>$HOME</tt> refers to
the Unix account whose name matches the Webmin user. SQLite 3.39 or later is
required for a restricted list because it prevents symbolic links in any path
component from escaping these directories. A list containing <tt>/</tt> does
not restrict database paths and does not need this check. This requirement
applies to the SQLite library used by <tt>DBD::SQLite</tt>, which may differ
from the system <tt>sqlite3</tt> command. Unix permissions still apply, but they
do not enforce this module's directory list.</p>
<p>The row and table permission enables forms for editing rows and changing
schemas. The file creation permission allows new files in writable allowed
directories. The module offers file creation only when the user can also add
a table through a form or SQL.</p>
<p><b>Execute SQL statements</b> permits one direct SQL statement at a time.
This is a separate write permission: SQL may change the current database when
its file and directory are writable. The module blocks attached databases,
extensions, and SQL operations that access other files.</p>
<p><b>Unix user for database access</b> selects the filesystem identity used
by the module. Root normally bypasses file ownership and permissions. A
matching or specific Unix user limits access to files that account can read
and write.</p>
<footer>

View File

@@ -1,16 +0,0 @@
<header>Add or Rename Column</header>
This page adds or renames a column without rebuilding the table.
<p>An added column has a name, type and optional default. Existing rows
receive the default, so a column that does not allow NULL must have one.
SQLite does not allow an added column to be a primary key, to be unique, or
to default to the current time or date. Rebuild the table with SQL to make
those changes.</p>
<p>Renaming changes only the name. SQLite also updates dependent indexes,
triggers and views. It rejects the rename if the updated schema would be
ambiguous or invalid.</p>
<p>Changing a column's type, NULL rule, default or key requires rebuilding
the table. Use <b>Execute SQL</b> if your module permissions allow it.</p>

View File

@@ -1,11 +0,0 @@
<header>Database lock wait</header>
This is how long, in milliseconds, SQLite waits for another connection to
release a database lock. The default is 5,000 milliseconds. The maximum is
600,000 milliseconds, or 10 minutes.
<p>A negative value makes a locked operation fail immediately. A longer wait
can help when another application makes short, frequent changes, but it also
makes a blocked Webmin request take longer to fail.</p>
<footer>

View File

@@ -1,10 +0,0 @@
<header>Confirm row deletion</header>
When enabled, deleting selected table rows opens a confirmation page before
the rows are removed. Disable it only when an extra confirmation is not
needed.
<p>Dropping a table, view, or column always requires confirmation because it
can remove stored data.</p>
<footer>

View File

@@ -1,10 +0,0 @@
<header>SQLite Database Browser configuration</header>
These settings control recent-file history, table paging, displayed text,
database lock waits, SQL result limits, and row-deletion confirmation.
<p>They apply to all users of this module. File access and editing
permissions are configured separately under <b>Webmin Users</b> in the
module access-control settings.</p>
<footer>

View File

@@ -1,9 +0,0 @@
<header>Maximum SQL result rows</header>
This is the maximum number of rows displayed after an SQL query. The default
is 1,000 rows. The allowed range is 1 to 100,000 rows.
<p>When a result is larger, the page shows the configured number of rows and
reports that the output was truncated.</p>
<footer>

Some files were not shown because too many files have changed in this diff Show More