Compare commits

..

1 Commits

Author SHA1 Message Date
Ilia Ross
cbe1f7f17e Add single page layout to Framed Theme
This PR adds a "Page layout" setting to the Framed Theme, with a new single page layout that shows the menu on every page instead of in a frame.

It's meant for people who can't or don't want to use frames, so the Legacy Theme is no longer the only option without frames.

The menu code moved out of `left.cgi` into a shared menu-lib.pl, so both layouts build the same menu, frames look the same as before, and `left.cgi` shrinks from about 400 lines to about 60.

The menu's module search box now follows the global "webminsearch" permission, which the old frame code never actually checked.

One exception to be aware of. A server opened through Webmin Servers Index always uses frames, because that index only rewrites the links and redirects of framed pages.
2026-10-01 05:44:49 +02:00
291 changed files with 4547 additions and 14799 deletions

View File

@@ -1,31 +1,7 @@
## Changelog
#### 2.680 (October, 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/)
* 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
#### 2.671 (October, 2026)
* 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 database backups allowing restricted users to write files or save root-run settings outside their access limits
* 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:
- Add support for Terminal module color preferences
- Add support for the new Cloudmin navigation, graphical console and live virtual machine controls
- Fix spacing and borders in widgets, tables and tab panes
#### 2.670 (September 20, 2026)
* Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details

File diff suppressed because one or more lines are too long

View File

@@ -447,11 +447,7 @@ if ($miniserv{'userdb'} && !$miniserv{'userdb_addto'}) {
$user->{'proto'} = $proto;
}
else {
# Add a single-line record to the local user file
foreach my $value (values %$user) {
$value =~ s/[\r\n].*//s
if (defined($value) && !ref($value));
}
# Adding to local files
&lock_file($ENV{'MINISERV_CONFIG'});
if ($user->{'theme'}) {
$miniserv{"preroot_".$user->{'name'}} =
@@ -632,11 +628,7 @@ if ($user->{'proto'}) {
&disconnect_userdb($miniserv{'userdb'}, $dbh);
}
else {
# Update the single-line record in the local user file
foreach my $value (values %$user) {
$value =~ s/[\r\n].*//s
if (defined($value) && !ref($value));
}
# In local files
&lock_file($ENV{'MINISERV_CONFIG'});
delete($miniserv{"preroot_".$username});
if ($user->{'theme'}) {
@@ -1474,9 +1466,7 @@ return if (&is_readonly_mode());
my $sid = &generate_random_session_id();
return if (!$sid);
my $t = time();
# miniserv only looks up sessions by their hashed key
$sessiondb{&hash_session_id($sid)} =
"$username $t 127.0.0.1".($lifetime ? " ".$lifetime : "");
$sessiondb{$sid} = "$username $t 127.0.0.1".($lifetime ? " ".$lifetime : "");
dbmclose(%sessiondb);
return $sid;
}

View File

@@ -108,11 +108,7 @@ foreach my $u (@users) {
$msg = "<i>".&text('convert_exists', $u->{'user'})."</i>";
$exists++;
}
elsif ($u->{'user'} !~ /^[A-z0-9\-\_\.]+$/ ||
(!$access{'acl'} &&
$u->{'user'} =~ /^(root|admin|sysadm)$/)) {
# Reject invalid names and privileged RPC names that this
# administrator is not allowed to create.
elsif ($u->{'user'} !~ /^[A-z0-9\-\_\.]+$/) {
$msg = "<i>".&text('convert_invalid', $u->{'user'})."</i>";
$invalid++;
}

View File

@@ -28,7 +28,6 @@ else {
my %minfo = $in{'mod'} ? &get_module_info($in{'mod'})
: ( 'desc' => $text{'index_global'} );
%minfo || &error($text{'acl_emod2'});
my $below = &text($in{'group'} ? 'acl_title3' : 'acl_title2',
"<tt>".&html_escape($who)."</tt>",
"<tt>$minfo{'desc'}</tt>");

View File

@@ -129,7 +129,6 @@ save_emd5=The Same as Unix password option cannot be used on systems with MD5 en
save_eunix=The Unix user '$1' does not exist
save_emod=You cannot grant access to module '$1'
save_ecreate=You are not allowed to create users
save_ecreateroot=You are not allowed to create RPC root users
save_euser=You are not allowed to edit this user
save_ecolon=Passwords cannot contain the : character
save_epass=Password is not valid : $1
@@ -202,7 +201,6 @@ acl_chcert=Can change SSL certificate name?
acl_euser=You are not allowed to edit the ACL for this user
acl_egroup=You are not allowed to edit group ACLs
acl_emod=You are not allowed to edit the ACL for this module
acl_emod2=Module does not exist
acl_others=Can see inaccessible modules?
acl_cert=Can request certificate?
acl_err=Failed to save access control

View File

@@ -7,7 +7,7 @@ no warnings 'redefine';
no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access);
$access{'sql'} || &error($text{'sql_ecannot'});
$access{'pass'} || &error($text{'sql_ecannot'});
&ReadParse();
&error_setup($text{'makedn_err'});

View File

@@ -7,7 +7,7 @@ no warnings 'redefine';
no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access);
$access{'sql'} || &error($text{'sql_ecannot'});
$access{'pass'} || &error($text{'sql_ecannot'});
&ReadParse();
&error_setup($text{'make_err'});

View File

@@ -10,7 +10,6 @@ require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $base_remote_user, %gconfig,
$config_directory);
&ReadParse();
$access{'acl'} || &error($text{'acl_emod'});
my $who;
if ($in{'_acl_group'}) {
@@ -31,7 +30,6 @@ my $aclfile = $in{'_acl_group'} ? "$config_directory/$in{'_acl_mod'}/$who.gacl"
: "$config_directory/$in{'_acl_mod'}/$who.acl";
my %minfo = $in{'_acl_mod'} ? &get_module_info($in{'_acl_mod'})
: ( 'desc' => $text{'index_global'} );
%minfo || &error($text{'acl_emod2'});
if ($in{'reset'}) {
# Just remove the .acl file
&lock_file($aclfile);

View File

@@ -9,7 +9,6 @@ no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access, $config_directory);
&ReadParse();
$access{'groups'} || &error($text{'gedit_ecannot'});
# Check for special button clicks, and redirect
if ($in{'but_clone'}) {

View File

@@ -7,7 +7,7 @@ no warnings 'redefine';
no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access);
$access{'sql'} || &error($text{'sql_ecannot'});
$access{'pass'} || &error($text{'sql_ecannot'});
&ReadParse();
&error_setup($text{'sql_err'});
my %miniserv;

View File

@@ -56,14 +56,6 @@ else {
}
&error_setup($text{'save_err'});
if ((!$in{'old'} ||
$in{'name'} ne $in{'old'}) &&
$in{'name'} =~ /^(root|admin|sysadm)$/) {
# User has one of the privileged names, make sure this user can
# edit ACLs to prevent creation of RPC-root users
$access{'acl'} || &error($text{'save_ecreateroot'});
}
# Validate username, and check for a clash
$in{'name'} =~ /^[A-Za-z0-9\-\_\.\@]+$/ && $in{'name'} !~ /^\@/ ||
&error(&text('save_ename', &html_escape($in{'name'})));

View File

@@ -7,7 +7,7 @@ no warnings 'redefine';
no warnings 'uninitialized';
require './acl-lib.pl'; ## no critic
our (%in, %text, %config, %access);
$access{'sql'} || &error($text{'sql_ecannot'});
$access{'pass'} || &error($text{'sql_ecannot'});
&ui_print_unbuffered_header(undef, $text{'schema_title'}, "");

View File

@@ -1265,31 +1265,6 @@ subtest 'CGI harness smoke' => sub {
'error path emits an HTML error page');
};
# All SQL and LDAP user database helpers must use the dedicated SQL access
# flag. Password-editing access alone must not allow direct CGI requests.
subtest 'user database CGI permission gates' => sub {
foreach my $cgi ('schema.cgi', 'maketables.cgi', 'makedn.cgi') {
_reset_fixture();
_seed_user_acl('admin', { pass => 1, sql => 0 });
my $r = run_cgi($cgi, {});
ok(!$r->{location}, "$cgi does not redirect without SQL access");
like($r->{out},
qr/not allowed to configure the user and group database/i,
"$cgi rejects password-only administrators");
}
# The read-only schema endpoint provides a safe positive permission test.
_reset_fixture();
_seed_user_acl('admin', { pass => 0, sql => 1 });
my $r = run_cgi('schema.cgi', {});
is($r->{status}, 0, 'schema.cgi renders with SQL access');
unlike($r->{out},
qr/not allowed to configure the user and group database/i,
'schema.cgi accepts SQL-only administrators');
like($r->{out}, qr/attributetype/i,
'schema.cgi displays the LDAP schema');
};
# delete_user.cgi — four distinct gates. NOTE: <rootdir>/acl/defaultacl has
# every flag set to 1, and get_module_acl merges it under the per-user ACL.
# So each denial test must EXPLICITLY set the relevant key to 0; relying on
@@ -1474,40 +1449,6 @@ subtest 'save_user.cgi create privilege' => sub {
delete_user('admin');
};
# Unix conversion must not bypass the privileged-name guard in save_user.cgi.
# An ACL-capable administrator can still deliberately convert such a user.
subtest 'convert.cgi privileged username gate' => sub {
_reset_fixture();
_seed_user_acl('operator', {
sync => 1, create => 1, acl => 0, gassign => '*',
});
create_group({ name => 'converted', members => [], modules => ['acl'] });
my $r = run_cgi('convert.cgi', {
conv => 1, users => 'root', wgroup => 'converted', sync => 1,
}, user => 'operator');
_clear_caches();
is($r->{status}, 0, 'conversion completes without ACL access');
ok(!get_user('root'),
'conversion skips a privileged RPC username without ACL access');
like($r->{out}, qr/root.*not a valid Webmin username/i,
'conversion reports the privileged username as invalid');
_reset_fixture();
_seed_user_acl('operator', {
sync => 1, create => 1, acl => 1, gassign => '*',
});
create_group({ name => 'converted', members => [], modules => ['acl'] });
$r = run_cgi('convert.cgi', {
conv => 1, users => 'root', wgroup => 'converted', sync => 1,
}, user => 'operator');
_clear_caches();
is($r->{status}, 0, 'conversion completes with ACL access');
ok(get_user('root'),
'ACL-capable administrator can convert a privileged username');
};
subtest 'save_user.cgi gassign whitelist' => sub {
# gassign='admins' allows assigning only to group "admins"; trying to
# assign to a different group should fail.

View File

@@ -1991,56 +1991,29 @@ return @rv;
# or undef.
sub test_config
{
return undef if ($httpd_modules{'core'} < 1.301);
my $okre = qr/(syntax|Checking).*\s+ok/i;
# Build the httpd command from the configured paths
my $httpd = &find_httpd();
my $httpdcmd = "\"$httpd\" -d \"$config{'httpd_dir'}\"";
if ($config{'httpd_conf'}) {
$httpdcmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach my $d (&get_httpd_defines()) {
$httpdcmd .= " -D$d";
}
# Pick the standard test command
my ($cmd, $apachectl);
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl, which may set up environment the config needs
$apachectl = "\"$config{'apachectl_path'}\"";
$cmd = "$apachectl configtest";
}
else {
# Test with httpd
$cmd = "$httpdcmd -t";
}
# Without a global ServerName, Apache looks up the system hostname in DNS
# while loading the config, which stalls the test when DNS is slow. So first
# try the test with a placeholder ServerName. Apache applies -C directives
# before reading the config files, so a ServerName that this check misses,
# such as one inside an IfModule section, still takes effect.
my $conf = &get_config();
if ($conf && !&find_directive("ServerName", $conf)) {
my $noname = "-C \"ServerName localhost\"";
my $out = &backquote_command(
($apachectl ? "$apachectl $noname -t" : "$httpdcmd $noname -t").
" 2>&1");
if ($apachectl && $httpd && $out =~ /no\s+longer\s+supported/i) {
# Red Hat's apachectl refuses extra arguments, and its
# configtest just runs httpd -t, so test with httpd instead
$out = &backquote_command("$httpdcmd $noname -t 2>&1");
if ($httpd_modules{'core'} >= 1.301) {
# Test the configuration with the available command
local $cmd;
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl
$cmd = "\"$config{'apachectl_path'}\" configtest";
}
else {
# Test with httpd
local $httpd = &find_httpd();
$cmd = "\"$httpd\" -d \"$config{'httpd_dir'}\" -t";
if ($config{'httpd_conf'}) {
$cmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach $d (&get_httpd_defines()) {
$cmd .= " -D$d";
}
}
local $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ /(syntax|Checking).*\s+ok/i) {
return $out;
}
return undef if ($out =~ $okre);
}
# Run the standard test. This also runs when the quick test failed, in case
# the extra arguments, rather than the config, caused the failure.
my $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ $okre) {
return $out;
}
return undef;
}

20
apache/config-windows Normal file
View File

@@ -0,0 +1,20 @@
show_list=0
httpd_path=c:/program files/apache group/apache2/bin/Apache.exe
httpd_conf=
httpd_dir=c:/program files/apache group/apache2
mime_types=
srm_conf=
access_conf=
show_order=0
max_servers=100
test_config=0
test_manual=0
test_always=0
test_apachectl=0
start_cmd=net start apache2
stop_cmd=net stop apache2
#apply_cmd="c:/program files/apache group/apache2/bin/Apache.exe" -k restart
auto_mods=1
show_names=0
allow_virtualmin=0
format_config=1

View File

@@ -1,256 +0,0 @@
#!/usr/bin/perl
# Tests for test_config, which must check the Apache config without waiting
# on a DNS lookup of the system hostname.
use strict;
use warnings;
use Test::More;
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use Cwd qw(abs_path);
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..', '..'));
my $tmp = abs_path(tempdir(CLEANUP => 1));
my $webmin_config = File::Spec->catdir($tmp, 'webmin-config');
my $webmin_var = File::Spec->catdir($tmp, 'webmin-var');
my $bin = File::Spec->catdir($tmp, 'bin');
my $log = File::Spec->catfile($tmp, 'commands.log');
# Paths with spaces check that the configured paths are used and quoted
my $apache_root = File::Spec->catdir($tmp, 'apache root');
my $apache_conf = File::Spec->catfile($apache_root, 'main config.conf');
make_path($webmin_config, $webmin_var, "$webmin_config/apache",
"$webmin_var/apache", $apache_root, $bin);
# write_text(file, text, [mode])
# Writes a file, optionally setting its permissions
sub write_text
{
my ($file, $text, $mode) = @_;
open(my $fh, '>', $file) || die "Failed to write $file: $!";
print $fh $text;
close($fh) || die "Failed to close $file: $!";
chmod($mode, $file) || die "Failed to chmod $file: $!" if (defined($mode));
}
# Shell snippet that logs the program name and its arguments, one per field
my $log_args = <<'EOF';
{ printf '%s' "$FAKE_NAME"; for a in "$@"; do printf '|%s' "$a"; done;
printf '\n'; } >> "$FAKE_APACHE_LOG"
EOF
# Fake httpd, which passes or fails the test based on the environment
my $httpd = File::Spec->catfile($bin, 'httpd');
write_text($httpd, "#!/bin/sh\nFAKE_NAME=httpd\n".$log_args.<<'EOF', 0755);
if [ "$FAKE_APACHE_RESULT" = "error" ]; then
echo "AH00526: Syntax error on line 2 of test.conf:"
echo "Invalid command 'Bogus', perhaps misspelled"
exit 1
fi
echo "Syntax OK"
EOF
# Debian-style apache2ctl, which passes extra arguments to httpd
my $debian_ctl = File::Spec->catfile($bin, 'debian-ctl');
write_text($debian_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
case "\$*" in
configtest) exec "$httpd" -t ;;
*) exec "$httpd" "\$@" ;;
esac
EOF
# Red Hat-style apachectl, which refuses extra arguments
my $redhat_ctl = File::Spec->catfile($bin, 'redhat-ctl');
write_text($redhat_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
if [ "x\$2" != "x" ]; then
echo "Passing arguments to httpd using apachectl is no longer supported."
exit 1
fi
case "\$1" in
configtest|-t) exec "$httpd" -t ;;
esac
exit 2
EOF
# Wrapper that only understands configtest
my $strict_ctl = File::Spec->catfile($bin, 'strict-ctl');
write_text($strict_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
case "\$1" in
configtest) exec "$httpd" -t ;;
esac
echo "usage: apachectl configtest"
exit 2
EOF
write_text(File::Spec->catfile($webmin_config, 'config'),
"os_type=debian-linux\n".
"os_version=12\n".
"real_os_type=Debian Linux\n".
"real_os_version=12\n");
write_text(File::Spec->catfile($webmin_config, 'miniserv.conf'),
"root=$root\n");
write_text(File::Spec->catfile($webmin_config, 'apache', 'config'),
"httpd_dir=$apache_root\n".
"httpd_path=$httpd\n".
"httpd_conf=$apache_conf\n".
"apachectl_path=$debian_ctl\n".
"httpd_version=2.4.57\n".
"test_apachectl=0\n".
"test_config=1\n");
$ENV{'WEBMIN_CONFIG'} = $webmin_config;
$ENV{'WEBMIN_VAR'} = $webmin_var;
$ENV{'FOREIGN_MODULE_NAME'} = 'apache';
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $root;
$ENV{'REMOTE_USER'} = 'root';
$ENV{'FAKE_APACHE_LOG'} = $log;
$ENV{'FAKE_APACHE_RESULT'} = 'ok';
unshift(@INC, $root);
require File::Spec->catfile($root, 'apache', 'apache-lib.pl');
{
no warnings 'once';
$main::httpd_modules{'core'} = 2.4;
}
# set_apache_conf(text)
# Writes the main Apache config file and clears the cached copy
sub set_apache_conf
{
my ($text) = @_;
write_text($apache_conf, $text);
main::flush_config_cache();
}
# A ServerName inside a virtual host is not global, so Apache still looks up
# the system hostname
my $no_global_name = "Listen 80\n".
"<VirtualHost *:80>\n".
" ServerName vhost.example\n".
"</VirtualHost>\n";
set_apache_conf($no_global_name);
# Expected log lines for the fake commands
my $noname = 'ServerName localhost';
my $direct_fast = join('|', 'httpd', '-d', $apache_root, '-f', $apache_conf,
'-C', $noname, '-t');
my $direct_std = join('|', 'httpd', '-d', $apache_root, '-f', $apache_conf,
'-t');
my $ctl_fast = join('|', 'apachectl', '-C', $noname, '-t');
my $ctl_std = 'apachectl|configtest';
my $httpd_fast = join('|', 'httpd', '-C', $noname, '-t');
my $httpd_std = 'httpd|-t';
# run_test(test-apachectl, [apachectl-path], result)
# Runs test_config with the given settings, and returns its result and the
# commands it ran
sub run_test
{
my ($use_ctl, $ctl, $result) = @_;
no warnings 'once';
$main::config{'test_apachectl'} = $use_ctl;
$main::config{'apachectl_path'} = $ctl if ($ctl);
$ENV{'FAKE_APACHE_RESULT'} = $result;
unlink($log);
my $err = main::test_config();
my @cmds;
if (open(my $fh, '<', $log)) {
chomp(@cmds = <$fh>);
close($fh);
}
return ($err, \@cmds);
}
subtest 'httpd test skips the hostname lookup' => sub {
my ($err, $cmds) = run_test(0, undef, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $direct_fast ],
'httpd runs once with configured paths and ServerName');
};
subtest 'httpd test reports config errors' => sub {
my ($err, $cmds) = run_test(0, undef, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $direct_fast, $direct_std ],
'failure is confirmed with the standard test');
};
subtest 'Debian apachectl passes ServerName to httpd' => sub {
my ($err, $cmds) = run_test(1, $debian_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $httpd_fast ],
'apachectl runs httpd with the placeholder ServerName');
};
subtest 'Debian apachectl reports config errors' => sub {
my ($err, $cmds) = run_test(1, $debian_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $ctl_fast, $httpd_fast, $ctl_std, $httpd_std ],
'failure is confirmed with apachectl configtest');
};
subtest 'Red Hat apachectl falls back to httpd' => sub {
my ($err, $cmds) = run_test(1, $redhat_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $direct_fast ],
'httpd is run directly when apachectl refuses arguments');
};
subtest 'Red Hat apachectl reports config errors' => sub {
my ($err, $cmds) = run_test(1, $redhat_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
unlike($err, qr/no longer supported/,
'apachectl refusal is not reported as a config error');
is_deeply($cmds, [ $ctl_fast, $direct_fast, $ctl_std, $httpd_std ],
'failure is confirmed with apachectl configtest');
};
subtest 'unknown apachectl falls back to configtest' => sub {
my ($err, $cmds) = run_test(1, $strict_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $ctl_std, $httpd_std ],
'standard test runs when apachectl rejects the arguments');
};
subtest 'global ServerName uses only the standard test' => sub {
set_apache_conf("ServerName www.example.com\n".$no_global_name);
my ($err, $cmds) = run_test(0, undef, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $direct_std ], 'httpd runs without a placeholder');
($err, $cmds) = run_test(1, $redhat_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $ctl_std, $httpd_std ],
'apachectl configtest runs as before');
set_apache_conf($no_global_name);
};
subtest 'global ServerName in an included file is found' => sub {
# Webmin expands includes with glob(), which splits paths on spaces
my $inc = File::Spec->catfile($tmp, 'servername.conf');
write_text($inc, "ServerName www.example.com\n");
set_apache_conf("Include $inc\n".$no_global_name);
my ($err, $cmds) = run_test(1, $debian_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_std, $httpd_std ],
'apachectl configtest runs as before');
unlink($inc);
set_apache_conf($no_global_name);
};
subtest 'old Apache versions are not tested' => sub {
no warnings 'once';
local $main::httpd_modules{'core'} = 1.3;
my ($err, $cmds) = run_test(0, undef, 'error');
is($err, undef, 'no error is reported');
is_deeply($cmds, [ ], 'no test command runs');
};
done_testing();

