mirror of
https://github.com/webmin/webmin.git
synced 2026-10-23 20:50:39 +01:00
Compare commits
7 Commits
dev/fix-cg
...
dev/fix-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925af91ce8 | ||
|
|
e3e6a9b44f | ||
|
|
29cf93863d | ||
|
|
b502deaddf | ||
|
|
0d4c4b7582 | ||
|
|
a272f94d93 | ||
|
|
9026459a53 |
@@ -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
21
fastrpc.cgi
21
fastrpc.cgi
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
24
gray-theme/edit_overlay.cgi
Executable 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'});
|
||||
|
||||
@@ -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 |
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
33
gray-theme/save_overlay.cgi
Executable 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'});
|
||||
@@ -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
15
gray-theme/top.cgi
Executable 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
lang/en
2
lang/en
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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=<All folders>
|
||||
|
||||
@@ -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 = "e_javascript($text{'sform_larger'});
|
||||
my $sform_smaller_js = "e_javascript($text{'sform_smaller'});
|
||||
my $sform_neg0_js = "e_javascript($text{'sform_neg0'});
|
||||
my $sform_neg1_js = "e_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'} ],
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -21,7 +21,7 @@ elsif ($in{'empty'}) {
|
||||
&execute_sql_logged($in{'db'}, "drop table "."estr($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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ if ($in{'confirm'}) {
|
||||
&error_setup($text{'tdrop_err'});
|
||||
&execute_sql_logged($in{'db'}, "drop table "."estr($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 "."estr($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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ if ($in{'confirm'}) {
|
||||
&execute_sql_logged($in{'db'}, "drop table "."estr($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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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 ),
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
©_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");
|
||||
©_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'});
|
||||
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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 ..
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -716,12 +716,10 @@ return "estr($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 "."estr($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);
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 "."e_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>";
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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 "")
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "."e_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(
|
||||
"e_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 = "e_path($config{'psql'})." -f "."e_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 = "e_path($config{'psql'}).
|
||||
" -X -c ".
|
||||
quotemeta("\\restrict $restrict_key").
|
||||
" -f "."e_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, ...)
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'} = '';
|
||||
}
|
||||
}
|
||||
@@ -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'});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" );
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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'});
|
||||
@@ -1,6 +0,0 @@
|
||||
paths=/
|
||||
write=1
|
||||
create=1
|
||||
sql=1
|
||||
work_as_root=1
|
||||
work_as_user=
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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'});
|
||||
@@ -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'});
|
||||
@@ -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'});
|
||||
@@ -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('');
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user