Compare commits
1 Commits
master
...
dev/framed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbe1f7f17e |
26
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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>");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'}) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'})));
|
||||
|
||||
@@ -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'}, "");
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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();
|
||||
12
bacula-backup/config-windows
Normal 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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,6 @@ command=1
|
||||
create=1
|
||||
delete=1
|
||||
move=1
|
||||
env=1
|
||||
kill=1
|
||||
hourly=2
|
||||
stop=1
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
stop=0
|
||||
env=0
|
||||
|
||||
@@ -4,6 +4,5 @@ command=1
|
||||
create=1
|
||||
delete=1
|
||||
move=1
|
||||
env=0
|
||||
kill=1
|
||||
hourly=0
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
19
fastrpc.cgi
@@ -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);
|
||||
|
||||
@@ -40,17 +40,11 @@ else {
|
||||
}
|
||||
$| = 1;
|
||||
|
||||
# list_disks_partitions([include-cds], [disk-only], [error-ref])
|
||||
# Returns a list of disk hash refs; disk-only mode omits partition metadata.
|
||||
# Failure returns an empty list, never an error string. The optional error-ref
|
||||
# is a scalar reference that receives the reason without changing the return type.
|
||||
# Example: my @disks = list_disks_partitions(undef, 1, \$error);
|
||||
# list_disks_partitions([include-cds])
|
||||
# Returns a structure containing the details of all disks and partitions
|
||||
sub list_disks_partitions
|
||||
{
|
||||
local ($include_cds, $disk_only) = @_;
|
||||
my $error_ref = $_[2];
|
||||
$$error_ref = undef if ($disk_only && $error_ref);
|
||||
if (!$disk_only && scalar(@list_disks_partitions_cache)) {
|
||||
if (scalar(@list_disks_partitions_cache)) {
|
||||
return @list_disks_partitions_cache;
|
||||
}
|
||||
|
||||
@@ -128,13 +122,10 @@ else {
|
||||
}
|
||||
|
||||
local (@disks, @devs, $d);
|
||||
my $discovery_partitions = '';
|
||||
|
||||
if (open(PARTS, "</proc/partitions")) {
|
||||
# The list of all disks can come from the kernel
|
||||
local $sc = 0;
|
||||
while(<PARTS>) {
|
||||
$discovery_partitions .= $_ if ($disk_only);
|
||||
if (/\d+\s+\d+\s+\d+\s+sd([a-z]+)\s/ ||
|
||||
/\d+\s+\d+\s+\d+\s+(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)\s+/) {
|
||||
# New or old style SCSI device
|
||||
@@ -166,7 +157,7 @@ if (open(PARTS, "</proc/partitions")) {
|
||||
if (open(MEDIA, "</proc/ide/hd$n/media")) {
|
||||
local $media = <MEDIA>;
|
||||
close(MEDIA);
|
||||
if ($media =~ /^disk/ && !$include_cds) {
|
||||
if ($media =~ /^disk/ && !$_[0]) {
|
||||
push(@devs, "/dev/hd$n");
|
||||
}
|
||||
}
|
||||
@@ -222,36 +213,17 @@ if (open(PARTS, "</proc/partitions")) {
|
||||
@devs = sort { ($b =~ /\/hd[a-z]+$/ ? 1 : 0) <=>
|
||||
($a =~ /\/hd[a-z]+$/ ? 1 : 0) } @devs;
|
||||
}
|
||||
elsif ($disk_only) {
|
||||
return &disk_discovery_error("Cannot open disk inventory for disk-only discovery", $error_ref);
|
||||
}
|
||||
if ($disk_only) {
|
||||
return &disk_discovery_error("Invalid disk inventory for disk-only discovery", $error_ref)
|
||||
if ($discovery_partitions !~ /major\s+minor\s+#blocks\s+name/);
|
||||
return &disk_discovery_error("Disk inventory changed during disk-only discovery", $error_ref)
|
||||
if ($discovery_partitions ne &read_file_contents('/proc/partitions'));
|
||||
}
|
||||
return ( ) if (!@devs); # No disks, ie on Xen
|
||||
|
||||
# Skip cd-rom drive, identified from symlink. Don't do this if we can identify
|
||||
# cds by their media type though
|
||||
if (!-d "/proc/ide") {
|
||||
local @cdstat = stat("/dev/cdrom");
|
||||
if (@cdstat && !$include_cds) {
|
||||
if (@cdstat && !$_[0]) {
|
||||
@devs = grep { (stat($_))[1] != $cdstat[1] } @devs;
|
||||
}
|
||||
}
|
||||
|
||||
my %discovery_identity;
|
||||
if ($disk_only) {
|
||||
foreach my $device (@devs) {
|
||||
my $identity = &disk_discovery_identity($device);
|
||||
return &disk_discovery_error("Missing device during disk-only discovery: $device", $error_ref)
|
||||
if (!defined($identity));
|
||||
$discovery_identity{$device} = $identity;
|
||||
}
|
||||
}
|
||||
|
||||
# Get Linux disk ID mapping
|
||||
local %id_map;
|
||||
local %all_id_map;
|
||||
@@ -268,246 +240,6 @@ foreach my $id (readdir(IDS)) {
|
||||
}
|
||||
closedir(IDS);
|
||||
|
||||
# Both paths use the same classification, ID and model logic.
|
||||
my $set_metadata = sub {
|
||||
my ($disk) = @_;
|
||||
if ($disk->{'device'} =~ /\/sd([a-z]+)$/) {
|
||||
# Old-style SCSI disk
|
||||
$disk->{'desc'} = &text('select_device', 'SCSI',
|
||||
uc($1));
|
||||
local ($dscsi) = grep { $_->{'dev'} eq "sd$1" } @dscsi;
|
||||
$disk->{'scsi'} = $dscsi ? &indexof($dscsi, @dscsi)
|
||||
: ord(uc($1))-65;
|
||||
$disk->{'type'} = 'scsi';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/hd([a-z]+)$/) {
|
||||
# IDE disk
|
||||
$disk->{'desc'} = &text('select_device', 'IDE', uc($1));
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/xvd([a-z]+)$/) {
|
||||
# Xen virtual disk
|
||||
$disk->{'desc'} = &text('select_device', 'Xen', uc($1));
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/mmcblk([0-9]+)$/) {
|
||||
# SD-card / MMC
|
||||
$disk->{'desc'} = &text('select_device', 'SD-Card', $1);
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/vd([a-z]+)$/) {
|
||||
# KVM virtual disk
|
||||
$disk->{'desc'} = &text('select_device',
|
||||
'VirtIO', uc($1));
|
||||
$disk->{'type'} = 'virtio';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
|
||||
# New complete SCSI disk specification
|
||||
$disk->{'host'} = $2;
|
||||
$disk->{'bus'} = $3;
|
||||
$disk->{'target'} = $4;
|
||||
$disk->{'lun'} = $5;
|
||||
$disk->{'desc'} = &text('select_scsi',
|
||||
"$2", "$3", "$4", "$5");
|
||||
|
||||
# Work out the SCSI index for this disk
|
||||
local $j;
|
||||
if ($dscsi_mode) {
|
||||
for($j=0; $j<@dscsi; $j++) {
|
||||
if ($dscsi[$j]->{'host'} == $disk->{'host'} && $dscsi[$j]->{'bus'} == $disk->{'bus'} && $dscsi[$j]->{'target'} == $disk->{'target'} && $dscsi[$j]->{'lnun'} == $disk->{'lun'}) {
|
||||
$disk->{'scsi'} = $j;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for($j=0; $j<@pscsi; $j++) {
|
||||
if ($pscsi[$j] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i && $disk->{'host'} == $1 && $disk->{'target'} == $2) {
|
||||
$disk->{'scsi'} = $j;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
$disk->{'type'} = 'scsi';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ide\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
|
||||
# New-style IDE specification
|
||||
$disk->{'host'} = $2;
|
||||
$disk->{'bus'} = $3;
|
||||
$disk->{'target'} = $4;
|
||||
$disk->{'lun'} = $5;
|
||||
$disk->{'desc'} = &text('select_newide',
|
||||
"$2", "$3", "$4", "$5");
|
||||
$disk->{'type'} = 'ide';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(rd\/c(\d+)d(\d+))/) {
|
||||
# Mylex raid device
|
||||
local ($mc, $md) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_mylex', $mc, $md);
|
||||
open(RD, "</proc/rd/c$mc/current_status");
|
||||
while(<RD>) {
|
||||
if (/^Configuring\s+(.*)/i) {
|
||||
$disk->{'model'} = $1;
|
||||
}
|
||||
elsif (/\s+(\S+):\s+([^, ]+)/ &&
|
||||
$1 eq $disk->{'device'}) {
|
||||
$disk->{'raid'} = $2;
|
||||
}
|
||||
}
|
||||
close(RD);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ida\/c(\d+)d(\d+))/) {
|
||||
# Compaq RAID device
|
||||
local ($ic, $id) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_cpq', $ic, $id);
|
||||
open(IDA, -d "/proc/driver/array" ? "</proc/driver/array/ida$ic" : "</proc/driver/cpqarray/ida$ic");
|
||||
while(<IDA>) {
|
||||
if (/^(\S+):\s+(.*)/ && $1 eq "ida$ic") {
|
||||
$disk->{'model'} = $2;
|
||||
}
|
||||
}
|
||||
close(IDA);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(cciss\/c(\d+)d(\d+))/) {
|
||||
# Compaq Smart Array RAID
|
||||
local ($ic, $id) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_smart', $ic, $id);
|
||||
open(CCI, "</proc/driver/cciss/cciss$ic");
|
||||
while(<CCI>) {
|
||||
if (/^\s*(\S+):\s*(.*)/ && $1 eq "cciss$ic") {
|
||||
$disk->{'model'} = $2;
|
||||
}
|
||||
}
|
||||
close(CCI);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ataraid\/disc(\d+)\/disc)/) {
|
||||
# Promise RAID controller
|
||||
local $dd = $2;
|
||||
$disk->{'desc'} = &text('select_promise', $dd);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/nvme(\d+)n(\d+)$/) {
|
||||
# NVME SSD controller
|
||||
$disk->{'desc'} = &text('select_nvme', "$1", "$2");
|
||||
$disk->{'type'} = 'scsi';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
|
||||
# Work out short name, like sda
|
||||
local $short;
|
||||
if (defined($disk->{'host'})) {
|
||||
$short = &hbt_to_device($disk->{'host'},
|
||||
$disk->{'bus'},
|
||||
$disk->{'target'});
|
||||
}
|
||||
else {
|
||||
$short = $disk->{'device'};
|
||||
$short =~ s/^.*\///g;
|
||||
}
|
||||
$disk->{'short'} = $short;
|
||||
|
||||
$disk->{'id'} = $id_map{$disk->{'device'}} ||
|
||||
$id_map{"/dev/$short"};
|
||||
$disk->{'ids'} = $all_id_map{$disk->{'device'}} ||
|
||||
$all_id_map{"/dev/$short"};
|
||||
};
|
||||
my $enrich_metadata = sub {
|
||||
local @disks = @_;
|
||||
local $d;
|
||||
# Check /proc/ide for IDE disk models
|
||||
foreach $d (@disks) {
|
||||
if ($d->{'type'} eq 'ide') {
|
||||
local $short = $d->{'short'};
|
||||
$d->{'model'} = &read_file_contents("/proc/ide/$short/model");
|
||||
$d->{'model'} =~ s/\r|\n//g;
|
||||
$d->{'media'} = &read_file_contents("/proc/ide/$short/media");
|
||||
$d->{'media'} =~ s/\r|\n//g;
|
||||
if ($d->{'short'} =~ /^vd/ && !$d->{'model'}) {
|
||||
# Fake up model for KVM VirtIO disks
|
||||
$d->{'model'} = "KVM VirtIO";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Fill in SCSI information
|
||||
foreach $d (@disks) {
|
||||
if ($d->{'type'} eq 'scsi') {
|
||||
local $s = $d->{'scsi'};
|
||||
local $sysdir = "/sys/block/$d->{'short'}/device";
|
||||
if (-d $sysdir) {
|
||||
# From kernel 2.6.30+ sys directory
|
||||
$d->{'model'} = &read_file_contents("$sysdir/vendor").
|
||||
" ".
|
||||
&read_file_contents("$sysdir/model");
|
||||
$d->{'model'} =~ s/\r|\n//g;
|
||||
$d->{'media'} = &read_file_contents("$sysdir/media");
|
||||
$d->{'media'} =~ s/\r|\n//g;
|
||||
}
|
||||
elsif ($dscsi_mode) {
|
||||
# From other scsi files
|
||||
$d->{'model'} = "$dscsi[$s]->{'make'} $dscsi[$s]->{'model'}";
|
||||
$d->{'controller'} = $dscsi[$s]->{'host'};
|
||||
$d->{'scsiid'} = $dscsi[$s]->{'target'};
|
||||
}
|
||||
else {
|
||||
# From /proc/scsi/scsi lines
|
||||
if ($pscsi[$s] =~ /Vendor:\s+(\S+).*Model:\s+(.*)\s+Rev:/i) {
|
||||
$d->{'model'} = "$1 $2";
|
||||
}
|
||||
if ($pscsi[$s] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i) {
|
||||
$d->{'controller'} = int($1);
|
||||
$d->{'scsiid'} = int($2);
|
||||
}
|
||||
}
|
||||
if ($d->{'model'} =~ /ATA/) {
|
||||
# Fake SCSI disk, actually IDE
|
||||
$d->{'scsi'} = 0;
|
||||
$d->{'desc'} =~ s/SCSI/SATA/g;
|
||||
foreach my $p (@{$d->{'parts'} || []}) {
|
||||
$p->{'desc'} =~ s/SCSI/SATA/g;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if ($disk_only) {
|
||||
my @inventory = @devs;
|
||||
foreach my $device (@inventory) {
|
||||
my $disk = { 'device' => $device, 'prefix' => $device };
|
||||
$set_metadata->($disk);
|
||||
push(@disks, $disk);
|
||||
}
|
||||
$enrich_metadata->(@disks);
|
||||
foreach my $disk (@disks) {
|
||||
my $device = $disk->{'device'};
|
||||
my $identity = &disk_discovery_identity($device);
|
||||
return &disk_discovery_error("Missing device during disk-only discovery: $device", $error_ref)
|
||||
if (!defined($identity));
|
||||
return &disk_discovery_error("Disk identity changed during disk-only discovery: $device", $error_ref)
|
||||
if ($discovery_identity{$device} ne $identity);
|
||||
# An absent controller model must not silently bypass RAID expansion.
|
||||
return &disk_discovery_error("Missing disk model during disk-only discovery: $device", $error_ref)
|
||||
if (($disk->{'type'} eq 'scsi' || $disk->{'type'} eq 'raid') &&
|
||||
$disk->{'model'} !~ /\S/);
|
||||
}
|
||||
return &disk_discovery_error("Disk inventory changed during disk-only discovery", $error_ref)
|
||||
if ($discovery_partitions ne &read_file_contents('/proc/partitions'));
|
||||
# These are metadata records, not claims of empty partition tables.
|
||||
# Do not read or populate the full partition cache.
|
||||
return @disks;
|
||||
}
|
||||
|
||||
# Call fdisk to get partition and geometry information
|
||||
local $qdevs = join(" ", map { quotemeta($_) } @devs);
|
||||
local ($disk, $m2);
|
||||
@@ -565,7 +297,154 @@ while(<FDISK>) {
|
||||
|
||||
local @st = stat($disk->{'device'});
|
||||
next if (@cdstat && $st[1] == $cdstat[1]);
|
||||
$set_metadata->($disk);
|
||||
if ($disk->{'device'} =~ /\/sd([a-z]+)$/) {
|
||||
# Old-style SCSI disk
|
||||
$disk->{'desc'} = &text('select_device', 'SCSI',
|
||||
uc($1));
|
||||
local ($dscsi) = grep { $_->{'dev'} eq "sd$1" } @dscsi;
|
||||
$disk->{'scsi'} = $dscsi ? &indexof($dscsi, @dscsi)
|
||||
: ord(uc($1))-65;
|
||||
$disk->{'type'} = 'scsi';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/hd([a-z]+)$/) {
|
||||
# IDE disk
|
||||
$disk->{'desc'} = &text('select_device', 'IDE', uc($1));
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/xvd([a-z]+)$/) {
|
||||
# Xen virtual disk
|
||||
$disk->{'desc'} = &text('select_device', 'Xen', uc($1));
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/mmcblk([0-9]+)$/) {
|
||||
# SD-card / MMC
|
||||
$disk->{'desc'} = &text('select_device', 'SD-Card', $1);
|
||||
$disk->{'type'} = 'ide';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/vd([a-z]+)$/) {
|
||||
# KVM virtual disk
|
||||
$disk->{'desc'} = &text('select_device',
|
||||
'VirtIO', uc($1));
|
||||
$disk->{'type'} = 'virtio';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(scsi\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
|
||||
# New complete SCSI disk specification
|
||||
$disk->{'host'} = $2;
|
||||
$disk->{'bus'} = $3;
|
||||
$disk->{'target'} = $4;
|
||||
$disk->{'lun'} = $5;
|
||||
$disk->{'desc'} = &text('select_scsi',
|
||||
"$2", "$3", "$4", "$5");
|
||||
|
||||
# Work out the SCSI index for this disk
|
||||
local $j;
|
||||
if ($dscsi_mode) {
|
||||
for($j=0; $j<@dscsi; $j++) {
|
||||
if ($dscsi[$j]->{'host'} == $disk->{'host'} && $dscsi[$j]->{'bus'} == $disk->{'bus'} && $dscsi[$j]->{'target'} == $disk->{'target'} && $dscsi[$j]->{'lnun'} == $disk->{'lun'}) {
|
||||
$disk->{'scsi'} = $j;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for($j=0; $j<@pscsi; $j++) {
|
||||
if ($pscsi[$j] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i && $disk->{'host'} == $1 && $disk->{'target'} == $2) {
|
||||
$disk->{'scsi'} = $j;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
$disk->{'type'} = 'scsi';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ide\/host(\d+)\/bus(\d+)\/target(\d+)\/lun(\d+)\/disc)/) {
|
||||
# New-style IDE specification
|
||||
$disk->{'host'} = $2;
|
||||
$disk->{'bus'} = $3;
|
||||
$disk->{'target'} = $4;
|
||||
$disk->{'lun'} = $5;
|
||||
$disk->{'desc'} = &text('select_newide',
|
||||
"$2", "$3", "$4", "$5");
|
||||
$disk->{'type'} = 'ide';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(rd\/c(\d+)d(\d+))/) {
|
||||
# Mylex raid device
|
||||
local ($mc, $md) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_mylex', $mc, $md);
|
||||
open(RD, "</proc/rd/c$mc/current_status");
|
||||
while(<RD>) {
|
||||
if (/^Configuring\s+(.*)/i) {
|
||||
$disk->{'model'} = $1;
|
||||
}
|
||||
elsif (/\s+(\S+):\s+([^, ]+)/ &&
|
||||
$1 eq $disk->{'device'}) {
|
||||
$disk->{'raid'} = $2;
|
||||
}
|
||||
}
|
||||
close(RD);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ida\/c(\d+)d(\d+))/) {
|
||||
# Compaq RAID device
|
||||
local ($ic, $id) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_cpq', $ic, $id);
|
||||
open(IDA, -d "/proc/driver/array" ? "</proc/driver/array/ida$ic" : "</proc/driver/cpqarray/ida$ic");
|
||||
while(<IDA>) {
|
||||
if (/^(\S+):\s+(.*)/ && $1 eq "ida$ic") {
|
||||
$disk->{'model'} = $2;
|
||||
}
|
||||
}
|
||||
close(IDA);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(cciss\/c(\d+)d(\d+))/) {
|
||||
# Compaq Smart Array RAID
|
||||
local ($ic, $id) = ($2, $3);
|
||||
$disk->{'desc'} = &text('select_smart', $ic, $id);
|
||||
open(CCI, "</proc/driver/cciss/cciss$ic");
|
||||
while(<CCI>) {
|
||||
if (/^\s*(\S+):\s*(.*)/ && $1 eq "cciss$ic") {
|
||||
$disk->{'model'} = $2;
|
||||
}
|
||||
}
|
||||
close(CCI);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/(ataraid\/disc(\d+)\/disc)/) {
|
||||
# Promise RAID controller
|
||||
local $dd = $2;
|
||||
$disk->{'desc'} = &text('select_promise', $dd);
|
||||
$disk->{'type'} = 'raid';
|
||||
$disk->{'prefix'} =~ s/disc$/part/g;
|
||||
}
|
||||
elsif ($disk->{'device'} =~ /\/nvme(\d+)n(\d+)$/) {
|
||||
# NVME SSD controller
|
||||
$disk->{'desc'} = &text('select_nvme', "$1", "$2");
|
||||
$disk->{'type'} = 'scsi';
|
||||
$disk->{'prefix'} = $disk->{'device'}.'p';
|
||||
}
|
||||
|
||||
# Work out short name, like sda
|
||||
local $short;
|
||||
if (defined($disk->{'host'})) {
|
||||
$short = &hbt_to_device($disk->{'host'},
|
||||
$disk->{'bus'},
|
||||
$disk->{'target'});
|
||||
}
|
||||
else {
|
||||
$short = $disk->{'device'};
|
||||
$short =~ s/^.*\///g;
|
||||
}
|
||||
$disk->{'short'} = $short;
|
||||
|
||||
$disk->{'id'} = $id_map{$disk->{'device'}} ||
|
||||
$id_map{"/dev/$short"};
|
||||
$disk->{'ids'} = $all_id_map{$disk->{'device'}} ||
|
||||
$all_id_map{"/dev/$short"};
|
||||
|
||||
push(@disks, $disk);
|
||||
}
|
||||
@@ -721,40 +600,66 @@ while(<FDISK>) {
|
||||
}
|
||||
close(FDISK);
|
||||
|
||||
$enrich_metadata->(@disks);
|
||||
# Check /proc/ide for IDE disk models
|
||||
foreach $d (@disks) {
|
||||
if ($d->{'type'} eq 'ide') {
|
||||
local $short = $d->{'short'};
|
||||
$d->{'model'} = &read_file_contents("/proc/ide/$short/model");
|
||||
$d->{'model'} =~ s/\r|\n//g;
|
||||
$d->{'media'} = &read_file_contents("/proc/ide/$short/media");
|
||||
$d->{'media'} =~ s/\r|\n//g;
|
||||
if ($d->{'short'} =~ /^vd/ && !$d->{'model'}) {
|
||||
# Fake up model for KVM VirtIO disks
|
||||
$d->{'model'} = "KVM VirtIO";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Fill in SCSI information
|
||||
foreach $d (@disks) {
|
||||
if ($d->{'type'} eq 'scsi') {
|
||||
local $s = $d->{'scsi'};
|
||||
local $sysdir = "/sys/block/$d->{'short'}/device";
|
||||
if (-d $sysdir) {
|
||||
# From kernel 2.6.30+ sys directory
|
||||
$d->{'model'} = &read_file_contents("$sysdir/vendor").
|
||||
" ".
|
||||
&read_file_contents("$sysdir/model");
|
||||
$d->{'model'} =~ s/\r|\n//g;
|
||||
$d->{'media'} = &read_file_contents("$sysdir/media");
|
||||
$d->{'media'} =~ s/\r|\n//g;
|
||||
}
|
||||
elsif ($dscsi_mode) {
|
||||
# From other scsi files
|
||||
$d->{'model'} = "$dscsi[$s]->{'make'} $dscsi[$s]->{'model'}";
|
||||
$d->{'controller'} = $dscsi[$s]->{'host'};
|
||||
$d->{'scsiid'} = $dscsi[$s]->{'target'};
|
||||
}
|
||||
else {
|
||||
# From /proc/scsi/scsi lines
|
||||
if ($pscsi[$s] =~ /Vendor:\s+(\S+).*Model:\s+(.*)\s+Rev:/i) {
|
||||
$d->{'model'} = "$1 $2";
|
||||
}
|
||||
if ($pscsi[$s] =~ /Host:\s+scsi(\d+).*Id:\s+(\d+)/i) {
|
||||
$d->{'controller'} = int($1);
|
||||
$d->{'scsiid'} = int($2);
|
||||
}
|
||||
}
|
||||
if ($d->{'model'} =~ /ATA/) {
|
||||
# Fake SCSI disk, actually IDE
|
||||
$d->{'scsi'} = 0;
|
||||
$d->{'desc'} =~ s/SCSI/SATA/g;
|
||||
foreach my $p (@{$d->{'parts'}}) {
|
||||
$p->{'desc'} =~ s/SCSI/SATA/g;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@list_disks_partitions_cache = @disks;
|
||||
return @disks;
|
||||
}
|
||||
|
||||
# Report disk-only failure without changing the list-returning caller contract.
|
||||
sub disk_discovery_error
|
||||
{
|
||||
my ($message, $error_ref) = @_;
|
||||
if ($error_ref) {
|
||||
$$error_ref = $message;
|
||||
}
|
||||
else {
|
||||
warn "$message\n";
|
||||
}
|
||||
return ();
|
||||
}
|
||||
|
||||
# Compare device-node and sysfs object identity without opening the device.
|
||||
# This detects disappearance/replacement during enumeration, not all possible
|
||||
# changes after this function returns. The next collection always scans again.
|
||||
sub disk_discovery_identity
|
||||
{
|
||||
my ($device) = @_;
|
||||
my @node = stat($device);
|
||||
return if (!@node);
|
||||
my $short = $device;
|
||||
$short =~ s{^/dev/}{};
|
||||
$short =~ s{/}{!}g;
|
||||
my @sys = stat("/sys/class/block/$short");
|
||||
return join(':', @node[0,1,6], @sys ? @sys[0,1] : ('', ''));
|
||||
}
|
||||
|
||||
# partition_description(device)
|
||||
# Converts a device path like /dev/hda1 into a human-readable name
|
||||
sub partition_description
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Disk-only discovery tests
|
||||
|
||||
Run `prove -lr fdisk/t`. Perl::Critic enables lint checks for the added tests.
|
||||
Fixtures use temporary kernel/device trees and fake partition commands; they
|
||||
never query real disks. Coverage includes GPT/MBR, fdisk fallback, cache isolation,
|
||||
fresh inventory and identifiers, disappearance, invalid/empty inventory, and
|
||||
non-throwing error reporting, controller-qualified sysfs replacement with a retained
|
||||
device node, and canonical identities through symlinked temporary parents. Physical
|
||||
RAID and real hotplug need hardware tests.
|
||||
@@ -1,16 +0,0 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use FindBin qw($Bin);
|
||||
BEGIN {
|
||||
eval { require Perl::Critic; 1 }
|
||||
or plan skip_all => 'Perl::Critic not installed';
|
||||
}
|
||||
# Apply the existing policy to new tests, not untouched legacy module files.
|
||||
my $critic = Perl::Critic->new(-profile => "$Bin/../../.perlcriticrc");
|
||||
for my $file (qw(disk-discovery.t fixtures/disk-discovery.pl disk-discovery-perlcritic.t)) {
|
||||
my @violations = $critic->critique("$Bin/$file");
|
||||
is(scalar @violations, 0, "$file perlcritic");
|
||||
diag join('', @violations) if @violations;
|
||||
}
|
||||
done_testing();
|
||||
@@ -1,223 +0,0 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use JSON::PP;
|
||||
use File::Temp qw(tempdir);
|
||||
use File::Path qw(make_path remove_tree);
|
||||
use File::Basename qw(dirname);
|
||||
use Cwd qw(abs_path);
|
||||
use IPC::Open3;
|
||||
use Symbol qw(gensym);
|
||||
|
||||
my $here = abs_path(dirname(__FILE__));
|
||||
my $library = "$here/../fdisk-lib.pl";
|
||||
my $fixture;
|
||||
|
||||
sub write_file {
|
||||
my ($name, $value) = @_;
|
||||
make_path(dirname("$fixture/$name"));
|
||||
open(my $fh, '>', "$fixture/$name") or die $!;
|
||||
print {$fh} $value;
|
||||
close($fh) or die $!;
|
||||
}
|
||||
sub disk {
|
||||
my ($name, $vendor, $model) = @_;
|
||||
write_file("dev/$name", '');
|
||||
write_file("sys/block/$name/device/vendor", "$vendor\n");
|
||||
write_file("sys/block/$name/device/model", "$model\n");
|
||||
make_path("$fixture/sys/class/block/$name");
|
||||
}
|
||||
sub setup_fixture {
|
||||
my ($parent) = @_;
|
||||
$fixture = abs_path(tempdir(CLEANUP => 1, defined($parent) ? (DIR => $parent) : ()));
|
||||
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n 8 1 90 sda1\n");
|
||||
write_file('proc/scsi/scsi', "Attached devices:\n");
|
||||
disk('sda', 'Example', 'USB NVMe');
|
||||
disk('sdb', 'ATA', 'second');
|
||||
make_path("$fixture/dev/disk/by-id");
|
||||
symlink('../../sda', "$fixture/dev/disk/by-id/old") or die $!;
|
||||
write_file('tool-output', '');
|
||||
for my $tool (qw(parted fdisk)) {
|
||||
write_file("bin/$tool", "#!$^X\n" . <<'TOOL');
|
||||
use strict;
|
||||
my $root = $ENV{'FIXTURE_ROOT'};
|
||||
open(my $calls, '>>', "$root/calls") or die $!;
|
||||
print {$calls} "query\n";
|
||||
close($calls);
|
||||
open(my $data, '<', "$root/tool-output") or die $!;
|
||||
print while <$data>;
|
||||
TOOL
|
||||
chmod(0755, "$fixture/bin/$tool") or die $!;
|
||||
}
|
||||
}
|
||||
sub run_library {
|
||||
my ($scenario) = @_;
|
||||
local $ENV{'PATH'} = "$fixture/bin:$ENV{'PATH'}";
|
||||
local $ENV{'FIXTURE_ROOT'} = $fixture;
|
||||
local $SIG{'ALRM'} = sub { die "fixture timed out\n"; };
|
||||
alarm(15);
|
||||
my $err = gensym();
|
||||
my $pid = open3(my $in, my $out, $err, $^X, "$here/fixtures/disk-discovery.pl",
|
||||
$library, $fixture, $scenario || 'disk');
|
||||
close($in);
|
||||
my ($stdout, $stderr);
|
||||
{ local $/; $stdout = <$out>; $stderr = <$err>; }
|
||||
waitpid($pid, 0);
|
||||
my $status = $?;
|
||||
alarm(0);
|
||||
is($status, 0, 'fixture process succeeds') or diag($stderr);
|
||||
is($stderr, '', 'no unexpected warnings');
|
||||
my $result = decode_json($stdout);
|
||||
is($result->{'exception'}, undef, 'library does not throw an exception');
|
||||
return $result;
|
||||
}
|
||||
sub parted_output {
|
||||
my ($table) = @_;
|
||||
my $part = $table eq 'gpt' ? ' 1 1cyl 9cyl 8cyl ext4 name' : ' 1 1cyl 9cyl 8cyl primary ext4';
|
||||
my $extra = $table eq 'msdos' ? " 2 9cyl 10cyl 1cyl extended\n" : '';
|
||||
write_file('tool-output', "Disk $fixture/dev/sda: 10cyl\nBIOS cylinder,head,sector geometry: 10,4,8. Each cylinder is 16384b.\nPartition Table: $table\n$part\n$extra");
|
||||
}
|
||||
|
||||
subtest 'disk-only inventory avoids partition commands and cache' => sub {
|
||||
setup_fixture();
|
||||
my $r = run_library();
|
||||
is($r->{'error'}, undef, 'discovery succeeds');
|
||||
my $d = $r->{'results'}->[0]->[0];
|
||||
is($d->{'model'}, 'Example USB NVMe', 'model retained');
|
||||
is_deeply($d->{'ids'}, ['old'], 'stable ID retained');
|
||||
ok(!exists($d->{'parts'}) && !exists($d->{'table'}) && !exists($d->{'size'}),
|
||||
'metadata does not claim partition or geometry data');
|
||||
is_deeply($r->{'cache'}, [], 'full partition cache not populated');
|
||||
ok(!-e "$fixture/calls", 'no partition tool called');
|
||||
};
|
||||
subtest 'full discovery still parses GPT, MBR and extended partitions' => sub {
|
||||
for my $table (qw(gpt msdos)) {
|
||||
setup_fixture(); parted_output($table);
|
||||
my $r = run_library('full');
|
||||
is($r->{'error'}, undef, 'full discovery succeeds');
|
||||
my $d = $r->{'results'}->[0]->[0];
|
||||
is($d->{'table'}, $table, 'partition table retained');
|
||||
is(scalar(@{$d->{'parts'}}), $table eq 'gpt' ? 1 : 2, 'partitions retained');
|
||||
is($d->{'model'}, 'Example USB NVMe', 'shared model metadata retained');
|
||||
ok(-e "$fixture/calls", 'full path queries partition tool');
|
||||
}
|
||||
};
|
||||
subtest 'full fdisk fallback remains available' => sub {
|
||||
setup_fixture();
|
||||
write_file('tool-output', "Disk $fixture/dev/sda: 1 GiB, 1073741824 bytes, 2097152 sectors\nGeometry: 4 heads, 8 sectors/track, 10 cylinders\nUnits = cylinders of 32 * 512\nDisklabel type: gpt\n$fixture/dev/sda1 1 9 9 4K Linux filesystem\n");
|
||||
my $r = run_library('full_fdisk');
|
||||
is($r->{'error'}, undef, 'fdisk fallback succeeds');
|
||||
is(scalar(@{$r->{'results'}->[0]->[0]->{'parts'}}), 1, 'fdisk partition retained');
|
||||
ok(-e "$fixture/calls", 'fdisk command used');
|
||||
};
|
||||
subtest 'cache isolation in both call orders' => sub {
|
||||
for my $scenario (qw(disk_full full_disk)) {
|
||||
setup_fixture(); parted_output('gpt');
|
||||
my $r = run_library($scenario);
|
||||
my ($disk, $full) = @{$r->{'results'}};
|
||||
($disk, $full) = ($full, $disk) if $scenario eq 'full_disk';
|
||||
ok(!exists($disk->[0]->{'parts'}), 'disk-only result has no partition claim');
|
||||
ok(@{$full->[0]->{'parts'}}, 'full result has partitions');
|
||||
is_deeply($r->{'cache'}, $full, 'full cache contains only full result');
|
||||
}
|
||||
};
|
||||
subtest 'replacement refreshes model and identifiers' => sub {
|
||||
setup_fixture(); my $r = run_library('refresh');
|
||||
my ($a, $b) = map { $_->[0] } @{$r->{'results'}};
|
||||
is($a->{'model'}, 'Example USB NVMe', 'first model');
|
||||
is($b->{'model'}, 'Example Replacement', 'new model');
|
||||
is_deeply($b->{'ids'}, ['new'], 'new stable ID');
|
||||
};
|
||||
subtest 'addition and removal refresh inventory' => sub {
|
||||
setup_fixture(); my $r = run_library('remove_add');
|
||||
is($r->{'results'}->[0]->[0]->{'device'}, "$fixture/dev/sda", 'initial disk');
|
||||
is($r->{'results'}->[1]->[0]->{'device'}, "$fixture/dev/sdb", 'replacement inventory');
|
||||
};
|
||||
subtest 'disappearance during scan fails explicitly' => sub {
|
||||
setup_fixture(); like(run_library('race')->{'error'}, qr/Missing device/, 'no stale result');
|
||||
};
|
||||
subtest 'missing model and missing inventory fail explicitly' => sub {
|
||||
setup_fixture();
|
||||
unlink("$fixture/sys/block/sda/device/vendor", "$fixture/sys/block/sda/device/model");
|
||||
like(run_library()->{'error'}, qr/Missing disk model/, 'missing model fails');
|
||||
unlink("$fixture/proc/partitions");
|
||||
like(run_library()->{'error'}, qr/Cannot open disk inventory/, 'missing inventory fails');
|
||||
};
|
||||
subtest 'multiple drives and namespaces retain classifications' => sub {
|
||||
setup_fixture();
|
||||
my @names = qw(sda sdaa nvme0n1 nvme0n2 mmcblk0 xvda vda hda);
|
||||
write_file('proc/partitions', "major minor #blocks name\n" . join('', map { "8 0 100 $_\n" } @names));
|
||||
write_file('proc/ide/hda/media', "disk\n"); write_file('proc/ide/hda/model', "IDE fixture\n");
|
||||
disk($_, 'Example', 'disk') for @names;
|
||||
my $r = run_library();
|
||||
is($r->{'error'}, undef, 'multi-drive discovery succeeds');
|
||||
is_deeply([sort map { $_->{'short'} } @{$r->{'results'}->[0]}], [sort @names], 'all devices retained');
|
||||
my ($virtio) = grep { $_->{'short'} eq 'vda' } @{$r->{'results'}->[0]};
|
||||
is($virtio->{'type'}, 'virtio', 'VirtIO classification preserved');
|
||||
ok(!-e "$fixture/calls", 'multiple drives need no partition query');
|
||||
};
|
||||
subtest 'unpartitioned disk without stable link remains discoverable' => sub {
|
||||
setup_fixture(); unlink("$fixture/dev/disk/by-id/old");
|
||||
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n");
|
||||
my $r = run_library();
|
||||
is(scalar(@{$r->{'results'}->[0]}), 1, 'disk retained');
|
||||
is($r->{'results'}->[0]->[0]->{'ids'}, undef, 'no fabricated identifiers');
|
||||
};
|
||||
subtest 'legacy controller metadata and SCSI fallback remain available' => sub {
|
||||
setup_fixture();
|
||||
my @names = qw(cciss/c0d0 rd/c0d0 ida/c0d0);
|
||||
write_file('proc/partitions', "major minor #blocks name\n" . join('', map { "8 0 100 $_\n" } @names));
|
||||
write_file("dev/$_", '') for @names;
|
||||
write_file('proc/driver/cciss/cciss0', "cciss0: Smart Array\n");
|
||||
write_file('proc/rd/c0/current_status', "Configuring Mylex\n");
|
||||
write_file('proc/driver/cpqarray/ida0', "ida0: Compaq\n");
|
||||
my $r = run_library();
|
||||
is_deeply([map { $_->{'model'} } @{$r->{'results'}->[0]}], ['Smart Array','Mylex','Compaq'], 'controller models retained');
|
||||
ok(!grep({ $_->{'type'} ne 'raid' } @{$r->{'results'}->[0]}), 'legacy RAID classification retained');
|
||||
write_file('proc/partitions', "major minor #blocks name\n 8 0 100 sda\n");
|
||||
remove_tree("$fixture/sys/block/sda/device");
|
||||
write_file('proc/scsi/scsi', "Attached devices:\nHost: scsi0 Channel: 00 Id: 00 Lun: 00\n Vendor: LSI Model: RAID fixture Rev: 1\n Type: Direct-Access\n");
|
||||
like(run_library()->{'results'}->[0]->[0]->{'model'}, qr/LSI/, 'proc SCSI fallback retained');
|
||||
};
|
||||
subtest 'failure reporting preserves the list-returning contract' => sub {
|
||||
setup_fixture();
|
||||
unlink("$fixture/dev/sda");
|
||||
my $r = run_library('warn');
|
||||
is_deeply($r->{'results'}, [[]], 'unreported-reference failure returns no partial disks');
|
||||
is(scalar @{$r->{'warnings'}}, 1, 'caller without error reference receives one warning');
|
||||
like($r->{'warnings'}->[0], qr/Missing device/, 'warning explains the failure');
|
||||
setup_fixture();
|
||||
is(run_library('reset_error')->{'error'}, undef, 'successful disk-only call clears stale error');
|
||||
write_file('proc/partitions', "invalid inventory\n");
|
||||
like(run_library()->{'error'}, qr/Invalid disk inventory/, 'invalid header is reported');
|
||||
write_file('proc/partitions', "major minor #blocks name\n");
|
||||
$r = run_library();
|
||||
is_deeply($r->{'results'}, [[]], 'empty valid inventory succeeds');
|
||||
is($r->{'error'}, undef, 'empty valid inventory is not an error');
|
||||
};
|
||||
subtest 'controller-qualified sysfs replacement is detected with a retained device node' => sub {
|
||||
setup_fixture();
|
||||
write_file('proc/partitions', "major minor #blocks name\n 104 0 100 cciss/c0d0\n");
|
||||
write_file('dev/cciss/c0d0', '');
|
||||
write_file('proc/driver/cciss/cciss0', "cciss0: Smart Array\n");
|
||||
make_path("$fixture/sys/class/block/cciss!c0d0");
|
||||
my $stable = run_library('legacy_stable');
|
||||
is($stable->{'error'}, undef, 'controller sysfs path is accepted');
|
||||
my $before = [stat("$fixture/dev/cciss/c0d0")];
|
||||
my $changed = run_library('legacy_replace');
|
||||
like($changed->{'error'}, qr/Disk identity changed/, 'sysfs replacement is detected');
|
||||
is_deeply($changed->{'results'}, [[]], 'replacement returns no stale inventory');
|
||||
my $after = [stat("$fixture/dev/cciss/c0d0")];
|
||||
is_deeply([@{$after}[0,1,6]], [@{$before}[0,1,6]], 'device-node identity was retained');
|
||||
};
|
||||
subtest 'symlinked temporary parents produce canonical fixture identities' => sub {
|
||||
my $parent = abs_path(tempdir(CLEANUP => 1));
|
||||
make_path("$parent/real");
|
||||
symlink("$parent/real", "$parent/alias") or die $!;
|
||||
setup_fixture("$parent/alias");
|
||||
is($fixture, abs_path($fixture), 'fixture root is canonical');
|
||||
my $result = run_library();
|
||||
is($result->{'error'}, undef, 'discovery succeeds through symlinked parent');
|
||||
is_deeply($result->{'results'}->[0]->[0]->{'ids'}, ['old'], 'resolved stable IDs match canonical devices');
|
||||
};
|
||||
done_testing();
|
||||
@@ -1,90 +0,0 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
use JSON::PP;
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(dirname);
|
||||
my ($source, $root, $scenario) = @ARGV;
|
||||
open(my $fh, '<', $source) or die $!;
|
||||
local $/; my $code = <$fh>; close($fh); $/ = "\n";
|
||||
# Keep the actual library functions; replace literal filesystem roots only.
|
||||
$code = substr($code, index($code, 'sub list_disks_partitions'));
|
||||
for my $prefix ('/proc/', '/sys/', '/dev/') {
|
||||
$code =~ s/\Q$prefix\E/$root$prefix/g;
|
||||
}
|
||||
our (@reads, $has_parted, @list_disks_partitions_cache, $change);
|
||||
$has_parted = $scenario !~ /fdisk/;
|
||||
# Mock the cross-module filesystem helper used by the real library.
|
||||
{ package mount; ## no critic (Modules::RequireFilenameMatchesPackage)
|
||||
sub list_fstypes { return ('ext2', 'ext3', 'ext4', 'xfs'); }
|
||||
}
|
||||
sub text { return join(' ', @_); }
|
||||
sub indexof { my ($x, @v) = @_; for (0..$#v) { return $_ if $v[$_] eq $x; } return -1; }
|
||||
sub resolve_links { return abs_path($_[0]); }
|
||||
sub simplify_path { my $p = $_[0]; $p =~ s{/dev/disk/by-id/../../}{/dev/}; return $p; }
|
||||
sub read_file_contents {
|
||||
my ($p) = @_;
|
||||
die "Attempt to read device data" if $p =~ m{\Q$root\E/dev/};
|
||||
push(@reads, $p);
|
||||
open(my $f, '<', $p) or return '';
|
||||
local $/; my $s = <$f>; close($f);
|
||||
if ($change && $p =~ m{/device/model$}) {
|
||||
unlink("$root/dev/sda");
|
||||
$change = 0;
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
{
|
||||
# Load the real legacy functions, which use Webmin package globals.
|
||||
no strict 'vars'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
|
||||
# Webmin does not enable these warnings for optional legacy metadata.
|
||||
no warnings 'uninitialized';
|
||||
eval $code; ## no critic (BuiltinFunctions::ProhibitStringyEval)
|
||||
die $@ if $@;
|
||||
}
|
||||
if ($scenario eq 'legacy_replace') {
|
||||
my $identity = \&disk_discovery_identity;
|
||||
my $calls = 0;
|
||||
no warnings 'redefine';
|
||||
*disk_discovery_identity = sub {
|
||||
if (++$calls == 2) {
|
||||
my $sys = "$root/sys/class/block/cciss!c0d0";
|
||||
rename($sys, "$sys.old") or die $!;
|
||||
mkdir($sys) or die $!;
|
||||
}
|
||||
return $identity->(@_);
|
||||
};
|
||||
}
|
||||
my @results;
|
||||
my $error;
|
||||
my $exception;
|
||||
my @warnings;
|
||||
eval {
|
||||
if ($scenario eq 'race') { $change = 1; }
|
||||
if ($scenario eq 'reset_error') { $error = 'previous failure'; }
|
||||
if ($scenario eq 'disk_full' || $scenario eq 'full_disk') {
|
||||
my $first = $scenario eq 'disk_full' ? 1 : 0;
|
||||
push(@results, [list_disks_partitions(undef, $first, \$error)]);
|
||||
push(@results, [list_disks_partitions(undef, !$first, \$error)]);
|
||||
}
|
||||
elsif ($scenario eq 'refresh') {
|
||||
push(@results, [list_disks_partitions(undef, 1, \$error)]);
|
||||
open(my $f, '>', "$root/sys/block/sda/device/model") or die $!;
|
||||
print $f "Replacement\n"; close($f);
|
||||
unlink("$root/dev/disk/by-id/old");
|
||||
symlink('../../sda', "$root/dev/disk/by-id/new") or die $!;
|
||||
push(@results, [list_disks_partitions(undef, 1, \$error)]);
|
||||
}
|
||||
elsif ($scenario eq 'remove_add') {
|
||||
push(@results, [list_disks_partitions(undef, 1, \$error)]);
|
||||
open(my $f, '>', "$root/proc/partitions") or die $!;
|
||||
print $f "major minor #blocks name\n 8 16 100 sdb\n"; close($f);
|
||||
push(@results, [list_disks_partitions(undef, 1, \$error)]);
|
||||
}
|
||||
elsif ($scenario eq 'warn') {
|
||||
local $SIG{__WARN__} = sub { push(@warnings, @_); };
|
||||
push(@results, [list_disks_partitions(undef, 1)]);
|
||||
}
|
||||
else { push(@results, [list_disks_partitions(undef, $scenario =~ /^full/ ? 0 : 1, \$error)]); }
|
||||
1;
|
||||
} or $exception = $@;
|
||||
print JSON::PP->new->canonical->encode({results=>\@results, error=>$error, exception=>$exception, warnings=>\@warnings, reads=>\@reads, cache=>\@list_disks_partitions_cache});
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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 ".
|
||||
|
||||
@@ -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/) {
|
||||
|
||||
BIN
filemin/images/icons/quick/acls.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
filemin/images/icons/quick/bookmark-new.png
Normal file
|
After Width: | Height: | Size: 466 B |
BIN
filemin/images/icons/quick/browse.png
Normal file
|
After Width: | Height: | Size: 944 B |
BIN
filemin/images/icons/quick/chattr.png
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
filemin/images/icons/quick/chcon.png
Normal file
|
After Width: | Height: | Size: 631 B |
BIN
filemin/images/icons/quick/chmod.png
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
filemin/images/icons/quick/chown.png
Normal file
|
After Width: | Height: | Size: 788 B |
BIN
filemin/images/icons/quick/compress.png
Normal file
|
After Width: | Height: | Size: 738 B |
BIN
filemin/images/icons/quick/document-new.png
Normal file
|
After Width: | Height: | Size: 529 B |
BIN
filemin/images/icons/quick/drive-harddisk.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
filemin/images/icons/quick/edit-copy.png
Normal file
|
After Width: | Height: | Size: 589 B |
BIN
filemin/images/icons/quick/edit-cut.png
Normal file
|
After Width: | Height: | Size: 981 B |
BIN
filemin/images/icons/quick/edit-find.png
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
filemin/images/icons/quick/edit-paste.png
Normal file
|
After Width: | Height: | Size: 608 B |
BIN
filemin/images/icons/quick/edit.png
Normal file
|
After Width: | Height: | Size: 702 B |
BIN
filemin/images/icons/quick/extract.png
Normal file
|
After Width: | Height: | Size: 660 B |
BIN
filemin/images/icons/quick/folder-new.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
filemin/images/icons/quick/from-url.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
filemin/images/icons/quick/go-down.png
Normal file
|
After Width: | Height: | Size: 537 B |
BIN
filemin/images/icons/quick/go-next.png
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
filemin/images/icons/quick/invert.png
Normal file
|
After Width: | Height: | Size: 356 B |
BIN
filemin/images/icons/quick/remove.png
Normal file
|
After Width: | Height: | Size: 838 B |
BIN
filemin/images/icons/quick/rename.png
Normal file
|
After Width: | Height: | Size: 736 B |
BIN
filemin/images/icons/quick/select-all.png
Normal file
|
After Width: | Height: | Size: 294 B |
BIN
filemin/images/icons/quick/upload.png
Normal file
|
After Width: | Height: | Size: 754 B |
@@ -13,6 +13,7 @@ $query = $in{'query'};
|
||||
$text{'search_results'} . " '" .
|
||||
&html_escape($query) . "'", "");
|
||||
|
||||
print $head;
|
||||
if ($in{'caseins'}) {
|
||||
$criteria = '-iname';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
114
filemin/unauthenticated/dropdown/fg.menu.css
Normal 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; }
|
||||
|
||||
648
filemin/unauthenticated/dropdown/fg.menu.js
Normal 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;
|
||||
};
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
7
filemin/unauthenticated/jquery/jquery-ui.min.css
vendored
Normal file
6
filemin/unauthenticated/jquery/jquery-ui.min.js
vendored
Normal file
2
filemin/unauthenticated/jquery/jquery.min.js
vendored
Normal file
475
filemin/unauthenticated/js/legacy.js
Normal 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;
|
||||
}
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
246
filemin/unauthenticated/templates/legacy_dialogs.html
Normal 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"> *</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'}: </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'}: </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>
|
||||
29
filemin/unauthenticated/templates/legacy_quicks.html
Normal 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>
|
||||
67
filemin/unauthenticated/templates/quicks.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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'});
|
||||
|
||||
@@ -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'}).
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 |