View File

@@ -0,0 +1,12 @@
driver=Pg
user=bacula
pass=opencountry
db=bacula
bacula_dir=c:/bacula/bin
bextract=bextract
bls=bls
btape=btape
wait=1
apply=1
showdirs=0
init_start=0

61
bind8/config-windows Normal file
View File

@@ -0,0 +1,61 @@
soa_style=1
named_path=c:/windows/system32/dns/bin/named.exe
named_conf=c:/windows/system32/dns/etc/named.conf
show_list=1
records_order=0
max_zones=50
rev_def=0
support_aaaa=1
allow_comments=0
allow_wild=1
allow_long=0
master_ttl=1
whois_cmd=c:/windows/system32/dns/bin/whois.exe
short_names=0
updserial_def=0
updserial_on=1
updserial_man=1
ndc_cmd=c:/windows/system32/dns/bin/ndc.exe
forwardzonefilename_format=ZONE.hosts
reversezonefilename_format=ZONE.rev
rev_must=0
ipv6_mode=1
allow_underscore=0
by_view=0
confirm_zone=1
confirm_rec=0
no_chroot=0
relative_paths=0
rndc_cmd=c:/windows/system32/dns/bin/rndc.exe
no_pid_chroot=0
soa_start=0
rndc_conf=c:/windows/system32/dns/etc/rndc.conf
rndcconf_cmd=c:/windows/system32/dns/bin/rndc-confgen.exe
largezones=0
auto_chroot=
pid_file=c:/windows/system32/dns/etc/named.pid
default_prins=
restart_cmd=sc restart named
file_perms=
extra_reverse=
default_master=
master_dir=
chroot=
file_owner=
named_group=
this_ip=
stop_cmd=sc stop named
named_user=
free_nets=
start_cmd=sc start named
zones_file=
extra_forward=
slave_dir=
keygen=dnssec-keygen
signzone=dnssec-signzone
checkconf=named-checkconf
other_slaves=1
force_random=0
spf_record=0
dnssec_info=1
allow_alias=0

View File

@@ -48,10 +48,6 @@ print &ui_table_row($text{'acl_move'},
&ui_radio("move", $_[0]->{'move'},
[ [ 1, $text{'yes'} ], [ 0, $text{'no'} ] ]));
print &ui_table_row($text{'acl_env'},
&ui_radio("env", $_[0]->{'env'},
[ [ 1, $text{'yes'} ], [ 0, $text{'no'} ] ]));
print &ui_table_row($text{'acl_kill'},
&ui_radio("kill", $_[0]->{'kill'},
[ [ 1, $text{'yes'} ], [ 0, $text{'no'} ] ]));
@@ -83,7 +79,6 @@ $_[0]->{'command'} = $in{'command'};
$_[0]->{'create'} = $in{'create'};
$_[0]->{'delete'} = $in{'delete'};
$_[0]->{'move'} = $in{'move'};
$_[0]->{'env'} = $in{'env'};
$_[0]->{'kill'} = $in{'kill'};
$_[0]->{'hourly'} = $in{'hourly'};
$_[0]->{'stop'} = $in{'stop'};

14
cron/config-windows Normal file
View File

@@ -0,0 +1,14 @@
cron_input=0
vixie_cron=0
cron_deny_all=2
max_len=80
show_run=0
show_time=0
match_mode=0
match_user=1
kill_subs=0
single_file=c:/cronw/crontab.txt
show_comment=0
hourly_only=0
max_jobs=400
show_next=0

View File

@@ -1079,8 +1079,6 @@ if ($in{'special_def'}) {
foreach $arr (@pers) {
delete($job->{$arr});
}
# Special times are single words like daily or reboot
$in{'special'} =~ /^[a-z]+$/i || &error($text{'save_especial'});
$job->{'special'} = $in{'special'};
}
else {
@@ -1094,11 +1092,6 @@ else {
# Need to work out and simplify ranges selected
local (@range, @newrange, $i);
@range = split(/\0/, $in{$arr});
# Selected times are plain numbers
foreach my $v (@range) {
$v =~ /^\d+$/ || &error(&text('save_etime',
$text{"edit_$arr"}));
}
@range = sort { $a <=> $b } @range;
local $start = -1;
for($i=0; $i<@range; $i++) {
@@ -1175,11 +1168,6 @@ if ($in->{'range_def'}) {
else {
# Validate and store range
foreach my $r ("start", "end") {
# Dates go into the job's command, so only allow numbers
foreach my $f ("day", "month", "year") {
$in->{'range_'.$r.'_'.$f} =~ /^\d+$/ ||
&error($text{'range_e'.$r});
}
eval { timelocal(0, 0, 0, $in->{'range_'.$r.'_day'},
$in->{'range_'.$r.'_month'}-1,
$in->{'range_'.$r.'_year'}-1900) };
@@ -1320,43 +1308,6 @@ elsif ($config{cron_deny_all} == 1) {
return !$err;
}
=head2 get_cron_move_target(\@jobs, index, direction)
Returns the Cron entry that the indexed entry would be swapped with for an up,
down, top or bottom move.
=cut
sub get_cron_move_target
{
my ($jobs, $idx, $direction) = @_;
my $job = $jobs->[$idx];
return if (!$job);
if ($direction eq "up") {
return $idx > 0 ? $jobs->[$idx-1] : undef;
}
elsif ($direction eq "down") {
return $idx < $#$jobs ? $jobs->[$idx+1] : undef;
}
elsif ($direction eq "top") {
my $swap;
for(my $i=$idx-1;
$i >= 0 && $jobs->[$i]->{'file'} eq $job->{'file'}; $i--) {
$swap = $jobs->[$i];
}
return $swap;
}
elsif ($direction eq "bottom") {
my $swap;
for(my $i=$idx+1;
$i < @$jobs && $jobs->[$i]->{'file'} eq $job->{'file'}; $i++) {
$swap = $jobs->[$i];
}
return $swap;
}
return;
}
=head2 swap_cron_jobs(&job1, &job2)
Swaps two Cron jobs, which must be in the same file, identified by their

View File

@@ -6,7 +6,6 @@ command=1
create=1
delete=1
move=1
env=1
kill=1
hourly=2
stop=1

View File

@@ -6,10 +6,9 @@ require './cron-lib.pl';
&ReadParse();
@jobs = &list_cron_jobs();
$job = $jobs[$in{'idx'}];
$job && !$job->{'name'} || &error($text{'edit_egone'});
&lock_file($job->{'file'});
$access{'delete'} && &can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
&lock_file($job->{'file'});
&delete_cron_job($job);
&unlock_file($job->{'file'});
&webmin_log("delete", "cron", $job->{'user'}, $job);

View File

@@ -4,13 +4,11 @@
require './cron-lib.pl';
&ReadParse();
$access{'env'} || &error($text{'env_ecannot'});
@jobs = &list_cron_jobs();
$job = $jobs[$in{'idx'}];
$job && $job->{'name'} || &error($text{'env_egone'});
$access{'delete'} && &can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
&lock_file($job->{'file'});
&can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
&delete_cron_job($job);
&unlock_file($job->{'file'});
&webmin_log("delete", "env", $job->{'user'}, $job);

View File

@@ -9,20 +9,13 @@ require './cron-lib.pl';
@d || &error($text{'delete_enone'});
if ($in{'delete'}) {
# Validate all selected jobs before changing any of them
@selected = ( );
# Delete selected jobs
foreach $d (sort { $b <=> $a } @d) {
($job) = grep { $_->{'index'} eq $d } @jobs;
$job || next;
!$job->{'name'} || $access{'env'} ||
&error($text{'env_ecannot'});
&lock_file($job->{'file'});
$access{'delete'} && &can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
push(@selected, $job);
}
# Delete selected jobs
foreach $job (@selected) {
&lock_file($job->{'file'});
&delete_cron_job($job);
@jobs = grep { $_ ne $job } @jobs;
}
@@ -30,20 +23,13 @@ if ($in{'delete'}) {
&webmin_log("delete", "crons", scalar(@d));
}
elsif ($in{'disable'} || $in{'enable'}) {
# Validate all selected jobs before changing any of them
@selected = ( );
# Disable selected
foreach $d (@d) {
($job) = grep { $_->{'index'} eq $d } @jobs;
$job || next;
!$job->{'name'} || $access{'env'} ||
&error($text{'env_ecannot'});
&lock_file($job->{'file'});
&can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
push(@selected, $job);
}
# Enable or disable selected jobs
foreach $job (@selected) {
&lock_file($job->{'file'});
$job->{'active'} = $in{'disable'} ? 0 : 1;
&change_cron_job($job);
@jobs = grep { $_ ne $job } @jobs;

View File

@@ -8,7 +8,7 @@ require './cron-lib.pl';
if (!$in{'new'}) {
$job = $jobs[$in{'idx'}];
$job && !$job->{'name'} || &error($text{'edit_egone'});
$job || &error($text{'edit_egone'});
&can_edit_user(\%access, $job->{'user'}) ||
&error($text{'edit_ecannot'});
&ui_print_header(undef, $text{'edit_title'}, "");
@@ -18,7 +18,7 @@ else {
if (defined($in{'clone'})) {
# Default to clone source
$clone = $jobs[$in{'clone'}];
$clone && !$clone->{'name'} || &error($text{'edit_egone'});
$clone || &error($text{'edit_egone'});
&can_edit_user(\%access, $clone->{'user'}) ||
&error($text{'edit_ecannot'});
$job = { %$clone };

View File

@@ -4,18 +4,15 @@
require './cron-lib.pl';
&ReadParse();
$access{'env'} || &error($text{'env_ecannot'});
if (!$in{'new'}) {
@jobs = &list_cron_jobs();
$env = $jobs[$in{'idx'}];
$env && $env->{'name'} || &error($text{'env_egone'});
&can_edit_user(\%access, $env->{'user'}) ||
&error($text{'edit_ecannot'});
&ui_print_header(undef, $text{'env_title1'}, "");
}
else {
$access{'create'} || &error($text{'save_ecannot2'});
&ui_print_header(undef, $text{'env_title2'}, "");
$env = { 'active' => 1 };
}
@@ -71,9 +68,8 @@ if ($in{'new'}) {
print &ui_form_end([ [ undef, $text{'create'} ] ]);
}
else {
my @buttons = ( [ undef, $text{'save'} ] );
push(@buttons, [ "delete", $text{'delete'} ]) if ($access{'delete'});
print &ui_form_end(\@buttons);
print &ui_form_end([ [ undef, $text{'save'} ],
[ "delete", $text{'delete'} ] ]);
}
&ui_print_footer("", $text{'index_return'});

View File

@@ -7,7 +7,6 @@ require './cron-lib.pl';
@jobs = &list_cron_jobs();
$job = $jobs[$in{'idx'}];
$job && !$job->{'name'} || &error($text{'edit_egone'});
&can_edit_user(\%access, $job->{'user'}) || &error($text{'exec_ecannot'});
&foreign_require("proc", "proc-lib.pl");

View File

@@ -52,8 +52,7 @@ if ($access{'create'}) {
$text{'index_create'}) );
push(@crlinks, &ui_link("edit_env.cgi?new=1&search=".
&urlize($in{'search'}),
$text{'index_ecreate'}) )
if ($env_support && $access{'env'});
$text{'index_ecreate'}) ) if ($env_support);
}
if ($config{cron_allow_file} && $config{cron_deny_file} && $access{'allow'}) {
push(@crlinks, &ui_link("edit_allow.cgi", $text{'index_allow'}) );
@@ -107,11 +106,9 @@ foreach $u (@ulist) {
if ($job->{'name'}) {
# An environment variable - show the name only
$cmdidx = scalar(@cols);
local $envdesc = "<i>$text{'index_env'}</i> ".
"<tt>@{[&html_escape($job->{'name'})]} = @{[&html_escape($job->{'value'})]}</tt>";
push(@cols, $access{'env'} ?
&ui_link("edit_env.cgi?idx=".$idx, $envdesc) :
$envdesc);
push(@cols, &ui_link("edit_env.cgi?idx=".$idx,
"<i>$text{'index_env'}</i> ".
"<tt>@{[&html_escape($job->{'name'})]} = @{[&html_escape($job->{'value'})]}</tt>") );
$donelink = 1;
}
elsif (@exp && $access{'command'}) {
@@ -222,7 +219,6 @@ foreach $u (@ulist) {
push(@cols, $job->{'command'}.' '.$job->{'name'}.' '.
$job->{'comment'});
push(@rows, \@cols);
$noenv{$idx}++ if ($job->{'name'} && !$access{'env'});
}
}
@@ -268,10 +264,8 @@ elsif (@rows) {
$access{'move'} ? ( $text{'index_move'} ) : ( ),
], 100, 0, \@tds);
foreach my $r (@rows) {
# Variables cannot be selected without permission to edit them
print &ui_checked_columns_row([ @$r[1..(@$r-2)] ],
\@tds, "d", $r->[0], 0,
$noenv{$r->[0]});
\@tds, "d", $r->[0]);
}
print &ui_columns_end();
print &ui_links_row(\@links);

View File

@@ -7,7 +7,6 @@ require './cron-lib.pl';
@jobs = &list_cron_jobs();
$job = $jobs[$in{'idx'}];
$job && !$job->{'name'} || &error($text{'edit_egone'});
&can_edit_user(\%access, $job->{'user'}) || &error($text{'kill_ecannot'});
$proc = &find_cron_process($job);
$proc || &error($text{'kill_egone'});

View File

@@ -81,11 +81,6 @@ lcedit_special_reboot=when system boots
save_err=Failed to save cron job
save_ecmd=You did not enter a command to execute
save_enewline=The command and comment cannot contain line breaks
save_especial=Invalid special time selected
save_etime=Invalid $1 selected
save_ename=Missing or invalid environment variable name
save_evalue=Invalid environment variable value
save_euser=You must select a user
save_euser2=User '$1' does not exist
save_eallow=The user '$1' is not allowed to access cron
@@ -93,7 +88,6 @@ save_ecannot=You are not allowed to create or edit cron jobs for '$1'
save_enone=You did not choose any $1 to execute
save_ecannot2=You are not allowed to create cron jobs
save_eidx=Cron job was successfully saved, but cannot be run as it was not found!
save_ecomment=Invalid characters in comment
allow_title=Control Cron Access
allow_desc=This form allows you to control which users can create and run cron jobs.
@@ -119,8 +113,6 @@ env_where2=Move environment variable
env_top=Before all Cron jobs for user
env_bot=After all Cron jobs
env_leave=Current position
env_ecannot=You are not allowed to edit environment variables
env_egone=Environment variable does not exist
exec_title=Execute Cron Job
exec_ecannot=You are not allowed to execute cron jobs for this user
@@ -140,7 +132,6 @@ acl_gid=Users with primary group
acl_create=Can create Cron jobs?
acl_delete=Can delete Cron jobs?
acl_move=Can move Cron jobs?
acl_env=Can edit environment variables?
acl_kill=Can terminate Cron jobs?
acl_hourly=Limit jobs to at most hourly?
acl_hourlydef=As set in Module Config

View File

@@ -8,32 +8,35 @@ require './cron-lib.pl';
@jobs = &list_cron_jobs();
$job = $jobs[$in{'idx'}];
$job || &error($text{'edit_egone'});
if ($in{'up'}) {
$direction = "up";
$swap = $jobs[$in{'idx'}-1];
}
elsif ($in{'down'}) {
$direction = "down";
$swap = $jobs[$in{'idx'}+1];
}
elsif ($in{'top'}) {
$direction = "top";
for(my $i=$in{'idx'};
$i && $jobs[$i]->{'file'} eq $job->{'file'}; $i--) {
$swap = $jobs[$i];
}
}
elsif ($in{'bottom'}) {
$direction = "bottom";
for(my $i=$in{'idx'};
$i < @jobs && $jobs[$i]->{'file'} eq $job->{'file'}; $i++) {
$swap = $jobs[$i];
}
}
else {
&error("Unknown mode!");
}
$swap = &get_cron_move_target(\@jobs, $in{'idx'}, $direction);
$swap || &error("No job to swap with found");
$access{'move'} && &can_edit_user(\%access, $job->{'user'}) ||
&error(&text('save_ecannot', $job->{'user'}));
&can_edit_user(\%access, $swap->{'user'}) ||
&error(&text('save_ecannot', $swap->{'user'}));
$job->{'file'} eq $swap->{'file'} &&
($job->{'name'} || $job->{'type'} == 0 || $job->{'type'} == 3) &&
($swap->{'name'} || $swap->{'type'} == 0 || $swap->{'type'} == 3) ||
&error($text{'move_etype'});
($job->{'type'} == 0 || $job->{'type'} == 3) &&
($swap->{'type'} == 0 || $swap->{'type'} == 3) || &error($text{'move_etype'});
&lock_file($job->{'file'});
&swap_cron_jobs($job, $swap);
&unlock_file($job->{'file'});

View File

@@ -1,2 +1 @@
stop=0
env=0

View File

@@ -4,6 +4,5 @@ command=1
create=1
delete=1
move=1
env=0
kill=1
hourly=0

View File

@@ -13,7 +13,6 @@ if ($in{'new'}) {
}
else {
$oldjob = $jobs[$in{'idx'}];
$oldjob && !$oldjob->{'name'} || &error($text{'edit_egone'});
$job->{'type'} = $oldjob->{'type'};
$job->{'file'} = $oldjob->{'file'};
$job->{'line'} = $oldjob->{'line'};
@@ -38,10 +37,6 @@ foreach $f (@files) { &lock_file($f); }
if ($in{"cmd"} !~ /\S/ && $access{'command'}) {
&error($text{'save_ecmd'});
}
# A line break would add extra lines to the crontab
if ($in{'cmd'} =~ /[\r\n]/ || $in{'comment'} =~ /[\r\n]/) {
&error($text{'save_enewline'});
}
if (&supports_users()) {
if (!$in{'user'}) {
&error($text{'save_euser'});
@@ -75,7 +70,6 @@ if (defined($in{'range_def'})) {
&unconvert_range($job);
}
$in{'comment'} =~ /\r|\n/ && &error($text{'save_ecomment'});
$job->{'comment'} = $in{'comment'};
&unconvert_comment($job);

View File

@@ -5,16 +5,13 @@
require './cron-lib.pl';
&error_setup($text{'env_err'});
&ReadParse();
$access{'env'} || &error($text{'env_ecannot'});
@jobs = &list_cron_jobs();
if ($in{'new'}) {
$access{'create'} || &error($text{'save_ecannot2'});
$job = { };
}
else {
$oldjob = $jobs[$in{'idx'}];
$oldjob && $oldjob->{'name'} || &error($text{'env_egone'});
$job->{'file'} = $oldjob->{'file'};
$job->{'line'} = $oldjob->{'line'};
}
@@ -41,9 +38,6 @@ foreach $f (@files) { &lock_file($f); }
if ($in{'name'} !~ /^\S+$/) {
&error($text{'save_ename'});
}
if ($in{'value'} =~ /\r|\n/) {
&error($text{'save_evalue'});
}
if (!$in{'user'}) {
&error($text{'save_euser'});
}

View File

@@ -1,299 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
use Cwd qw(abs_path);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use IPC::Open3 ();
use Symbol qw(gensym);
my $bindir = abs_path($0);
$bindir =~ s{/[^/]+$}{};
my $rootdir = abs_path("$bindir/../..") or die "rootdir: $!";
my $confdir = tempdir(CLEANUP => 1);
my $vardir = tempdir(CLEANUP => 1);
open(my $cfh, ">", "$confdir/config") or die "config: $!";
print $cfh "os_type=generic-linux\nos_version=0\n";
close($cfh);
open(my $vfh, ">", "$confdir/var-path") or die "var-path: $!";
print $vfh "$vardir\n";
close($vfh);
make_path("$confdir/cron");
my $miniservconf = "$confdir/miniserv.conf";
open(my $mfh, ">", $miniservconf) or die "miniserv.conf: $!";
print $mfh "root=$rootdir\n";
close($mfh);
$ENV{'WEBMIN_CONFIG'} = $confdir;
$ENV{'WEBMIN_VAR'} = $vardir;
$ENV{'FOREIGN_MODULE_NAME'} = 'cron';
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $rootdir;
chdir("$rootdir/cron") or die "chdir: $!";
require "$rootdir/cron/cron-lib.pl";
my $other = "/tmp/other-crontab";
my $file = "/tmp/test-crontab";
my @jobs = (
{ 'index' => 0, 'file' => $other, 'type' => 0 },
{ 'index' => 1, 'file' => $file, 'type' => 0 },
{ 'index' => 2, 'file' => $file, 'type' => 0 },
{ 'index' => 3, 'file' => $file, 'name' => 'PATH' },
{ 'index' => 4, 'file' => $file, 'type' => 0 },
{ 'index' => 5, 'file' => $file, 'type' => 0 },
{ 'index' => 6, 'file' => $other, 'type' => 0 },
);
my @leading_jobs = (
{ 'index' => 0, 'file' => $file, 'type' => 0 },
{ 'index' => 1, 'file' => $file, 'type' => 0 },
);
is(get_cron_move_target(\@jobs, 4, "up"), $jobs[3],
"up targets the previous entry");
is(get_cron_move_target(\@jobs, 2, "down"), $jobs[3],
"down targets the next entry");
is(get_cron_move_target(\@jobs, 5, "top"), $jobs[1],
"top targets the first entry in the same file");
is(get_cron_move_target(\@leading_jobs, 1, "top"), $leading_jobs[0],
"top can target the first entry in the full list");
is(get_cron_move_target(\@jobs, 1, "bottom"), $jobs[5],
"bottom targets the last entry in the same file");
is(get_cron_move_target(\@jobs, 1, "up"), $jobs[0],
"up returns an entry from another file for caller validation");
is(get_cron_move_target(\@jobs, 0, "top"), undef,
"top has no target for the first entry");
is(get_cron_move_target(\@jobs, 5, "bottom"), undef,
"bottom has no target for the last entry in a file");
my $cronfile = "$vardir/crontab";
my $crondir = "$vardir/crontabs";
make_path($crondir);
make_path("$vardir/cron.d");
# Write the Cron module config, with the given lines added.
sub write_cron_config
{
my (@lines) = @_;
open(my $fh, ">", "$confdir/cron/config") or die "cron config: $!";
print $fh map { "$_\n" } (@lines,
"cron_dir=$crondir",
"system_crontab=$vardir/system-crontab",
"cronfiles_dir=$vardir/cron.d",
"vixie_cron=1",
"cron_deny_all=2");
close($fh);
}
write_cron_config("single_file=$cronfile");
# Reset the crontab fixture to its original contents.
sub write_crontab
{
open(my $fh, ">", $cronfile) or die "crontab: $!";
print $fh "* * * * * echo first\n";
print $fh "* * * * * echo second\n";
print $fh "PATH=/usr/bin\n";
print $fh "* * * * * echo third\n";
print $fh "* * * * * echo fourth\n";
close($fh);
}
# Return the current crontab fixture contents.
sub read_crontab_fixture
{
open(my $fh, "<", $cronfile) or die "crontab: $!";
local $/;
my $contents = <$fh>;
close($fh);
return $contents;
}
# Write the requested module ACL values for the CGI subprocess.
sub seed_acl
{
my (%acl) = @_;
open(my $fh, ">", "$confdir/cron/admin.acl") or die "admin.acl: $!";
foreach my $key (sort keys %acl) {
print $fh "$key=$acl{$key}\n";
}
close($fh);
}
# Encode one form value for an application/x-www-form-urlencoded body.
sub url_encode
{
my ($value) = @_;
$value = "" if (!defined($value));
$value =~ s/([^A-Za-z0-9._~-])/sprintf("%%%02X", ord($1))/ge;
return $value;
}
# Serialize a hash of scalar or array form values.
sub form_body
{
my ($form) = @_;
my @pairs;
foreach my $key (sort keys %$form) {
my @values = ref($form->{$key}) eq "ARRAY" ?
@{$form->{$key}} : ($form->{$key});
push(@pairs, map { url_encode($key)."=".url_encode($_) } @values);
}
return join("&", @pairs);
}
# Run one Cron CGI with the supplied form and isolated Webmin environment.
sub run_cgi
{
my ($cgi, $form) = @_;
my $body = form_body($form);
my %env = (
'PATH' => $ENV{'PATH'},
'WEBMIN_CONFIG' => $confdir,
'WEBMIN_VAR' => $vardir,
'FOREIGN_MODULE_NAME' => 'cron',
'FOREIGN_ROOT_DIRECTORY' => $rootdir,
'MINISERV_CONFIG' => $miniservconf,
'REMOTE_USER' => 'admin',
'BASE_REMOTE_USER' => 'admin',
'REQUEST_METHOD' => 'POST',
'SCRIPT_NAME' => "/cron/$cgi",
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
'CONTENT_LENGTH' => length($body),
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => '10000',
'HTTP_HOST' => 'localhost:10000',
);
my $errfh = gensym();
local %ENV = %env;
my $pid = IPC::Open3::open3(my $in, my $out, $errfh,
$^X, "-I$rootdir", "$rootdir/cron/$cgi");
print $in $body;
close($in);
local $/;
my $stdout = <$out>;
my $stderr = <$errfh>;
$stdout = "" if (!defined($stdout));
$stderr = "" if (!defined($stderr));
close($out);
close($errfh);
waitpid($pid, 0);
return { 'out' => $stdout, 'err' => $stderr, 'status' => $? >> 8 };
}
my %full_acl = (
'mode' => 0,
'command' => 1,
'create' => 1,
'delete' => 1,
'move' => 1,
'env' => 1,
);
write_crontab();
my $original = read_crontab_fixture();
seed_acl(%full_acl, 'env' => 0);
my $result = run_cgi("delete_cron.cgi", { 'idx' => 2 });
like($result->{'out'}, qr/job does not exist/i,
"job delete endpoint rejects a variable row");
$result = run_cgi("save_cron.cgi", { 'idx' => 2 });
like($result->{'out'}, qr/job does not exist/i,
"job save endpoint rejects a variable row");
seed_acl(%full_acl);
$result = run_cgi("delete_env.cgi", { 'idx' => 0 });
like($result->{'out'}, qr/environment variable does not exist/i,
"variable delete endpoint rejects a job row");
$result = run_cgi("save_env.cgi", { 'idx' => 0 });
like($result->{'out'}, qr/environment variable does not exist/i,
"variable save endpoint rejects a job row");
seed_acl(%full_acl, 'create' => 0);
$result = run_cgi("save_env.cgi", { 'new' => 1 });
like($result->{'out'}, qr/not allowed to create cron jobs/i,
"creating a variable requires create permission");
seed_acl(%full_acl, 'delete' => 0);
$result = run_cgi("delete_env.cgi", { 'idx' => 2 });
like($result->{'out'}, qr/not allowed to edit cron jobs/i,
"deleting a variable requires delete permission");
seed_acl(%full_acl, 'env' => 0);
$result = run_cgi("delete_jobs.cgi",
{ 'd' => [ 3, 2 ], 'delete' => 1 });
like($result->{'out'}, qr/not allowed to edit environment variables/i,
"bulk delete rejects a mixed selection without env permission");
is(read_crontab_fixture(), $original,
"denied bulk delete does not partially change the crontab");
$result = run_cgi("delete_jobs.cgi", { 'd' => 0, 'disable' => 1 });
like($result->{'out'}, qr/^Location:/m,
"a normal job can still be disabled without env permission");
like(read_crontab_fixture(), qr/^# \* \* \* \* \* echo first/m,
"disabling a normal job updates only that row");
$result = run_cgi("delete_jobs.cgi", { 'd' => 0, 'enable' => 1 });
like($result->{'out'}, qr/^Location:/m,
"a normal job can still be enabled without env permission");
is(read_crontab_fixture(), $original,
"re-enabling the normal job restores the fixture");
# Job fields must not add lines or fields to the crontab
seed_acl(%full_acl);
my %edit = ( 'idx' => 0, 'user' => 'root', 'active' => 1,
'cmd' => 'echo first', 'range_def' => 1,
'special_def' => 0, 'all_mins' => 1, 'all_hours' => 1,
'all_days' => 1, 'all_months' => 1, 'all_weekdays' => 1 );
foreach my $case (
[ "a line break in the command",
{ 'cmd' => "echo first\nPATH=/tmp" }, qr/cannot contain line breaks/ ],
[ "a line break in the comment",
{ 'comment' => "x\nPATH=/tmp" }, qr/cannot contain line breaks/ ],
[ "a special time with spaces",
{ 'special_def' => 1, 'special' => "reboot echo x #" },
qr/Invalid special time/ ],
[ "a minute with spaces",
{ 'all_mins' => 0, 'mins' => "* * * * * echo x #" },
qr/Invalid Minutes selected/ ],
[ "a minute with a line break",
{ 'all_mins' => 0, 'mins' => "5\nPATH=/tmp" },
qr/Invalid Minutes selected/ ],
[ "a range date with shell text",
{ 'range_def' => 0, 'range_start_day' => '1;echo x;',
'range_start_month' => 1, 'range_start_year' => 2026,
'range_end_day' => 31, 'range_end_month' => 12,
'range_end_year' => 2026 }, qr/invalid start date/i ],
) {
my ($desc, $form, $re) = @$case;
$result = run_cgi("save_cron.cgi", { %edit, %$form });
like($result->{'out'}, $re, "save_cron.cgi rejects $desc");
is(read_crontab_fixture(), $original,
"rejected $desc leaves the crontab unchanged");
}
seed_acl(%full_acl, 'env' => 0);
$result = run_cgi("move.cgi", { 'idx' => 4, 'top' => 1 });
like($result->{'out'}, qr/^Location:/m,
"a job can move across a variable without env permission");
isnt(read_crontab_fixture(), $original,
"the permitted top move changes the crontab");
# Saving a job needs a real Unix user, so check a valid save in a per-user
# crontab, as on Linux. Plain shell commands stand in for the crontab
# command, so the real one never runs.
write_cron_config("cron_copy_command=cat >$crondir/USER",
"cron_delete_command=rm -f $crondir/USER");
$cronfile = "$crondir/root";
write_crontab();
$result = run_cgi("save_cron.cgi",
{ %edit, 'all_mins' => 0, 'mins' => [ 5, 6, 30 ],
'range_def' => 0, 'range_start_day' => 1,
'range_start_month' => 1, 'range_start_year' => 2026,
'range_end_day' => 31, 'range_end_month' => 12,
'range_end_year' => 2026 });
like($result->{'out'}, qr/^Location:/m,
"save_cron.cgi accepts valid times and dates");
like(read_crontab_fixture(),
qr/^5-6,30 \* \* \* \* \S+range\.pl 1-1-2026 31-12-2026 echo\\ first$/m,
"the saved job has the expected times and date range");
done_testing();

View File

@@ -481,11 +481,6 @@ foreach my $a (@{$cmd->{'args'}}) {
&error(&text('run_emust', $a->{'desc'}));
}
$rv =~ /\0/ && &error($text{'run_ezero'});
# Stop non-path parameters from changing the editor's directory
if ($cmd->{'edit'} && $a->{'type'} != 5 && $a->{'type'} != 6 &&
($rv =~ /[\/\\\r\n]/ || $rv =~ /^\.\.?$/)) {
&error($text{'view_ecannot'});
}
$ENV{$n} = $rv;
$env .= "$n=".quotemeta($rv)."\n";
$export .= " $n";
@@ -501,30 +496,6 @@ foreach my $a (@{$cmd->{'args'}}) {
return ($env, $export, $str, $displaystr, \@vals);
}
# resolve_editor_file(template)
# Expands environment variables and keeps the path in its fixed directory.
sub resolve_editor_file
{
local ($file) = @_;
# Leave fixed paths unchanged
return $file if ($file !~ /\$/);
local $template = $file;
# Expand each supported variable once, but not variables inside its value
$file =~ s{\$\{([A-Za-z_][A-Za-z0-9_]*)\}|
\$([A-Za-z_][A-Za-z0-9_]*)}{
my $name = defined($1) ? $1 : $2;
defined($ENV{$name}) ? $ENV{$name} : "";
}gex;
# Resolve the fixed directory and full path before checking their locations
$template =~ s/\$.*$//;
$template =~ s/\/[^\/]*$// if ($template ne "/");
$template = &simplify_path(&resolve_links($template));
$file = &simplify_path(&resolve_links($file));
$template && $file && &is_under_directory($template, $file) ||
&error($text{'view_ecannot'});
return $file;
}
# list_dbi_drivers()
# Returns a list of DBI driver details, which are actually installed
sub list_dbi_drivers

View File

@@ -18,9 +18,10 @@ if ($file !~ /^\//) {
# Set environment variables for parameters
($env, $export, $str, $displayfile) = &set_parameter_envs($edit, $file);
if ($edit->{'envs'} || @{$edit->{'args'}}) {
# Expand variables without invoking a shell
$file = &resolve_editor_file($file);
# Do environment variable substitution
chop($file = `echo "$file"`);
}
# Run the before-command
@@ -36,15 +37,14 @@ $in{'data'} =~ s/\r//g;
&print_tempfile(FILE, $in{'data'});
&close_tempfile(FILE);
# Set file ownership and permissions without invoking a shell
# Set permissions
if ($edit->{'user'}) {
# Apply the configured owner and group
&set_ownership_permissions($edit->{'user'}, $edit->{'group'},
undef, $file);
&system_logged("chown $edit->{'user'}:$edit->{'group'} ".
"$file >/dev/null 2>&1");
}
if ($edit->{'perms'}) {
# Apply the configured file permissions
&set_ownership_permissions(undef, undef, oct($edit->{'perms'}), $file);
&system_logged("chmod $edit->{'perms'} ".
"$file >/dev/null 2>&1");
}
# Run the after-command

View File

@@ -17,12 +17,13 @@ if ($file !~ /^\//) {
# Set environment variables for parameters
($env, $export, $str, $displayfile) = &set_parameter_envs($edit, $file);
if ($edit->{'envs'} || @{$edit->{'args'}}) {
# Expand variables without invoking a shell
$file = &resolve_editor_file($file);
}
$displayfile = &html_escape($displayfile);
if ($edit->{'envs'} || @{$edit->{'args'}}) {
# Do environment variable substitution
chop($file = `echo "$file"`);
}
# Run any before-edit command
if ($edit->{'beforeedit'}) {
$out = &backquote_logged("($edit->{'beforeedit'}) 2>&1 </dev/null");

View File

@@ -211,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;
@@ -246,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);
@@ -303,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;
@@ -323,8 +309,7 @@ while(1) {
"started\n"
if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => [ $arg->{'file'}, $tport,
$arg->{'xferauth'} ? $sid : undef ] };
'rv' => [ $arg->{'file'}, $tport ] };
}
close(FILE);
close($tsock);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'});

View File

@@ -17,32 +17,32 @@ if ($in{'method'} eq 'plain-tar') {
$extension = ".tar";
$full = &validate_filename_path($in{'arch'}.$extension);
$command = "tar cf ".quotemeta($full).
" -C ".quotemeta($cwd)." --";
" -C ".quotemeta($cwd);
}
elsif ($in{'method'} eq 'xz-tar') {
$extension = ".tar.xz";
$full = &validate_filename_path($in{'arch'}.$extension);
$command = "tar cJf ".quotemeta($full).
" -C ".quotemeta($cwd)." --";
" -C ".quotemeta($cwd);
}
elsif ($in{'method'} eq 'zstd-tar') {
$extension = ".zst";
$full = &validate_filename_path($in{'arch'}.$extension);
$command = "ZSTD_CLEVEL=19 tar --zstd -cf ".
quotemeta($full).
" -C ".quotemeta($cwd)." --";
" -C ".quotemeta($cwd);
}
elsif ($in{'method'} eq 'tar') {
$extension = ".tar.gz";
$full = &validate_filename_path($in{'arch'}.$extension);
$command = "tar czf ".quotemeta($full).
" -C ".quotemeta($cwd)." --";
" -C ".quotemeta($cwd);
}
elsif ($in{'method'} eq 'zip') {
$extension = ".zip";
$full = &validate_filename_path($in{'arch'}.$extension);
$command = "cd ".quotemeta($cwd).
" && zip -r ".quotemeta($full)." --";
" && zip -r ".quotemeta($full);
}
else {
&error("Unknown method!");

View File

@@ -31,9 +31,9 @@ elsif ($archive_type =~ /x-7z/ ||
elsif ($archive_type =~ /\/zip/) {
my $unzip_out = `unzip --help`;
my $uu = ($unzip_out =~ /-UU/ ? '-UU' : undef);
$cmd = "unzip $uu -q -o".
" -d ".quotemeta($cwd).
" -- ".quotemeta($full);
$cmd = "unzip $uu -q -o ".
quotemeta($full).
" -d ".quotemeta($cwd);
}
elsif ($archive_type =~ /\/x-rar|\/vnd\.rar/) {
$cmd = "unrar x -r -y ".

View File

@@ -11,7 +11,7 @@ eval "use File::MimeInfo";
our ($path, $quote_escaped_path, $urlized_path, $base, $cwd, $upload_max,
$confdir, $template_name, $bookmarks, $page, $pagelimit, $edit_icon,
$rename_icon, $extract_icon, $goto_icon, $root_icon);
$rename_icon, $extract_icon, $goto_icon, $root_icon, $head);
our (%access, %userconfig, %allowed_for_edit);
our (@allowed_paths, @allowed_for_edit, @remote_user_info);
@@ -413,9 +413,124 @@ if ($tinfo{'bootstrap'}) {
"unauthenticated/templates/dialogs.html");
}
# Interface for other themes, built from the core widgets
# Interface for legacy themes
else {
print &plain_interface_head();
# Set icons variables
$edit_icon =
"<img src='images/icons/quick/edit.png'" .
" alt='$text{'edit'}' />";
$rename_icon =
"<img src='images/icons/quick/" .
"rename.png' " .
"alt='$text{'rename'}' />";
$extract_icon =
"<img src='images/icons/quick/" .
"extract.png' " .
"alt='$text{'extract_archive'}' />";
$goto_icon =
"<img src='images/icons/quick/" .
"go-next.png' " .
"alt='$text{'goto_folder'}'";
# Add static files
$head = "<link rel=\"stylesheet\" " .
"type=\"text/css\" " .
"href=\"unauthenticated/css/" .
"style.css\" />";
$head .= "<script type=\"text/javascript\"" .
" src=\"unauthenticated/jquery/" .
"jquery.min.js\"></script>";
$head .= "<script type=\"text/javascript\"" .
" src=\"unauthenticated/jquery/" .
"jquery-ui.min.js\"></script>";
$head .= "<script type=\"text/javascript\"" .
" src=\"unauthenticated/js/" .
"legacy.js\"></script>";
$head .= "<link rel=\"stylesheet\" " .
"type=\"text/css\" " .
"href=\"unauthenticated/jquery/" .
"jquery-ui.min.css\" />";
$head .= "<script type=\"text/javascript\"" .
" src=\"unauthenticated/js/" .
"chmod-calculator.js\"></script>";
$head .= "<link rel=\"stylesheet\" " .
"type=\"text/css\" " .
"href=\"unauthenticated/dropdown/" .
"fg.menu.css\" />";
$head .= "<script type=\"text/javascript\"" .
" src=\"unauthenticated/dropdown/" .
"fg.menu.js\"></script>";
print $head;
# Set "root" icon
if ($base eq '/') {
$root_icon =
"<img src=\"images/icons/quick/" .
"drive-harddisk.png\" " .
"class=\"hdd-icon\" />";
}
else {
$root_icon = "~";
}
# Legacy breadcrumbs
print "<div id='bread' style='float: left;" .
" padding-bottom: 2px;'>" .
"<a href='index.cgi?path='>" .
"$root_icon</a> / ";
my @breadcr = split('/', $path);
my $cp = '';
for (my $i = 1;
$i <= scalar(@breadcr)-1; $i++) {
chomp($breadcr[$i]);
$cp = $cp.'/'.$breadcr[$i];
print "<a href='index.cgi?path=" .
&urlize($cp) . "'>" .
&html_escape($breadcr[$i]) .
"</a> / ";
}
print "<br />";
# And pagination
$page = $in{'page'};
$pagelimit = $userconfig{'per_page'};
$pages = ceil((scalar(@list))/$pagelimit);
if (not defined $page or $page > $pages) {
$page = 1;
}
print "Pages: ";
for (my $i = 1; $i <= $pages; $i++) {
if ($page eq $i) {
print "<a class='pages active' " .
"href='?path=" .
&urlize($path) .
"&page=$i" .
"&query=" .
&urlize($query) .
"'>" .
&html_escape($i) .
"</a>";
}
else {
print "<a class='pages' " .
"href='?path=" .
&urlize($path) .
"&page=$i" .
"&query=" .
&urlize($query) .
"'>" .
&html_escape($i) .
"</a>";
}
}
print "</div>";
# And toolbar
print_template(
"unauthenticated/templates/" .
"legacy_quicks.html");
print_template(
"unauthenticated/templates/" .
"legacy_dialogs.html");
}
my $info_total;
my $info_files = scalar @files;
@@ -686,18 +801,6 @@ for (my $count = 1 + $pagelimit * ($page - 1);
print ui_columns_end();
print &ui_hidden("path", $path),"\n";
print &ui_form_end();
# Close the widget page area opened by the non-Bootstrap interface
print &ui_page_end() if (!$tinfo{'bootstrap'});
}
# list_bookmarks()
# Returns the directories the user has bookmarked
sub list_bookmarks
{
$confdir = "$remote_user_info[7]/.filemin";
return () if (!-e "$confdir/.bookmarks");
my $bookmarks = &read_file_lines($confdir.'/.bookmarks', 1);
return grep { $_ ne '' } @$bookmarks;
}
# get_bookmarks()
@@ -708,8 +811,10 @@ $confdir = "$remote_user_info[7]/.filemin";
if (!-e "$confdir/.bookmarks") {
return "<li><a>$text{'no_bookmarks'}</a></li>";
}
my $bookmarks =
&read_file_lines($confdir.'/.bookmarks', 1);
$result = '';
foreach $bookmark (&list_bookmarks()) {
foreach $bookmark (@$bookmarks) {
$result .=
"<li><a href='index.cgi?path=" .
&urlize($bookmark) . "'>" .
@@ -719,486 +824,6 @@ foreach $bookmark (&list_bookmarks()) {
return $result;
}
# plain_interface_head()
# Returns the top of the file list for themes without Bootstrap : the
# module assets, the breadcrumbs, the toolbar of menus and buttons, the
# page links and the dialogs behind the toolbar, all built from the core
# widgets. Also sets the row action icons and the pagination that the
# shared listing code uses, and opens the widget page area that
# print_interface closes after the listing.
sub plain_interface_head
{
my $rv = "";
# Row action icons from the built-in icon set
$edit_icon = &ui_svg_icon('edit', { 'title' => $text{'edit'} });
$rename_icon = &ui_svg_icon('cursor-text', { 'title' => $text{'rename'} });
$extract_icon = &ui_svg_icon('unarchive',
{ 'title' => $text{'extract_archive'} });
$goto_icon = &ui_svg_icon('arrow-right', { 'title' => $text{'goto_folder'} });
# Server-side pagination, with as many entries per page as the user chose
$page = int($in{'page'} || 0) || 1;
$pagelimit = int($userconfig{'per_page'} || 0) || 50;
$pages = ceil(scalar(@list) / $pagelimit) || 1;
$page = 1 if ($page > $pages);
# The widget page area, with the module's own stylesheet and script
$rv .= &ui_page_start({ 'class' => 'filemin_page' });
$rv .= &plain_asset_tags();
# Breadcrumbs at the left and the toolbar at the right
$rv .= &ui_tag('div', &plain_breadcrumb().&plain_toolbar(),
{ 'class' => 'filemin_head' });
# Page links, when the listing does not fit on one page. The URL is
# relative, so a search keeps its query from page to page
my $pager = &ui_pager({
'page' => $page,
'pages' => $pages,
'href' => "?path=".&urlize($path).
(defined($query) && $query ne '' ?
"&query=".&urlize($query) : "").
($in{'caseins'} ? "&caseins=1" : "").
"&page=" });
$rv .= &ui_tag('div', $pager, { 'class' => 'filemin_pager' }) if ($pager);
# The dialogs opened from the toolbar and the rows
$rv .= &plain_dialogs();
return $rv;
}
# plain_asset_tags()
# Returns the tags loading the module's stylesheet and script for the
# widget interface, each keyed by its modification time so that a changed
# file is fetched at once, and the permissions calculator shared with the
# Bootstrap interface
sub plain_asset_tags
{
my $rv = "";
foreach my $file ('css/plain.css', 'js/plain.js') {
my @st = stat("$module_root_directory/unauthenticated/$file");
my $url = "unauthenticated/$file?".(@st ? $st[9] : &get_webmin_version());
if ($file =~ /\.css$/) {
$rv .= &ui_tag('link', undef,
{ 'rel' => 'stylesheet', 'href' => $url })."\n";
}
else {
$rv .= &ui_tag('script', undef,
{ 'src' => $url, 'defer' => undef })."\n";
}
}
$rv .= &ui_tag('script', undef,
{ 'src' => 'unauthenticated/js/chmod-calculator.js' })."\n";
return $rv;
}
# plain_breadcrumb()
# Returns the path to the current directory as breadcrumb links, starting
# with the allowed base directory : the disk when it is the root, else
# the home
sub plain_breadcrumb
{
my @items = ( { 'icon' => $base eq '/' ? 'hard-drive' : 'home',
'href' => 'index.cgi?path=',
'title' => $base } );
my $cp = '';
foreach my $part (grep { $_ ne '' } split(/\//, $path)) {
$cp .= '/'.$part;
push(@items, { 'label' => $part,
'href' => 'index.cgi?path='.&urlize($cp) });
}
return &ui_breadcrumb(\@items, { 'class' => 'filemin_crumbs' });
}
# plain_toolbar()
# Returns the toolbar : the File, Edit, Tools and Bookmarks menus, and the
# refresh and delete buttons. Entries acting on the selected entries are
# marked, so that plain.js asks for a selection first. Tools whose
# command is missing are left out.
sub plain_toolbar
{
my $urlpath = &urlize($path);
my $sel = { 'data-filemin-selection' => '1' };
my @file = (
{ 'label' => $text{'create_file'}, 'icon' => 'file-plus',
'dialog' => 'filemin_create_file' },
{ 'label' => $text{'create_folder'}, 'icon' => 'folder-plus',
'dialog' => 'filemin_create_folder' },
'-',
{ 'label' => $text{'upload_files'}, 'icon' => 'upload',
'dialog' => 'filemin_upload' },
{ 'label' => $text{'get_from_url'}, 'icon' => 'globe',
'dialog' => 'filemin_download' },
);
my @edit = (
{ 'label' => $text{'copy_selected'}, 'icon' => 'copy',
'attrs' => { %$sel, 'data-filemin-submit' => 'copy.cgi' } },
{ 'label' => $text{'cut_selected'}, 'icon' => 'scissors',
'attrs' => { %$sel, 'data-filemin-submit' => 'cut.cgi' } },
{ 'label' => $text{'paste'}, 'icon' => 'clipboard',
'href' => 'paste.cgi?path='.$urlpath },
'-',
{ 'label' => $text{'select_all'}, 'icon' => 'check-square',
'attrs' => { 'data-filemin-select' => 'all' } },
{ 'label' => $text{'invert_selection'}, 'icon' => 'square-half',
'attrs' => { 'data-filemin-select' => 'invert' } },
);
my @tools = (
{ 'label' => $text{'search'}, 'icon' => 'search',
'dialog' => 'filemin_search' },
'-',
{ 'label' => $text{'compress_selected'}, 'icon' => 'archive',
'dialog' => 'filemin_compress', 'attrs' => $sel },
{ 'label' => $text{'chmod_selected'}, 'icon' => 'lock',
'dialog' => 'filemin_chmod', 'attrs' => $sel },
{ 'label' => $text{'chown_selected'}, 'icon' => 'user',
'dialog' => 'filemin_chown', 'attrs' => $sel },
);
push(@tools, { 'label' => $text{'chattr_selected'}, 'icon' => 'tag',
'dialog' => 'filemin_chattr', 'attrs' => $sel })
if (&has_command('chattr'));
push(@tools, { 'label' => $text{'chcon_selected'}, 'icon' => 'shield',
'dialog' => 'filemin_chcon', 'attrs' => $sel })
if (&get_selinux_status());
push(@tools, { 'label' => $text{'acls_selected'}, 'icon' => 'key',
'dialog' => 'filemin_acls', 'attrs' => $sel })
if (&has_command('setfacl'));
my @bookmarks = (
{ 'label' => $text{'bookmark_folder'}, 'icon' => 'bookmark',
'href' => 'bookmark.cgi?path='.$urlpath },
'-',
);
my @marks = &list_bookmarks();
if (@marks) {
push(@bookmarks, map { { 'label' => $_, 'icon' => 'folder',
'href' => 'index.cgi?path='.&urlize($_) } }
@marks);
}
else {
push(@bookmarks, { 'label' => $text{'no_bookmarks'}, 'disabled' => 1 });
}
my @buttons = (
&ui_dropdown($text{'menu_file'}, \@file, { 'icon' => 'file' }),
&ui_dropdown($text{'menu_edit'}, \@edit, { 'icon' => 'clipboard' }),
&ui_dropdown($text{'menu_tools'}, \@tools, { 'icon' => 'gear' }),
&ui_dropdown($text{'menu_bookmarks'}, \@bookmarks,
{ 'icon' => 'book', 'align' => 'end' }),
&ui_action_button($text{'refresh'}, 'neutral',
{ 'icon' => 'refresh', 'href' => 'index.cgi?path='.$urlpath }),
&ui_dialog_button($text{'delete'}, 'filemin_delete', 'danger',
{ 'icon' => 'trash', 'attrs' => $sel }),
);
return &ui_tag('div', join("\n", @buttons),
{ 'class' => 'filemin_toolbar', 'role' => 'toolbar' });
}
# plain_dialog_table(&rows)
# Returns a two-column table of label and input rows for a dialog body.
# A row with an empty label spans the table.
sub plain_dialog_table
{
my ($rows) = @_;
my $rv = &ui_table_start(undef, "width=100%", 2);
foreach my $row (@$rows) {
$rv .= defined($row->[0]) && $row->[0] ne '' ?
&ui_table_row($row->[0], $row->[1]) :
&ui_table_row(undef, $row->[1], 2);
}
$rv .= &ui_table_end();
return $rv;
}
# plain_dialogs()
# Returns the dialogs behind the toolbar and the row actions. A dialog
# acting on the selected entries carries the script to run in a
# data-filemin-apply attribute : plain.js sends the selection form to that
# script with the dialog's fields as URL parameters, as the action
# scripts expect them. The other dialogs are ordinary forms posting to
# their script.
sub plain_dialogs
{
my $rv = "";
my $hidden_path = &ui_hidden('path', $path);
my $cancel = $text{'dialog_cancel'};
my $recursive = &ui_checkbox('recursive', 'true', $text{'dialog_recursive'}, 0);
my $apply = sub {
# A form sent to the selection, closed natively if the script fails
my ($script, $name) = @_;
return { 'method' => 'dialog',
'name' => $name,
'attrs' => { 'data-filemin-apply' => $script } };
};
my $change = [ { 'label' => $text{'dialog_change'}, 'state' => 'success',
'icon' => 'check' } ];
# Shown when an action needs a selection and nothing is checked
$rv .= &ui_dialog({
'id' => 'filemin_nothing',
'title' => $text{'warning_title'},
'state' => 'warning',
'size' => 'small',
'text' => $text{'nothing_selected'},
'close' => $text{'dialog_ok'} });
# New file and directory
foreach my $kind ([ 'file', 'create_file', 'new_file_name', 'file-plus' ],
[ 'folder', 'create_folder', 'new_folder_name', 'folder-plus' ]) {
my ($what, $title, $label, $icon) = @$kind;
$rv .= &ui_dialog({
'id' => 'filemin_create_'.$what,
'title' => $text{$title},
'icon' => $icon,
'size' => 'small',
'form' => 'create_'.$what.'.cgi',
'body' => &plain_dialog_table([
[ $text{$label},
&ui_textbox('name', '', 30, 0, undef, 'required') ],
]).$hidden_path,
'buttons' => [ { 'label' => $text{'create'}, 'state' => 'success',
'icon' => 'check' } ],
'close' => $cancel });
}
# Rename, filled in by the row link
$rv .= &ui_dialog({
'id' => 'filemin_rename',
'title' => $text{'rename'},
'icon' => 'cursor-text',
'size' => 'small',
'form' => 'rename.cgi',
'body' => &plain_dialog_table([
[ $text{'new_name'},
&ui_textbox('name', '', 30, 0, undef, 'required') ],
]).&ui_hidden('file', '').$hidden_path,
'buttons' => [ { 'label' => $text{'rename'}, 'state' => 'success',
'icon' => 'check' } ],
'close' => $cancel });
# Upload. The options travel in the URL, which upload.cgi reads before
# the files arrive, so plain.js adds them to the action on submit
my $upload_url = "upload.cgi?path=".&urlize($path)."&id=".$upid;
$rv .= &ui_dialog({
'id' => 'filemin_upload',
'title' => $text{'upload_files'},
'icon' => 'upload',
'form' => { 'action' => $upload_url,
'method' => 'form-data',
'attrs' => {
'data-filemin-upload' => $upload_url,
'data-filemin-tracker' =>
&get_webprefix()."/uptracker.cgi?id=".$upid } },
'body' => &plain_dialog_table([
[ $text{'upload_files'},
&ui_upload('upfiles', 40, 0, 'multiple required') ],
[ undef,
&ui_checkbox('opt_dirs', 1, $text{'upload_dirs'}, 0,
'data-filemin-upload-dirs')."<br>".
&ui_checkbox('opt_extract', 1, $text{'extract_uploaded'}, 0,
"data-filemin-upload-option='extract_uploaded'").
"<br>".
&ui_checkbox('opt_overwrite', 1, $text{'overwrite_existing'}, 0,
"data-filemin-upload-option='overwrite_existing'") ],
]),
'buttons' => [ { 'label' => $text{'upload_files'}, 'state' => 'success',
'icon' => 'upload' } ],
'close' => $cancel });
# Download from a URL
$rv .= &ui_dialog({
'id' => 'filemin_download',
'title' => $text{'get_from_url'},
'icon' => 'globe',
'form' => 'http_download.cgi',
'body' => &plain_dialog_table([
[ $text{'dialog_file_url'},
&ui_tag('input', undef, { 'type' => 'url', 'name' => 'link',
'class' => 'ui_textbox', 'size' => 40,
'autocomplete' => 'url',
'required' => undef }) ],
[ $text{'dialog_user_name'},
&ui_textbox('username', '', 25, 0, undef, "autocomplete='off'") ],
[ $text{'dialog_user_pass'},
&ui_password('password', '', 25, 0, undef,
"autocomplete='new-password' data-password-meter-skip") ],
]).$hidden_path,
'buttons' => [ { 'label' => $text{'dialog_download'}, 'state' => 'success',
'icon' => 'download' } ],
'close' => $cancel });
# Search under the current directory
$rv .= &ui_dialog({
'id' => 'filemin_search',
'title' => $text{'search'},
'icon' => 'search',
'size' => 'small',
'form' => 'search.cgi',
'body' => &plain_dialog_table([
[ $text{'search_label'},
&ui_textbox('query', '', 30, 0, undef, 'required') ],
[ undef, &ui_checkbox('caseins', 1, $text{'search_insensitive'}, 1) ],
]).$hidden_path,
'buttons' => [ { 'label' => $text{'search_go'}, 'state' => 'success',
'icon' => 'search' } ],
'close' => $cancel });
# Compress the selection into an archive
$rv .= &ui_dialog({
'id' => 'filemin_compress',
'title' => $text{'compress_selected'},
'icon' => 'archive',
'size' => 'small',
'form' => &$apply('compress.cgi'),
'body' => &plain_dialog_table([
[ $text{'archive_name'},
&ui_textbox('arch', '', 30, 0, undef, 'required') ],
[ $text{'type'},
&ui_select('method', 'tar', [ [ 'plain-tar', '.tar' ],
[ 'xz-tar', '.tar.xz' ],
[ 'tar', '.tar.gz' ],
[ 'zstd-tar', '.zstd' ],
[ 'zip', '.zip' ] ]) ],
]),
'buttons' => [ { 'label' => $text{'dialog_compress'}, 'state' => 'success',
'icon' => 'archive' } ],
'close' => $cancel });
# Permissions. The form is named for the calculator, whose fields are
# kept out of the parameters; the perms parameter the script reads is a
# hidden field that plain.js keeps equal to the octal field
my $matrix = &ui_tag_start('table', { 'class' => 'filemin_chmod_matrix' }).
&ui_tag('tr', &ui_tag('th', '').
join('', map { &ui_tag('th', &html_escape($text{$_})) }
('chmod_owner', 'chmod_group', 'chmod_others')));
foreach my $bit ([ 'chmod_read', 4 ], [ 'chmod_write', 2 ],
[ 'chmod_execute', 1 ]) {
$matrix .= &ui_tag('tr', &ui_tag('td', &html_escape($text{$bit->[0]})).
join('', map { &ui_tag('td', &ui_checkbox($_.$bit->[1], $bit->[1],
undef, 0, 'data-filemin-chmod data-filemin-skip')) }
('owner', 'group', 'other')));
}
foreach my $extra ([ 'chmod_sticky', 'sticky', 1 ], [ 'chmod_setgid', 'setgid', 2 ]) {
$matrix .= &ui_tag('tr', &ui_tag('td', &html_escape($text{$extra->[0]})).
&ui_tag('td', &ui_checkbox($extra->[1], $extra->[2], undef, 0,
'data-filemin-chmod data-filemin-skip')).
&ui_tag('td', '').&ui_tag('td', ''));
}
$matrix .= &ui_tag_end('table');
$rv .= &ui_dialog({
'id' => 'filemin_chmod',
'title' => $text{'chmod_selected'},
'icon' => 'lock',
'form' => &$apply('chmod.cgi', 'chmod'),
'body' => &plain_dialog_table([
[ $text{'permissions'},
&ui_textbox('t_total', '0644', 4, 0, 4,
"data-filemin-chmod-octal data-filemin-skip ".
"pattern='[0-7]{3,4}' required")." ".
&ui_textbox('sym_total', '', 12, 0, undef,
'readonly data-filemin-skip').
&ui_hidden('perms', '0644') ],
[ undef, $matrix ],
[ $text{'chmod_apply_to'},
&ui_select('applyto', 1,
[ map { [ $_->[0], $text{$_->[1]} ] }
( [ 1, 'chmod_dir_only' ],
[ 2, 'chmod_dir_and_its_files' ],
[ 3, 'chmod_all' ],
[ 4, 'chmod_dir_files_and_subs_files' ],
[ 5, 'chmod_dir_and_subs' ] ) ]) ],
]),
'buttons' => $change,
'close' => $cancel });
# Ownership
$rv .= &ui_dialog({
'id' => 'filemin_chown',
'title' => $text{'chown_selected'},
'icon' => 'user',
'size' => 'small',
'form' => &$apply('chown.cgi'),
'body' => &plain_dialog_table([
[ $text{'user_name'},
&ui_textbox('owner', '', 20, 0, undef, 'required') ],
[ $text{'group_name'},
&ui_textbox('group', '', 20, 0, undef, 'required') ],
[ undef, $recursive ],
]),
'buttons' => $change,
'close' => $cancel });
# File attributes and SELinux context, which take one label each
foreach my $tool ([ 'chattr', 'tag', 'attr_label' ],
[ 'chcon', 'shield', 'context_label' ]) {
my ($what, $icon, $label) = @$tool;
$rv .= &ui_dialog({
'id' => 'filemin_'.$what,
'title' => $text{$what.'_selected'},
'icon' => $icon,
'size' => 'small',
'form' => &$apply($what.'.cgi'),
'body' => &plain_dialog_table([
[ $text{$label},
&ui_textbox('label', '', 25, 0, undef, 'required') ],
[ undef, $recursive ],
]),
'buttons' => $change,
'close' => $cancel });
}
# Access control lists, when setfacl exists
if (&has_command('setfacl')) {
$rv .= &ui_dialog({
'id' => 'filemin_acls',
'title' => $text{'acls_selected'},
'icon' => 'key',
'form' => &$apply('setfacl.cgi'),
'body' => &plain_dialog_table([
[ $text{'acls_action'},
&ui_select('action', '-m',
[ [ '', '' ], [ '-m', 'Modify (-m)' ],
[ '-x', 'Remove (-x)' ],
[ '-b', 'Remove all extended (-b)' ],
[ '-k', 'Remove default (-k)' ] ]) ],
[ $text{'acls_applyto'},
&ui_select('apply_to', [ ],
[ map { [ $_->[0], $text{$_->[1]} ] }
( [ 'u', 'acls_user' ], [ 'g', 'acls_group' ],
[ 'm', 'acls_mask' ], [ 'o', 'acls_other' ] ) ],
4, 1) ],
[ $text{'acls_perms'},
&ui_select('perms', '',
[ map { [ $_, $_ ] }
( '', '---', 'r--', '-w-', '--x',
'r-x', 'rw-', '-wx', 'rwx' ) ]) ],
[ $text{'acls_user'}, $acl_user_select ],
[ $text{'acls_group'}, $acl_group_select ],
[ undef, &ui_checkbox('recursive', 1, $text{'dialog_recursive'}, 0) ],
[ undef, $acl_manual ],
]),
'buttons' => $change,
'close' => $cancel });
}
# Delete, after listing what is selected. Cancel takes the focus, so
# Enter does not delete by accident
$rv .= &ui_dialog({
'id' => 'filemin_delete',
'title' => $text{'remove_selected'},
'state' => 'danger',
'icon' => 'trash',
'form' => &$apply('delete.cgi'),
'body' => &ui_tag('p', &html_escape($text{'about_to_delete'})).
&ui_tag('pre', '', { 'id' => 'filemin_delete_items',
'class' => 'filemin_delete_items' }).
&ui_tag('p', &html_escape($text{'are_you_sure'})),
'buttons' => [ { 'label' => $text{'delete'}, 'state' => 'danger',
'icon' => 'trash' } ],
'close' => $cancel,
'focus' => 'close' });
return $rv;
}
# get_paste_buffer_file()
# Returns the location of the file for temporary copy/paste state
sub get_paste_buffer_file
@@ -1485,9 +1110,10 @@ foreach my $fref (@{$files_to_extract}) {
my $uu = ($unzip_out =~ /-UU/
? '-UU' : undef);
$status = system(
"$unzip_cmd $uu -q -o".
" -d " .quotemeta($extract_to).
" -- ".quotemeta($archive));
"$unzip_cmd $uu -q -o " .
quotemeta($archive) .
" -d " .
quotemeta($extract_to));
}
}
elsif ($archive_type =~ /\/x-rar|\/vnd\.rar/) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

View File

@@ -13,6 +13,7 @@ $query = $in{'query'};
$text{'search_results'} . " '" .
&html_escape($query) . "'", "");
print $head;
if ($in{'caseins'}) {
$criteria = '-iname';
}

View File

@@ -1,85 +0,0 @@
/*
* plain.css
* Layout of the File Manager for themes without Bootstrap. The toolbar,
* its menus, the dialogs and the page links are core widgets styled by
* ui-lib.css; this file only arranges them around the listing and
* styles the row action links and the permissions matrix.
*/
/* Breadcrumbs at the left, toolbar at the right, wrapping on a narrow
* frame */
.filemin_head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 16px;
}
.filemin_head .ui_breadcrumb {
min-width: 0;
font-size: 1.05em;
}
.filemin_toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-left: auto;
}
.filemin_pager { display: flex; }
.filemin_page .total { color: var(--ui-fg-muted); }
/* Row action links hold icons and sit side by side */
.filemin_page .action-link {
display: inline-flex;
align-items: center;
margin-right: 4px;
color: var(--ui-fg-muted);
vertical-align: middle;
}
.filemin_page .action-link:hover { color: var(--ui-accent); }
.filemin_page td > a > img { vertical-align: middle; }
/* Dialog forms sit flush inside the dialog body, and stay within it :
* both the theme's wrapper table and the form table take the dialog
* width with fixed columns, the label column has a set width, and the
* inputs fit the value column instead of widening the dialog */
.filemin_page .ui_dialog_body > table.wrapper { margin: 0; }
.filemin_page .ui_dialog_body table.wrapper,
.filemin_page .ui_dialog_body table.ui_table {
width: 100%;
table-layout: fixed;
}
.filemin_page .ui_dialog_body td.ui_form_label {
width: 8.5em;
white-space: nowrap;
}
.filemin_page .ui_dialog_body td.ui_form_value select { width: 100%; }
.filemin_page .ui_dialog_body td.ui_form_value
input:not([type="checkbox"]):not([type="radio"]) {
max-width: 100%;
}
/* The permissions matrix */
.filemin_chmod_matrix { border-collapse: collapse; }
.filemin_chmod_matrix th, .filemin_chmod_matrix td {
padding: 2px 12px 2px 0;
text-align: left;
font-weight: normal;
}
.filemin_chmod_matrix th { font-weight: 600; }
.filemin_chmod_matrix input[type="checkbox"] { margin: 0; }
/* The entries about to be deleted */
.filemin_delete_items {
max-height: 180px;
margin: 8px 0;
padding: 8px 10px;
overflow: auto;
border: 1px solid var(--ui-border-soft);
background: var(--ui-surface-2);
font-family: var(--ui-font-mono);
font-size: 0.92em;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

View File

@@ -0,0 +1,114 @@
/* Styles for jQuery menu widget
Author: Maggie Wachs, maggie@filamentgroup.com
Date: September 2008
*/
/* REQUIRED STYLES - the menus will only render correctly with these rules */
.fg-menu-container { position: absolute; top:0; left:-999px; padding: .4em; overflow: hidden; }
.fg-menu-container.fg-menu-flyout { overflow: visible; }
.fg-menu, .fg-menu ul { list-style-type:none; padding: 0; margin:0; }
.fg-menu { position:relative; }
.fg-menu-flyout .fg-menu { position:static; }
.fg-menu ul { position:absolute; top:0; }
.fg-menu ul ul { top:-1px; }
.fg-menu-container.fg-menu-ipod .fg-menu-content,
.fg-menu-container.fg-menu-ipod .fg-menu-content ul { background: none !important; }
.fg-menu.fg-menu-scroll,
.fg-menu ul.fg-menu-scroll { overflow: scroll; overflow-x: hidden; }
.fg-menu li { clear:both; float:left; width:100%; margin: 0; padding:0; border: 0; }
.fg-menu li li { font-size:1em; } /* inner li font size must be reset so that they don't blow up */
.fg-menu-flyout ul ul { padding: .4em; }
.fg-menu-flyout li { position:relative; }
.fg-menu-scroll { overflow: scroll; overflow-x: hidden; }
.fg-menu-breadcrumb { margin: 0; padding: 0; }
.fg-menu-footer { margin-top: .4em; padding: .4em; }
.fg-menu-header { margin-bottom: .4em; padding: .4em; }
.fg-menu-breadcrumb li { float: left; list-style: none; margin: 0; padding: 0 .2em; font-size: .9em; opacity: .7; }
.fg-menu-breadcrumb li.fg-menu-prev-list,
.fg-menu-breadcrumb li.fg-menu-current-crumb { clear: left; float: none; opacity: 1; }
.fg-menu-breadcrumb li.fg-menu-current-crumb { padding-top: .2em; }
.fg-menu-breadcrumb a,
.fg-menu-breadcrumb span { float: left; }
.fg-menu-footer a:link,
.fg-menu-footer a:visited { float:left; width:100%; text-decoration: none; }
.fg-menu-footer a:hover,
.fg-menu-footer a:active { }
.fg-menu-footer a span { float:left; cursor: pointer; }
.fg-menu-breadcrumb .fg-menu-prev-list a:link,
.fg-menu-breadcrumb .fg-menu-prev-list a:visited,
.fg-menu-breadcrumb .fg-menu-prev-list a:hover,
.fg-menu-breadcrumb .fg-menu-prev-list a:active { background-image: none; text-decoration:none; }
.fg-menu-breadcrumb .fg-menu-prev-list a { float: left; padding-right: .4em; }
.fg-menu-breadcrumb .fg-menu-prev-list a .ui-icon { float: left; }
.fg-menu-breadcrumb .fg-menu-current-crumb a:link,
.fg-menu-breadcrumb .fg-menu-current-crumb a:visited,
.fg-menu-breadcrumb .fg-menu-current-crumb a:hover,
.fg-menu-breadcrumb .fg-menu-current-crumb a:active { display:block; background-image:none; font-size:1.3em; text-decoration:none; }
/* REQUIRED LINK STYLES: links are "display:block" by default; if the menu options are split into
selectable node links and 'next' links, the script floats the node links left and floats the 'next' links to the right */
.fg-menu a:link,
.fg-menu a:visited,
.fg-menu a:hover,
.fg-menu a:active { float:left; width:92%; padding:.3em 3%; text-decoration:none; outline: 0 !important; }
.fg-menu a { border: 1px dashed transparent; }
.fg-menu a.ui-state-default:link,
.fg-menu a.ui-state-default:visited,
.fg-menu a.ui-state-default:hover,
.fg-menu a.ui-state-default:active,
.fg-menu a.ui-state-hover:link,
.fg-menu a.ui-state-hover:visited,
.fg-menu a.ui-state-hover:hover,
.fg-menu a.ui-state-hover:active,
.fg-menu a.ui-state-active:link,
.fg-menu a.ui-state-active:visited,
.fg-menu a.ui-state-active:hover,
.fg-menu a.ui-state-active:active { border-style: solid; font-weight: normal; }
.fg-menu a span { display:block; cursor:pointer; }
/* SUGGESTED STYLES - for use with jQuery UI Themeroller CSS */
.fg-menu-indicator span { float:left; }
.fg-menu-indicator span.ui-icon { float:right; }
.fg-menu-content.ui-widget-content,
.fg-menu-content ul.ui-widget-content { border:0; }
/* ICONS AND DIVIDERS */
.fg-menu.fg-menu-has-icons a:link,
.fg-menu.fg-menu-has-icons a:visited,
.fg-menu.fg-menu-has-icons a:hover,
.fg-menu.fg-menu-has-icons a:active { padding-left:20px; }
.fg-menu .horizontal-divider hr, .fg-menu .horizontal-divider span { padding:0; margin:5px .6em; }
.fg-menu .horizontal-divider hr { border:0; height:1px; }
.fg-menu .horizontal-divider span { font-size:.9em; text-transform: uppercase; padding-left:.2em; }

View File

@@ -0,0 +1,648 @@
/*--------------------------------------------------------------------
Scripts for creating and manipulating custom menus based on standard <ul> markup
Version: 3.0, 03.31.2009
By: Maggie Costello Wachs (maggie@filamentgroup.com) and Scott Jehl (scott@filamentgroup.com)
http://www.filamentgroup.com
* reference articles: http://www.filamentgroup.com/lab/jquery_ipod_style_drilldown_menu/
Copyright (c) 2009 Filament Group
Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
--------------------------------------------------------------------*/
var allUIMenus = [];
$.fn.menu = function(options){
var caller = this;
var options = options;
var m = new Menu(caller, options);
allUIMenus.push(m);
$(this)
.mousedown(function(){
if (!m.menuOpen) { m.showLoading(); };
})
.click(function(){
if (m.menuOpen == false) { m.showMenu(); }
else { m.kill(); };
return false;
});
};
function Menu(caller, options){
var menu = this;
var caller = $(caller);
var container = $('<div class="fg-menu-container ui-widget ui-widget-content ui-corner-all">'+options.content+'</div>');
this.menuOpen = false;
this.menuExists = false;
var options = jQuery.extend({
content: null,
width: 180, // width of menu container, must be set or passed in to calculate widths of child menus
maxHeight: 180, // max height of menu (if a drilldown: height does not include breadcrumb)
positionOpts: {
posX: 'left',
posY: 'bottom',
offsetX: 0,
offsetY: 0,
directionH: 'right',
directionV: 'down',
detectH: true, // do horizontal collision detection
detectV: true, // do vertical collision detection
linkToFront: false
},
showSpeed: 200, // show/hide speed in milliseconds
callerOnState: 'ui-state-active', // class to change the appearance of the link/button when the menu is showing
loadingState: 'ui-state-loading', // class added to the link/button while the menu is created
linkHover: 'ui-state-hover', // class for menu option hover state
linkHoverSecondary: 'li-hover', // alternate class, may be used for multi-level menus
// ----- multi-level menu defaults -----
crossSpeed: 200, // cross-fade speed for multi-level menus
crumbDefaultText: 'Choose an option:',
backLink: true, // in the ipod-style menu: instead of breadcrumbs, show only a 'back' link
backLinkText: 'Back',
flyOut: false, // multi-level menus are ipod-style by default; this parameter overrides to make a flyout instead
flyOutOnState: 'ui-state-default',
nextMenuLink: 'ui-icon-triangle-1-e', // class to style the link (specifically, a span within the link) used in the multi-level menu to show the next level
topLinkText: 'All',
nextCrumbLink: 'ui-icon-carat-1-e'
}, options);
var killAllMenus = function(){
$.each(allUIMenus, function(i){
if (allUIMenus[i].menuOpen) { allUIMenus[i].kill(); };
});
};
this.kill = function(){
caller
.removeClass(options.loadingState)
.removeClass('fg-menu-open')
.removeClass(options.callerOnState);
container.find('li').removeClass(options.linkHoverSecondary).find('a').removeClass(options.linkHover);
if (options.flyOutOnState) { container.find('li a').removeClass(options.flyOutOnState); };
if (options.callerOnState) { caller.removeClass(options.callerOnState); };
if (container.is('.fg-menu-ipod')) { menu.resetDrilldownMenu(); };
if (container.is('.fg-menu-flyout')) { menu.resetFlyoutMenu(); };
container.parent().hide();
menu.menuOpen = false;
$(document).unbind('click', killAllMenus);
$(document).unbind('keydown');
};
this.showLoading = function(){
caller.addClass(options.loadingState);
};
this.showMenu = function(){
killAllMenus();
if (!menu.menuExists) { menu.create() };
caller
.addClass('fg-menu-open')
.addClass(options.callerOnState);
container.parent().show().click(function(){ menu.kill(); return false; });
container.hide().slideDown(options.showSpeed).find('.fg-menu:eq(0)');
menu.menuOpen = true;
caller.removeClass(options.loadingState);
$(document).click(killAllMenus);
// assign key events
$(document).keydown(function(event){
var e;
if (event.which !="") { e = event.which; }
else if (event.charCode != "") { e = event.charCode; }
else if (event.keyCode != "") { e = event.keyCode; }
var menuType = ($(event.target).parents('div').is('.fg-menu-flyout')) ? 'flyout' : 'ipod' ;
switch(e) {
case 37: // left arrow
if (menuType == 'flyout') {
$(event.target).trigger('mouseout');
if ($('.'+options.flyOutOnState).length > 0) { $('.'+options.flyOutOnState).trigger('mouseover'); };
};
if (menuType == 'ipod') {
$(event.target).trigger('mouseout');
if ($('.fg-menu-footer').find('a').length > 0) { $('.fg-menu-footer').find('a').trigger('click'); };
if ($('.fg-menu-header').find('a').length > 0) { $('.fg-menu-current-crumb').prev().find('a').trigger('click'); };
if ($('.fg-menu-current').prev().is('.fg-menu-indicator')) {
$('.fg-menu-current').prev().trigger('mouseover');
};
};
return false;
break;
case 38: // up arrow
if ($(event.target).is('.' + options.linkHover)) {
var prevLink = $(event.target).parent().prev().find('a:eq(0)');
if (prevLink.length > 0) {
$(event.target).trigger('mouseout');
prevLink.trigger('mouseover');
};
}
else { container.find('a:eq(0)').trigger('mouseover'); }
return false;
break;
case 39: // right arrow
if ($(event.target).is('.fg-menu-indicator')) {
if (menuType == 'flyout') {
$(event.target).next().find('a:eq(0)').trigger('mouseover');
}
else if (menuType == 'ipod') {
$(event.target).trigger('click');
setTimeout(function(){
$(event.target).next().find('a:eq(0)').trigger('mouseover');
}, options.crossSpeed);
};
};
return false;
break;
case 40: // down arrow
if ($(event.target).is('.' + options.linkHover)) {
var nextLink = $(event.target).parent().next().find('a:eq(0)');
if (nextLink.length > 0) {
$(event.target).trigger('mouseout');
nextLink.trigger('mouseover');
};
}
else { container.find('a:eq(0)').trigger('mouseover'); }
return false;
break;
case 27: // escape
killAllMenus();
break;
case 13: // enter
if ($(event.target).is('.fg-menu-indicator') && menuType == 'ipod') {
$(event.target).trigger('click');
setTimeout(function(){
$(event.target).next().find('a:eq(0)').trigger('mouseover');
}, options.crossSpeed);
};
break;
};
});
};
this.create = function() {
container.css({ width: options.width }).appendTo('body').find('ul:first').not('.fg-menu-breadcrumb').addClass('fg-menu');
container.find('ul, li a').addClass('ui-corner-all');
// aria roles & attributes
container.find('ul').attr('role', 'menu').eq(0).attr('aria-activedescendant','active-menuitem').attr('aria-labelledby', caller.attr('id'));
container.find('li').attr('role', 'menuitem');
container.find('li:has(ul)').attr('aria-haspopup', 'true').find('ul').attr('aria-expanded', 'false');
container.find('a').attr('tabindex', '-1');
// when there are multiple levels of hierarchy, create flyout or drilldown menu
if (container.find('ul').length > 1) {
if (options.flyOut) { menu.flyout(container, options); }
else { menu.drilldown(container, options); }
}
else {
container.find('a').click(function(){
menu.chooseItem(this);
return false;
});
};
if (options.linkHover) {
var allLinks = container.find('.fg-menu li a');
allLinks.hover(
function(){
var menuitem = $(this);
$('.'+options.linkHover).removeClass(options.linkHover).blur().parent().removeAttr('id');
$(this).addClass(options.linkHover).focus().parent().attr('id','active-menuitem');
},
function(){
$(this).removeClass(options.linkHover).blur().parent().removeAttr('id');
}
);
};
if (options.linkHoverSecondary) {
container.find('.fg-menu li').hover(
function(){
$(this).siblings('li').removeClass(options.linkHoverSecondary);
if (options.flyOutOnState) { $(this).siblings('li').find('a').removeClass(options.flyOutOnState); }
$(this).addClass(options.linkHoverSecondary);
},
function(){ $(this).removeClass(options.linkHoverSecondary); }
);
};
menu.setPosition(container, caller, options);
menu.menuExists = true;
// Hide empty
container.find('a:empty').parent('li').hide()
};
this.chooseItem = function(item){
menu.kill();
// edit this for your own custom function/callback:
$('#menuSelection').text($(item).text());
location.href = $(item).attr('href');
};
};
Menu.prototype.flyout = function(container, options) {
var menu = this;
this.resetFlyoutMenu = function(){
var allLists = container.find('ul ul');
allLists.removeClass('ui-widget-content').hide();
};
container.addClass('fg-menu-flyout').find('li:has(ul)').each(function(){
var linkWidth = container.width();
var showTimer, hideTimer;
var allSubLists = $(this).find('ul');
allSubLists.css({ left: linkWidth, width: linkWidth }).hide();
$(this).find('a:eq(0)').addClass('fg-menu-indicator').html('<span>' + $(this).find('a:eq(0)').text() + '</span><span class="ui-icon '+options.nextMenuLink+'"></span>').hover(
function(){
clearTimeout(hideTimer);
var subList = $(this).next();
if (!fitVertical(subList, $(this).offset().top)) { subList.css({ top: 'auto', bottom: 0 }); };
if (!fitHorizontal(subList, $(this).offset().left + 100)) { subList.css({ left: 'auto', right: linkWidth, 'z-index': 999 }); };
showTimer = setTimeout(function(){
subList.addClass('ui-widget-content').show(options.showSpeed).attr('aria-expanded', 'true');
}, 300);
},
function(){
clearTimeout(showTimer);
var subList = $(this).next();
hideTimer = setTimeout(function(){
subList.removeClass('ui-widget-content').hide(options.showSpeed).attr('aria-expanded', 'false');
}, 400);
}
);
$(this).find('ul a').hover(
function(){
clearTimeout(hideTimer);
if ($(this).parents('ul').prev().is('a.fg-menu-indicator')) {
$(this).parents('ul').prev().addClass(options.flyOutOnState);
}
},
function(){
hideTimer = setTimeout(function(){
allSubLists.hide(options.showSpeed);
container.find(options.flyOutOnState).removeClass(options.flyOutOnState);
}, 500);
}
);
});
container.find('a').click(function(){
menu.chooseItem(this);
return false;
});
};
Menu.prototype.drilldown = function(container, options) {
var menu = this;
var topList = container.find('.fg-menu');
var breadcrumb = $('<ul class="fg-menu-breadcrumb ui-widget-header ui-corner-all ui-helper-clearfix"></ul>');
var crumbDefaultHeader = $('<li class="fg-menu-breadcrumb-text">'+options.crumbDefaultText+'</li>');
var firstCrumbText = (options.backLink) ? options.backLinkText : options.topLinkText;
var firstCrumbClass = (options.backLink) ? 'fg-menu-prev-list' : 'fg-menu-all-lists';
var firstCrumbLinkClass = (options.backLink) ? 'ui-state-default ui-corner-all' : '';
var firstCrumbIcon = (options.backLink) ? '<span class="ui-icon ui-icon-triangle-1-w"></span>' : '';
var firstCrumb = $('<li class="'+firstCrumbClass+'"><a href="#" class="'+firstCrumbLinkClass+'">'+firstCrumbIcon+firstCrumbText+'</a></li>');
container.addClass('fg-menu-ipod');
if (options.backLink) { breadcrumb.addClass('fg-menu-footer').appendTo(container).hide(); }
else { breadcrumb.addClass('fg-menu-header').prependTo(container); };
breadcrumb.append(crumbDefaultHeader);
var checkMenuHeight = function(el){
if (el.height() > options.maxHeight) { el.addClass('fg-menu-scroll') };
el.css({ height: options.maxHeight });
};
var resetChildMenu = function(el){ el.removeClass('fg-menu-scroll').removeClass('fg-menu-current').height('auto'); };
this.resetDrilldownMenu = function(){
$('.fg-menu-current').removeClass('fg-menu-current');
topList.animate({ left: 0 }, options.crossSpeed, function(){
$(this).find('ul').each(function(){
$(this).hide();
resetChildMenu($(this));
});
topList.addClass('fg-menu-current');
});
$('.fg-menu-all-lists').find('span').remove();
breadcrumb.empty().append(crumbDefaultHeader);
$('.fg-menu-footer').empty().hide();
checkMenuHeight(topList);
};
topList
.addClass('fg-menu-content fg-menu-current ui-widget-content ui-helper-clearfix')
.css({ width: container.width() })
.find('ul')
.css({ width: container.width(), left: container.width() })
.addClass('ui-widget-content')
.hide();
checkMenuHeight(topList);
topList.find('a').each(function(){
// if the link opens a child menu:
if ($(this).next().is('ul')) {
$(this)
.addClass('fg-menu-indicator')
.each(function(){ $(this).html('<span>' + $(this).text() + '</span><span class="ui-icon '+options.nextMenuLink+'"></span>'); })
.click(function(){ // ----- show the next menu
var nextList = $(this).next();
var parentUl = $(this).parents('ul:eq(0)');
var parentLeft = (parentUl.is('.fg-menu-content')) ? 0 : parseFloat(topList.css('left'));
var nextLeftVal = Math.round(parentLeft - parseFloat(container.width()));
var footer = $('.fg-menu-footer');
// show next menu
resetChildMenu(parentUl);
checkMenuHeight(nextList);
topList.animate({ left: nextLeftVal }, options.crossSpeed);
nextList.show().addClass('fg-menu-current').attr('aria-expanded', 'true');
var setPrevMenu = function(backlink){
var b = backlink;
var c = $('.fg-menu-current');
var prevList = c.parents('ul:eq(0)');
c.hide().attr('aria-expanded', 'false');
resetChildMenu(c);
checkMenuHeight(prevList);
prevList.addClass('fg-menu-current').attr('aria-expanded', 'true');
if (prevList.hasClass('fg-menu-content')) { b.remove(); footer.hide(); };
};
// initialize "back" link
if (options.backLink) {
if (footer.find('a').length == 0) {
footer.show();
$('<a href="#"><span class="ui-icon ui-icon-triangle-1-w"></span> <span>Back</span></a>')
.appendTo(footer)
.click(function(){ // ----- show the previous menu
var b = $(this);
var prevLeftVal = parseFloat(topList.css('left')) + container.width();
topList.animate({ left: prevLeftVal }, options.crossSpeed, function(){
setPrevMenu(b);
});
return false;
});
}
}
// or initialize top breadcrumb
else {
if (breadcrumb.find('li').length == 1){
breadcrumb.empty().append(firstCrumb);
firstCrumb.find('a').click(function(){
menu.resetDrilldownMenu();
return false;
});
}
$('.fg-menu-current-crumb').removeClass('fg-menu-current-crumb');
var crumbText = $(this).find('span:eq(0)').text();
var newCrumb = $('<li class="fg-menu-current-crumb"><a href="javascript://" class="fg-menu-crumb">'+crumbText+'</a></li>');
newCrumb
.appendTo(breadcrumb)
.find('a').click(function(){
if ($(this).parent().is('.fg-menu-current-crumb')){
menu.chooseItem(this);
}
else {
var newLeftVal = - ($('.fg-menu-current').parents('ul').length - 1) * 180;
topList.animate({ left: newLeftVal }, options.crossSpeed, function(){
setPrevMenu();
});
// make this the current crumb, delete all breadcrumbs after this one, and navigate to the relevant menu
$(this).parent().addClass('fg-menu-current-crumb').find('span').remove();
$(this).parent().nextAll().remove();
};
return false;
});
newCrumb.prev().append(' <span class="ui-icon '+options.nextCrumbLink+'"></span>');
};
return false;
});
}
// if the link is a leaf node (doesn't open a child menu)
else {
$(this).click(function(){
menu.chooseItem(this);
return false;
});
};
});
};
/* Menu.prototype.setPosition parameters (defaults noted with *):
referrer = the link (or other element) used to show the overlaid object
settings = can override the defaults:
- posX/Y: where the top left corner of the object should be positioned in relation to its referrer.
X: left*, center, right
Y: top, center, bottom*
- offsetX/Y: the number of pixels to be offset from the x or y position. Can be a positive or negative number.
- directionH/V: where the entire menu should appear in relation to its referrer.
Horizontal: left*, right
Vertical: up, down*
- detectH/V: detect the viewport horizontally / vertically
- linkToFront: copy the menu link and place it on top of the menu (visual effect to make it look like it overlaps the object) */
Menu.prototype.setPosition = function(widget, caller, options) {
var el = widget;
var referrer = caller;
var dims = {
refX: referrer.offset().left,
refY: referrer.offset().top,
refW: referrer.getTotalWidth(),
refH: referrer.getTotalHeight()
};
var options = options;
var xVal, yVal;
var helper = $('<div class="positionHelper"></div>');
helper.css({ position: 'absolute', left: dims.refX, top: dims.refY, width: dims.refW, height: dims.refH });
el.wrap(helper);
// get X pos
switch(options.positionOpts.posX) {
case 'left': xVal = 0;
break;
case 'center': xVal = dims.refW / 2;
break;
case 'right': xVal = dims.refW;
break;
};
// get Y pos
switch(options.positionOpts.posY) {
case 'top': yVal = 0;
break;
case 'center': yVal = dims.refH / 2;
break;
case 'bottom': yVal = dims.refH;
break;
};
// add the offsets (zero by default)
xVal += options.positionOpts.offsetX;
yVal += options.positionOpts.offsetY;
// position the object vertically
if (options.positionOpts.directionV == 'up') {
el.css({ top: 'auto', bottom: yVal });
if (options.positionOpts.detectV && !fitVertical(el)) {
el.css({ bottom: 'auto', top: yVal });
}
}
else {
el.css({ bottom: 'auto', top: yVal });
if (options.positionOpts.detectV && !fitVertical(el)) {
el.css({ top: 'auto', bottom: yVal });
}
};
// and horizontally
if (options.positionOpts.directionH == 'left') {
el.css({ left: 'auto', right: xVal });
if (options.positionOpts.detectH && !fitHorizontal(el)) {
el.css({ right: 'auto', left: xVal });
}
}
else {
el.css({ right: 'auto', left: xVal });
if (options.positionOpts.detectH && !fitHorizontal(el)) {
el.css({ left: 'auto', right: xVal });
}
};
// if specified, clone the referring element and position it so that it appears on top of the menu
if (options.positionOpts.linkToFront) {
referrer.clone().addClass('linkClone').css({
position: 'absolute',
top: 0,
right: 'auto',
bottom: 'auto',
left: 0,
width: referrer.width(),
height: referrer.height()
}).insertAfter(el);
};
};
/* Utilities to sort and find viewport dimensions */
function sortBigToSmall(a, b) { return b - a; };
jQuery.fn.getTotalWidth = function(){
return $(this).width() + parseInt($(this).css('paddingRight')) + parseInt($(this).css('paddingLeft')) + parseInt($(this).css('borderRightWidth')) + parseInt($(this).css('borderLeftWidth'));
};
jQuery.fn.getTotalHeight = function(){
return $(this).height() + parseInt($(this).css('paddingTop')) + parseInt($(this).css('paddingBottom')) + parseInt($(this).css('borderTopWidth')) + parseInt($(this).css('borderBottomWidth'));
};
function getScrollTop(){
return self.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
};
function getScrollLeft(){
return self.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
};
function getWindowHeight(){
var de = document.documentElement;
return self.innerHeight || (de && de.clientHeight) || document.body.clientHeight;
};
function getWindowWidth(){
var de = document.documentElement;
return self.innerWidth || (de && de.clientWidth) || document.body.clientWidth;
};
/* Utilities to test whether an element will fit in the viewport
Parameters:
el = element to position, required
leftOffset / topOffset = optional parameter if the offset cannot be calculated (i.e., if the object is in the DOM but is set to display: 'none') */
function fitHorizontal(el, leftOffset){
var leftVal = parseInt(leftOffset) || $(el).offset().left;
return (leftVal + $(el).width() <= getWindowWidth() + getScrollLeft() && leftVal - getScrollLeft() >= 0);
};
function fitVertical(el, topOffset){
var topVal = parseInt(topOffset) || $(el).offset().top;
return (topVal + $(el).height() <= getWindowHeight() + getScrollTop() && topVal - getScrollTop() >= 0);
};
/*--------------------------------------------------------------------
* javascript method: "pxToEm"
* by:
Scott Jehl (scott@filamentgroup.com)
Maggie Wachs (maggie@filamentgroup.com)
http://www.filamentgroup.com
*
* Copyright (c) 2008 Filament Group
* Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
*
* Description: Extends the native Number and String objects with pxToEm method. pxToEm converts a pixel value to ems depending on inherited font size.
* Article: http://www.filamentgroup.com/lab/retaining_scalable_interfaces_with_pixel_to_em_conversion/
* Demo: http://www.filamentgroup.com/examples/pxToEm/
*
* Options:
scope: string or jQuery selector for font-size scoping
reverse: Boolean, true reverses the conversion to em-px
* Dependencies: jQuery library
* Usage Example: myPixelValue.pxToEm(); or myPixelValue.pxToEm({'scope':'#navigation', reverse: true});
*
* Version: 2.0, 08.01.2008
* Changelog:
* 08.02.2007 initial Version 1.0
* 08.01.2008 - fixed font-size calculation for IE
--------------------------------------------------------------------*/
Number.prototype.pxToEm = String.prototype.pxToEm = function(settings){
//set defaults
settings = jQuery.extend({
scope: 'body',
reverse: false
}, settings);
var pxVal = (this == '') ? 0 : parseFloat(this);
var scopeVal;
var getWindowWidth = function(){
var de = document.documentElement;
return self.innerWidth || (de && de.clientWidth) || document.body.clientWidth;
};
/* When a percentage-based font-size is set on the body, IE returns that percent of the window width as the font-size.
For example, if the body font-size is 62.5% and the window width is 1000px, IE will return 625px as the font-size.
When this happens, we calculate the correct body font-size (%) and multiply it by 16 (the standard browser font size)
to get an accurate em value. */
if (settings.scope == 'body' && $.browser.msie && (parseFloat($('body').css('font-size')) / getWindowWidth()).toFixed(1) > 0.0) {
var calcFontSize = function(){
return (parseFloat($('body').css('font-size'))/getWindowWidth()).toFixed(3) * 16;
};
scopeVal = calcFontSize();
}
else { scopeVal = parseFloat(jQuery(settings.scope).css("font-size")); };
var result = (settings.reverse == true) ? (pxVal * scopeVal).toFixed(2) + 'px' : (pxVal / scopeVal).toFixed(2) + 'em';
return result;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,475 @@
$( document ).ready(function() {
/*
$( "input[title]" ).tooltip({
position: {
my: "left top",
at: "right+5 top-5"
}
});
/*
$( "a[title]" ).tooltip({
position: {
my: "bottom center",
at: "bottom+30"
}
});*/
$('tr').removeAttr('onmouseover');
$('tr').removeAttr('onmouseout');
$('input').removeAttr('onclick');
$('._select-unselect_').change(function() { selectUnselect($(this)); });
// BUTTONS
$('.fg-button').hover(
function(){ $(this).removeClass('ui-state-default').addClass('ui-state-focus'); },
function(){ $(this).removeClass('ui-state-focus').addClass('ui-state-default'); }
);
// MENUS
$('#flat').menu({
content: $('#flat').next().html(), // grab content from this page
showSpeed: 100
});
});
window.onload = function() {
var checkboxes = document.getElementsByClassName('ui_checkbox');
for(var i = 0; i < checkboxes.length; i++) {
checkboxes[i].onclick = function() {
var row = this.parentNode.parentNode;
if (this.checked) {
row.className = row.className + ' checked';
}
else {
row.className = row.className.replace(' checked', '');
}
};
}
}
function countUploads(files) {
if(files.files.length = 0) return;
var info = '';
for (i = 0; i < files.files.length; i++) {
info += files.files[i].name + '<br>';
}
$('#readyForUploadList').html(info);
}
function selectAll() {
var rows = document.getElementsByClassName('ui_checked_columns');
for (i = 0; i < rows.length; i++) {
var input = rows[i].getElementsByTagName('input')[0];
if (!input.checked) {
rowClick(rows[i]);
}
}
}
function invertSelection() {
var rows = document.getElementsByClassName('ui_checked_columns');
for (i = 0; i < rows.length; i++)
rowClick(rows[i]);
}
function compressDialog() {
if(checkSelected()) {
$( "#compressDialog" ).dialog({
modal: true,
buttons: {
"Compress": function() {
compressSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function compressSelected() {
var filename = $('#compressSelectedForm input[name=filename]').val();
if (filename != null && filename != "") {
var method = $('#compressSelectedForm select[name=method] option:selected').val();
$('#list_form').attr('action', "compress.cgi?arch=" + filename + "&method=" + method);
$('#list_form').submit();
}
}
function removeSelected() {
if(checkSelected()) {
$('#items-to-remove').html('');
$(".ui_checked_checkbox input[type='checkbox']:checked").each(function() {
$('#items-to-remove').append($(this).val() + '<br>');
});
$( "#removeDialog" ).dialog({
modal: true,
buttons: {
"YES": function() {
document.forms['list_form'].action = "delete.cgi";
document.forms['list_form'].submit();
},
"NO": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function chmodDialog() {
if(checkSelected()) {
$( "#chmodDialog" ).dialog({
modal: true,
minWidth: 550,
buttons: {
"Change": function() {
chmodSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function chmodSelected() {
var perms = $('#perms').val();
if (perms != null && perms != "") {
var applyto = $('#chmodForm select[name=applyto] option:selected').val();
$('#list_form').attr('action', "chmod.cgi?perms=" + perms + "&applyto=" + applyto);
$('#list_form').submit();
}
}
function chownDialog() {
if(checkSelected()) {
$( "#chownDialog" ).dialog({
modal: true,
buttons: {
"Change": function() {
chownSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function chownSelected() {
var owner = $('#chownForm input[name=owner]').val();
var group = $('#chownForm input[name=group]').val();
var recursive = $('#chown-recursive').prop('checked');
if (owner != null && owner != "" && group != null && group != "") {
$('#list_form').attr('action', "chown.cgi?owner=" + owner + "&group=" + group + "&recursive=" + recursive);
$('#list_form').submit();
}
}
function chattrDialog() {
if(checkSelected()) {
$( "#chattrDialog" ).dialog({
modal: true,
buttons: {
"Change": function() {
chattrSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function chattrSelected() {
var label = $('#chattrForm input[name=label]').val(),
recursive = $('#chattr-recursive').prop('checked');
if (label != null && label != "") {
$('#list_form').attr('action', "chattr.cgi?label=" + encodeURIComponent(label) + "&recursive=" + recursive);
$('#list_form').submit();
}
}
function chconDialog() {
if(checkSelected()) {
$( "#chconDialog" ).dialog({
modal: true,
buttons: {
"Change": function() {
chconSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function chconSelected() {
var label = $('#chconForm input[name=label]').val(),
recursive = $('#chcon-recursive').prop('checked');
if (label != null && label != "") {
$('#list_form').attr('action', "chcon.cgi?label=" + label + "&recursive=" + recursive);
$('#list_form').submit();
}
}
function aclsDialog() {
if(checkSelected()) {
$( "#aclsDialog" ).dialog({
modal: true,
buttons: {
"Change": function() {
aclsSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
}
function aclsSelected() {
var aclsForm = $('#aclsForm').serialize();
$('#list_form').attr('action', "setfacl.cgi?" + aclsForm);
$('#list_form').submit();
}
function renameDialog(file) {
$("#renameForm input[name=name]").val(file);
$("#renameForm input[name=file]").val(file);
$( "#renameDialog" ).dialog({
modal: true,
buttons: {
"Rename": function() {
renameSelected();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function renameSelected() {
var name = $('#renameForm input[name=name]').val();
var file = $('#renameForm input[name=file]').val();
if (name != null && name != "" && name != file) {
$('#renameForm').submit();
}
}
function copySelected() {
if(checkSelected()) {
document.forms['list_form'].action = "copy.cgi";
document.forms['list_form'].submit();
}
}
function cutSelected() {
if(checkSelected()) {
document.forms['list_form'].action = "cut.cgi";
document.forms['list_form'].submit();
}
}
function viewReadyForUpload() {
$( "#readyForUploadDialog" ).dialog({
modal: true,
buttons: {
"OK": function() {
uploadFiles();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function browseForUpload() {
var files = document.getElementById('upfiles');
files.click();
return true;
}
function uploadFiles() {
var files = document.getElementById('upfiles');
if (files.files.length > 0)
$('#upload-form').submit();
else
files.click();
}
function createFolderDialog() {
$( "#createFolderDialog" ).dialog({
modal: true,
buttons: {
"Create": function() {
createFolder();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function createFolder() {
var name = $('#createFolderForm input[name=name]').val();
if (name != null && name != "")
$("#createFolderForm").submit();
else {
/* var tooltip = $('#createFolderForm input[name=name]').tooltip({
position: {
my: "left top",
at: "right+5 top-5"
}
});
tooltip.tooltip('open');*/
$('#createFolderForm input[name=name]').tooltip('open');
}
}
function createFileDialog() {
$( "#createFileDialog" ).dialog({
modal: true,
buttons: {
"Create": function() {
createFile();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function createFile() {
var name = $('#createFileForm input[name=name]').val();
if (name != null && name != "") {
$("#createFileForm").submit();
}
}
function downFromUrlDialog() {
$( "#downFromUrlDialog" ).dialog({
modal: true,
buttons: {
"Download": function() {
downFromUrl();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function downFromUrl() {
var link = $('#downFromUrlForm input[name=link]').val();
if (link != null && link != "")
$('#downFromUrlForm').submit();
}
function selectUnselect(cb) {
var rows = $('.ui_checked_columns');
for (i = 0; i < rows.length; i++) {
switch(cb.is(":checked")) {
case true:
selectRow(rows[i]);
break;
case false:
unselectRow(rows[i]);
break;
}
}
}
function rowClick(row) {
var input = row.getElementsByTagName('input')[0];
input.checked = !input.checked;
if (input.checked) {
row.className = row.className + ' checked';
}
else {
row.className = row.className.replace(' checked', '');
}
}
function selectRow(row) {
var input = row.getElementsByTagName('input')[0];
if(!input.checked) {
input.checked = true;
row.className = row.className + ' checked';
}
}
function unselectRow(row) {
var input = row.getElementsByTagName('input')[0];
if(input.checked) {
input.checked = false;
row.className = row.className.replace(' checked', '');
}
}
function checkSelected() {
var checkboxes = $('.ui_checkbox');
for(var i = 0; i < checkboxes.length; i++) {
if(checkboxes[i].checked) return true;
}
$( "#nothingSelected" ).dialog({
modal: true,
buttons: {
"OK": function() {
$( this ).dialog( "close" );
}
}
});
return false;
}
function searchDialog() {
$( "#searchDialog" ).dialog({
modal: true,
buttons: {
"Search": function() {
search();
},
"Cancel": function() {
$( this ).dialog( "close" );
}
}
});
}
function search() {
var query = $('#searchForm input[name=query]').val();
if (query != null && query != "")
$("#searchForm").submit();
}
/*
function checkSelected() {
var checkboxes = document.getElementsByClassName('ui_checkbox');
for(var i = 0; i < checkboxes.length; i++) {
if(checkboxes[i].checked) return true;
}
alert('Nothing selected');
return false;
}
*/

View File

@@ -1,183 +0,0 @@
/*
* plain.js
* Behavior of the File Manager for themes without Bootstrap, on top of
* the core widget script : row selection, the selection check before an
* action on the selected entries, sending the selection form to an
* action script with a dialog's fields as parameters, the rename and
* delete dialogs, the permissions calculator and the upload options.
* Everything is delegated from the document, keyed off data-filemin-*
* attributes, so no handlers are generated in the markup.
*/
(function () {
'use strict';
var widgets = window.webminUiWidgets;
// The selection form and its row checkboxes
function listForm() {
return document.getElementById('list_form');
}
function rowBoxes() {
var form = listForm();
return form ? Array.prototype.slice.call(
form.querySelectorAll('input[type="checkbox"][name="name"]')) : [];
}
function selectedBoxes() {
return rowBoxes().filter(function (box) { return box.checked; });
}
function selectRows(mode) {
rowBoxes().forEach(function (box) {
box.checked = mode === 'all' ? true :
mode === 'none' ? false : !box.checked;
});
}
// Called by the checkbox in the table heading
window.selectUnselect = function (master) {
selectRows(master.checked ? 'all' : 'none');
};
// Called by the rename link of a row, with the entry's name
window.renameDialog = function (file) {
widgets.openDialog('filemin_rename', { name: file, file: file });
};
// Send the selection form to a script, with parameters in its URL
function submitSelection(script, params) {
var form = listForm();
if (!form) return;
var url = script;
var query = params ? params.toString() : '';
if (query) url += (url.indexOf('?') >= 0 ? '&' : '?') + query;
form.setAttribute('action', url);
form.submit();
}
// The named, enabled fields of a dialog form as URL parameters,
// leaving out the calculator's own fields
function formParams(form) {
var params = new URLSearchParams();
Array.prototype.forEach.call(form.elements, function (field) {
if (!field.name || field.disabled ||
field.hasAttribute('data-filemin-skip')) return;
if (/^(?:file|submit|button|reset)$/.test(field.type)) return;
if ((field.type === 'checkbox' || field.type === 'radio') &&
!field.checked) return;
if (field.tagName === 'SELECT' && field.multiple) {
Array.prototype.forEach.call(field.options, function (option) {
if (option.selected) params.append(field.name, option.value);
});
return;
}
params.append(field.name, field.value);
});
return params;
}
// Toolbar entries : selecting rows, actions that need a selection,
// and actions that send the selection straight to a script. Caught
// before the widget script, so a dialog is not opened for an empty
// selection.
document.addEventListener('click', function (e) {
var control = e.target.closest && e.target.closest(
'[data-filemin-select], [data-filemin-selection], [data-filemin-submit]');
if (!control) return;
if (control.hasAttribute('data-filemin-select')) {
e.preventDefault();
selectRows(control.getAttribute('data-filemin-select'));
return;
}
if (control.hasAttribute('data-filemin-selection') && !selectedBoxes().length) {
e.preventDefault();
e.stopImmediatePropagation();
widgets.openDialog('filemin_nothing');
return;
}
var script = control.getAttribute('data-filemin-submit');
if (script) {
e.preventDefault();
submitSelection(script);
}
}, true);
document.addEventListener('submit', function (e) {
var form = e.target;
if (!form.getAttribute) return;
// A dialog acting on the selection : its fields become the
// parameters of the action script
var script = form.getAttribute('data-filemin-apply');
if (script) {
e.preventDefault();
var dialog = form.closest('dialog');
if (dialog && dialog.open) dialog.close();
submitSelection(script, formParams(form));
return;
}
// An upload : the options go in the URL, which the server reads
// before the files arrive, and progress is shown in a popup
var upload = form.getAttribute('data-filemin-upload');
if (upload) {
var action = upload;
form.querySelectorAll('[data-filemin-upload-option]').forEach(function (box) {
if (box.checked) {
action += '&' + box.getAttribute('data-filemin-upload-option') + '=1';
}
});
form.setAttribute('action', action);
var tracker = form.getAttribute('data-filemin-tracker');
if (tracker) {
window.open(tracker, 'uptracker',
'toolbar=no,menubar=no,scrollbars=no,location=no,resizable=no,' +
'width=' + window.screen.width + ',height=' + window.screen.height);
}
}
});
// Directory upload : the file input picks directories instead
document.addEventListener('change', function (e) {
var box = e.target;
if (box.matches && box.matches('[data-filemin-upload-dirs]')) {
var input = box.form && box.form.querySelector('input[type="file"]');
if (input) input.toggleAttribute('webkitdirectory', box.checked);
}
});
// The delete dialog lists the selected entries as it opens
document.addEventListener('ui-dialog-open', function (e) {
if (e.target.id !== 'filemin_delete') return;
var list = e.target.querySelector('#filemin_delete_items');
if (list) {
list.textContent = selectedBoxes().map(function (box) {
return box.value;
}).join('\n');
}
});
// Permissions : the octal field, the checkbox matrix and the hidden
// perms parameter stay in step through the shared calculator
function syncPerms(fromMatrix) {
var form = document.forms['chmod'];
if (!form || typeof window.calc_chmod !== 'function') return;
if (fromMatrix) {
window.calc_chmod();
}
else if (/^[0-7]{4}$/.test(form.t_total.value) &&
typeof window.octalchange === 'function') {
window.octalchange();
}
var perms = form.querySelector('input[name="perms"]');
if (perms) perms.value = form.t_total.value;
}
document.addEventListener('input', function (e) {
if (e.target.matches && e.target.matches('[data-filemin-chmod-octal]')) {
syncPerms(false);
}
});
document.addEventListener('change', function (e) {
if (e.target.matches && e.target.matches('[data-filemin-chmod]')) {
syncPerms(true);
}
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { syncPerms(false); });
} else {
syncPerms(false);
}
})();

View File

@@ -0,0 +1,246 @@
<div id="nothingSelected" class="modal">
<h3>$text{'nothing_selected'}</h3>
</div>
<div id="createFolderDialog" class="modal">
<form id="createFolderForm" action="create_folder.cgi" method="post">
<div class="form-group">
<label>$text{'new_folder_name'}</label>
<input id="name" name="name" type="text" title="$text{'provide_folder_name'}">
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="createFileDialog" class="modal">
<form id="createFileForm" action="create_file.cgi" method="post">
<div class="form-group">
<label>$text{'new_file_name'}</label>
<input name="name" type="text">
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="renameDialog" class="modal">
<form id="renameForm" action="rename.cgi" method="post">
<div class="form-group">
<label>$text{'new_name'}</label>
<input name="name" type="text">
<input type='hidden' name='file' value='' required>
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="compressDialog" class="modal">
<form id="compressSelectedForm" action="compress.cgi" method="post">
<div class="form-group">
<label>$text{'archive_name'}</label>
<input name="filename" type="text">
<select name="method">
<option value="plain-tar">.tar</option>
<option value="xz-tar">.tar.xz</option>
<option selected value="tar">.tar.gz</option>
<option value="zstd-tar">.zstd</option>
<option value="zip">.zip</option>
</select>
</div>
</form>
</div>
<div id="removeDialog" class="modal">
<p>$text{'about_to_delete'}</p>
<pre id="items-to-remove">
</pre>
$text{'are_you_sure'}
</div>
<div id="chownDialog" class="modal">
<form id="chownForm">
<div class="form-group">
<label>$text{'user_name'}</label><br>
<input name="owner" type="text"><br>
<label>$text{'group_name'}</label><br>
<input name="group" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chown-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="chattrDialog" class="modal">
<form id="chattrForm">
<div class="form-group">
<label>$text{'attr_label'}</label><br>
<input name="label" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chattr-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="chconDialog" class="modal">
<form id="chconForm">
<div class="form-group">
<label>$text{'context_label'}</label><br>
<input name="label" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chcon-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="aclsDialog" class="modal">
<form id="aclsForm">
<div class="form-group">
<label>$text{'acls_action'}</label><br>
<select name="action">
<option value=""></option>
<option selected value="-m">Modify (-m)</option>
<option value="-x">Remove (-x)</option>
<option value="-b">Remove all extended (-b)</option>
<option value="-k">Remove default (-k)</option>
</select><br><br>
<label>$text{'acls_applyto'}</label><br>
<select multiple size=4 name="apply_to">
<option value="u">$text{'acls_user'}</option>
<option value="g">$text{'acls_group'}</option>
<option value="m">$text{'acls_mask'}</option>
<option value="o">$text{'acls_other'}</option>
</select><br><br>
<label>$text{'acls_perms'}</label><br>
<select name="perms">
<option value=""></option>
<option value="---">---</option>
<option value="r--">r--</option>
<option value="-w-">-w-</option>
<option value="--x">--x</option>
<option value="r-x">r-x</option>
<option value="rw-">rw-</option>
<option value="-wx">-wx</option>
<option value="rwx">rwx</option>
</select><br><br>
<label>$text{'acls_user'}</label><br>
$acl_user_select<br><br>
<label>$text{'acls_group'}</label><br>
$acl_group_select<br><br>
<label>$text{'dialog_recursive'}</label>
<input id="acls-recursive" type="checkbox" name="recursive">
<br><br>
$acl_manual
</div>
</form>
</div>
<div id="downFromUrlDialog" class="modal">
<form id="downFromUrlForm" action="http_download.cgi" method="post">
<div class="form-group">
<label style="display: block;">$text{'dialog_file_url'} <sup class="text-danger">&nbsp;*</sup><br>
<input name="link" type="url" data-placement="top" data-content="$text{'provide_url'}" data-trigger="manual" autocomplete="url" style="font-weight: normal;">
</label>
<label style="display: block;">$text{'dialog_user_name'}<br>
<input name="username" type="text" autocomplete="off" style="font-weight: normal;">
</label>
<label style="display: block;">$text{'dialog_user_pass'}<br>
<input name="password" type="password" autocomplete="new-password" style="font-weight: normal;">
</label>
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="readyForUploadDialog" class="modal">
<form id='upload-form' method='post' onsubmit="this.extract_upload.checked=false" action='upload.cgi?path=$urlized_path&id=$upid' enctype='multipart/form-data' onsubmit='window.open("@{[&get_webprefix()]}/uptracker.cgi?id=$upid&uid=$uid", "uptracker", "toolbar=no, menubar=no, scrollbars=no, location=no, resizable=no, width=screen.width, height=screen.height")'>
<input type='file' id='upfiles' name='upfiles' multiple onchange='countUploads(this)'>
<input type='hidden' name='path' value='$quote_escaped_path'>
<br>
<br>
<input type="checkbox" id="filetype" onchange="this.parentElement.querySelector('#upfiles').toggleAttribute('webkitdirectory')"><label for="filetype"> $text{'upload_dirs'}</label>
<br>
<input type="checkbox" id="extract_upload" onchange="t=document.querySelector('#upload-form'),v=t.getAttribute('action'),x='&extract_uploaded=1',this.checked?t.setAttribute('action', t.getAttribute('action') + x):t.setAttribute('action',t.getAttribute('action').replace(x,''))"><label for="extract_upload"> $text{'extract_uploaded'}</label>
<br>
<input type="checkbox" id="overwrite_existing" onchange="t=document.querySelector('#upload-form'),v=t.getAttribute('action'),x='&overwrite_existing=1',this.checked?t.setAttribute('action', t.getAttribute('action') + x):t.setAttribute('action',t.getAttribute('action').replace(x,''))"><label for="overwrite_existing"> $text{'overwrite_existing'}</label>
</form>
<div id="readyForUploadList" class="well" style="max-height: 180px;">
</div>
</div>
<div id="chmodDialog" class="modal fade">
<form name="chmod" id="chmodForm">
<div class="form-group">
<label for="t_total">$text{'permissions'}:&nbsp; </label><br>
<input id="perms" type="text" oninput="if(this.value.length>4)this.value=this.value.slice(0,4)" name="t_total" value="0644" size="4" onKeyUp="octalchange()" class="form-group-item">
<input type="text" name="sym_total" value="" size="12" readonly="true" class="form-group-item">
</div>
<table class="table">
<tr>
<th></th>
<th><b>$text{'chmod_owner'}</b></th>
<th><b>$text{'chmod_group'}</b></th>
<th><b>$text{'chmod_others'}<b></th>
</tr>
<tr>
<td>$text{'chmod_read'}</td>
<td><input type="checkbox" name="owner4" value="4" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="group4" value="4" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="other4" value="4" onchange="calc_chmod()"></td>
</tr>
<tr>
<td>$text{'chmod_write'}</td>
<td><input type="checkbox" name="owner2" value="2" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="group2" value="2" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="other2" value="2" onchange="calc_chmod()"></td>
</tr>
<tr>
<td>$text{'chmod_execute'}</td>
<td><input type="checkbox" name="owner1" value="1" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="group1" value="1" onchange="calc_chmod()"></td>
<td><input type="checkbox" name="other1" value="1" onchange="calc_chmod()"></td>
</tr>
<tr>
<td>----------</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>$text{'chmod_sticky'}</td>
<td><input type="checkbox" name="sticky" value="1" onchange="calc_chmod()"></td>
<td></td>
<td></td>
</tr>
<tr>
<td>$text{'chmod_setgid'}</td>
<td><input type="checkbox" name="setgid" value="2" onchange="calc_chmod()"></td>
<td></td>
<td></td>
</tr>
</table>
<label>$text{'chmod_apply_to'}:&nbsp; </label>
<select name="applyto">
<option value="1">$text{'chmod_dir_only'}</option>
<option value="2">$text{'chmod_dir_and_its_files'}</option>
<option value="3">$text{'chmod_all'}</option>
<option value="4">$text{'chmod_dir_files_and_subs_files'}</option>
<option value="5">$text{'chmod_dir_and_subs'}</option>
</select>
</form>
</div>
<div id="searchDialog" class="modal">
<form id="searchForm" action="search.cgi" method="post">
<div class="form-group">
<label>$text{'search_label'}</label>
<input id="query" name="query" type="text" title="$text{'provide_search_query'}"><br><br>
<input id="caseins" name="caseins" type="checkbox" checked="checked"> $text{'search_insensitive'}
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>

View File

@@ -0,0 +1,29 @@
<div id="quicks" style="float:right">
<a href="javascript:void(0)" onclick='searchDialog()' title="$text{'search'}"><img src='images/icons/quick/edit-find.png' alt="$text{'search'}" /></a>
<a href="bookmark.cgi?path=$urlized_path" title="$text{'bookmark_folder'}"><img src='images/icons/quick/bookmark-new.png' alt="$text{'bookmark_folder'}" /></a>
<a tabindex="0" class="fg-button fg-button-icon-right ui-widget ui-state-default ui-corner-all" id="flat">
<img src="images/icons/quick/go-down.png" alt="$text{'config_bookmarks'}">
</a>
<div id="bookmarks" class="ui-helper-hidden">
<ul>
$bookmarks
</ul>
</div>
<a href="javascript:void(0)" onclick='selectAll()' title="$text{'select_all'}"><img src='images/icons/quick/select-all.png' alt="$text{'select_all'}" /></a>
<a href="javascript:void(0)" onclick='invertSelection()' title="$text{'invert_selection'}"><img src='images/icons/quick/invert.png' alt="$text{'invert_selection'}" /></a>
<a href="javascript:void(0)" onclick='copySelected()' title="$text{'copy_selected'}"><img src='images/icons/quick/edit-copy.png' alt="$text{'copy_selected'}" /></a>
<a href="javascript:void(0)" onclick='cutSelected()' title="$text{'cut_selected'}"><img src='images/icons/quick/edit-cut.png' alt="$text{'cut_selected'}" /></a>
<a href='paste.cgi?path=$urlized_path' title="$text{'paste'}"><img src='images/icons/quick/edit-paste.png' alt="$text{'paste'}" /></a>
<a href="javascript:void(0)" onclick='createFolderDialog()' title="$text{'create_folder'}"><img src='images/icons/quick/folder-new.png' alt="$text{'create_folder'}" /></a>
<a href="javascript:void(0)" onclick='createFileDialog()' title="$text{'create_file'}"><img src='images/icons/quick/document-new.png' alt="$text{'create_file'}" /></a>
<a href="javascript:void(0)" onclick='compressDialog()' title="$text{'compress_selected'}"><img src='images/icons/quick/compress.png' alt="$text{'compress_selected'}" /></a>
<a href="javascript:void(0)" onclick='chmodDialog()' title="$text{'chmod_selected'}"><img src='images/icons/quick/chmod.png' alt="$text{'chmod_selected'}" /></a>
<a href="javascript:void(0)" onclick='chownDialog()' title="$text{'chown_selected'}"><img src='images/icons/quick/chown.png' alt="$text{'chown_selected'}" /></a>
<a href="javascript:void(0)" onclick='chattrDialog()' title="$text{'chattr_selected'}"><img src='images/icons/quick/chattr.png' alt="$text{'chattr_selected'}" /></a>
<a href="javascript:void(0)" onclick='chconDialog()' title="$text{'chcon_selected'}"><img src='images/icons/quick/chcon.png' alt="$text{'chcon_selected'}" /></a>
<a href="javascript:void(0)" onclick='aclsDialog()' title="$text{'acls_selected'}"><img src='images/icons/quick/acls.png' alt="$text{'acls_selected'}" /></a>
<a href="javascript:void(0)" onclick='removeSelected()' title="$text{'remove_selected'}"><img src='images/icons/quick/remove.png' alt="$text{'remove_selected'}" /></a>
<a href="javascript:void(0)" onclick='viewReadyForUpload()' title="$text{'upload_files'}"><img src='images/icons/quick/upload.png' alt="$text{'upload_files'}" /></a>
<a href="javascript:void(0)" onclick='downFromUrlDialog()' title="$text{'get_from_url'}"><img src='images/icons/quick/from-url.png' alt="$text{'get_from_url'}" /></a>
</div>
<div id="info" style="float:right"></div>

View File

@@ -0,0 +1,67 @@
<div class="btn-group pull-right" role="group">
<button class="btn btn-inverse" onclick='searchDialog()' title="$text{'search'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-search" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick="location.href='bookmark.cgi?path=$urlized_path'" title="$text{'bookmark_folder'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-bookmark-o" aria-hidden="true"></i>
</button>
<div class="btn-group">
<button type="button" class="btn btn-inverse dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" title="$text{'config_bookmarks'}">
<i class="fa fa-book" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
$bookmarks
</ul>
</div>
<button class="btn btn-inverse" onclick='window.location.href="index.cgi?path=$urlized_path"' title="$text{'refresh'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-refresh" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='selectAll()' title="$text{'select_all'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-check-square-o" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='invertSelection()' title="$text{'invert_selection'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-check-square" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='copySelected()' title="$text{'copy_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-copy" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='cutSelected()' title="$text{'cut_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-cut" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick="location.href='paste.cgi?path=$urlized_path'" title="$text{'paste'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-paste" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='createFolderDialog()' title="$text{'create_folder'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-folder-o" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='createFileDialog()' title="$text{'create_file'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-file-o" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='compressDialog()' title="$text{'compress_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-file-archive-o" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='chmodDialog()' title="$text{'chmod_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-gears" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='chownDialog()' title="$text{'chown_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-users" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='chattrDialog()' title="$text{'chattr_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-tags" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='chconDialog()' title="$text{'chcon_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-shield" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='aclsDialog()' title="$text{'acls_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-account-key" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='removeDialog()' title="$text{'remove_selected'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-trash-o" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='viewReadyForUpload()' title="$text{'upload_files'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-upload" aria-hidden="true"></i>
</button>
<button class="btn btn-inverse" onclick='downFromUrlDialog()' title="$text{'get_from_url'}" data-toggle="tooltip" data-placement="bottom" data-container="body">
<i class="fa fa-globe" aria-hidden="true"></i>
</button>
</div>

View File

@@ -7,7 +7,7 @@ index_ecommands=Ninguno de los comandos de respaldo de sistemas de archivo (como
index_return=lista de respaldos
dump_dir=Directorio a respaldar
dump_dirs=Directorios a respaldar
dump_dirs=Directorios a rspaldar
dump_fs=Sistema de archivos
dump_sched=¿Programado?
dump_when=Horas programadas para respaldar

View File

@@ -16,3 +16,4 @@ applet_light_bg=ffffff
noindex=1
inframe=1
framed=1
oofunctions=theme-init.pl

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

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

View File

@@ -50,6 +50,12 @@ if ($hasvirt || $hasvm2) {
[ "webmin", $text{'edright_webmin'} ] ]));
}
# Menu in a frame of its own, or on every page
print &ui_table_row($text{'edright_layout'},
&ui_select("layout", $sects->{'layout'},
[ [ "", $text{'edright_layout_frames'} ],
[ "single", $text{'edright_layout_single'} ] ]));
# Left frame size
print &ui_table_row($text{'edright_fsize'},
&ui_opt_textbox("fsize", $sects->{'fsize'}, 6, $text{'edright_fsizedef'}).

View File

@@ -12,14 +12,11 @@ $right_frame_sections_file = "$config_directory/$current_theme/sections";
$default_domains_to_show = 10;
# get_left_frame_width()
# Returns the width of the left frame in pixels
# Returns the width of the left frame in pixels, the same as the width of
# the menu in the single page layout
sub get_left_frame_width
{
local $sects = &get_right_frame_sections();
return $sects->{'fsize'} ? $sects->{'fsize'} :
&get_product_name() eq 'usermin' ? 200 :
&foreign_available("server-manager") &&
&foreign_available("virtual-server") ? 280 : 260;
return &theme_menu_width();
}
# list_virtualmin_theme_overlays()

View File

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

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

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