Compare commits
111 Commits
dev/framed
...
dev/fix-rp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91d3dd843b | ||
|
|
5231d6de0f | ||
|
|
ffa551a24b | ||
|
|
dda9b972b9 | ||
|
|
cce4d69a09 | ||
|
|
6a9e0afa7b | ||
|
|
658f930d69 | ||
|
|
1c7781299d | ||
|
|
6c674def60 | ||
|
|
692cb6ea70 | ||
|
|
200444d8d6 | ||
|
|
01e454414b | ||
|
|
b5ce991eb3 | ||
|
|
045c7abfdf | ||
|
|
5e4b937410 | ||
|
|
3d22baaeea | ||
|
|
502a54b6df | ||
|
|
6fec16bbd2 | ||
|
|
65de483f37 | ||
|
|
222a2fd31b | ||
|
|
2d014758a2 | ||
|
|
01ce0a70d1 | ||
|
|
166cfefa28 | ||
|
|
3d0a9aeea8 | ||
|
|
616547c26f | ||
|
|
24db25a6c5 | ||
|
|
072bf2bda3 | ||
|
|
8bc89757a7 | ||
|
|
02d2ce697f | ||
|
|
bc9d7e67a8 | ||
|
|
7b6c3b3857 | ||
|
|
ba0d06743d | ||
|
|
759359db48 | ||
|
|
31680372d2 | ||
|
|
c23b6b5b48 | ||
|
|
0f71dac9cf | ||
|
|
6b78f7d8b5 | ||
|
|
c57711a3b5 | ||
|
|
6c008506d9 | ||
|
|
32d20fe18c | ||
|
|
2259de1962 | ||
|
|
6873c65c42 | ||
|
|
f2b94348dc | ||
|
|
6fecd282b8 | ||
|
|
1e1f9982d7 | ||
|
|
19e86a9657 | ||
|
|
42850e5795 | ||
|
|
e99841ebc6 | ||
|
|
c8e06cb06c | ||
|
|
2f8e484829 | ||
|
|
17ba87cbc6 | ||
|
|
22c31d206f | ||
|
|
f7f2fcbdb8 | ||
|
|
1a88fcdb74 | ||
|
|
34040bd0df | ||
|
|
89ab870924 | ||
|
|
bc87da9048 | ||
|
|
f9cac4c4a4 | ||
|
|
c51f0ed465 | ||
|
|
63a07308ab | ||
|
|
51cf5c0798 | ||
|
|
270ef90703 | ||
|
|
2bf3f4834c | ||
|
|
82e7e343f5 | ||
|
|
dea3af005f | ||
|
|
b4c79b1348 | ||
|
|
8f9663fd9c | ||
|
|
3bb7eeb144 | ||
|
|
51b5e9c15a | ||
|
|
5e90e1438a | ||
|
|
9f9a54ed51 | ||
|
|
0befd2663c | ||
|
|
f759798e7f | ||
|
|
cfe8d72d85 | ||
|
|
1fed13ba32 | ||
|
|
39786bae17 | ||
|
|
593fb94f11 | ||
|
|
37296f5b22 | ||
|
|
51e3dccb1d | ||
|
|
f354238c24 | ||
|
|
223b0f331f | ||
|
|
851883c41d | ||
|
|
95b74cc45f | ||
|
|
e327940bd6 | ||
|
|
18d5344043 | ||
|
|
fd7760d2e1 | ||
|
|
8b9cb8c603 | ||
|
|
77ffa83f8f | ||
|
|
32fb7f2462 | ||
|
|
afd6fad51e | ||
|
|
0be0ba411a | ||
|
|
11be3726f7 | ||
|
|
1726be59e6 | ||
|
|
61d3618cc5 | ||
|
|
84ea802947 | ||
|
|
2dea2f8090 | ||
|
|
22c580aea1 | ||
|
|
8f880ab366 | ||
|
|
f8f35b26c6 | ||
|
|
875c973658 | ||
|
|
8742ccd312 | ||
|
|
94436938d0 | ||
|
|
cec6e6074d | ||
|
|
aeee5edd15 | ||
|
|
359eb13f0e | ||
|
|
316f1330de | ||
|
|
ff83f05768 | ||
|
|
97ba9cbd46 | ||
|
|
1bfd92c21c | ||
|
|
97ed7cef25 | ||
|
|
ea691b895e |
18
CHANGELOG.md
@@ -1,7 +1,23 @@
|
||||
## Changelog
|
||||
#### 2.671 (October, 2026)
|
||||
#### 2.680 (October, 2026)
|
||||
* Add color schemes to the Terminal module
|
||||
* Add per-user Terminal preferences for color scheme, font size, screen reader mode and terminal dimensions
|
||||
* Add protection against infinite loops resolving recursive symlinks (thanks to Carl Zuzart, Zedden I.T.)
|
||||
* Add a Cloudmin virtualization host profile to the nftables module
|
||||
* Add drop-down menu, dialog, breadcrumb and page link widgets to the UI library, built on native `details` and `dialog` elements
|
||||
* Add confirmation dialogs for buttons and links carrying `data-ui-confirm`, replacing the browser's own prompt
|
||||
* Update 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 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 path traversal and command injection in parameterized Custom Commands file editors
|
||||
* Fix MySQL/MariaDB manual editor ACL bypass
|
||||
* 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,7 +447,11 @@ if ($miniserv{'userdb'} && !$miniserv{'userdb_addto'}) {
|
||||
$user->{'proto'} = $proto;
|
||||
}
|
||||
else {
|
||||
# Adding to local files
|
||||
# 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));
|
||||
}
|
||||
&lock_file($ENV{'MINISERV_CONFIG'});
|
||||
if ($user->{'theme'}) {
|
||||
$miniserv{"preroot_".$user->{'name'}} =
|
||||
@@ -628,7 +632,11 @@ if ($user->{'proto'}) {
|
||||
&disconnect_userdb($miniserv{'userdb'}, $dbh);
|
||||
}
|
||||
else {
|
||||
# In local files
|
||||
# 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));
|
||||
}
|
||||
&lock_file($ENV{'MINISERV_CONFIG'});
|
||||
delete($miniserv{"preroot_".$username});
|
||||
if ($user->{'theme'}) {
|
||||
@@ -1466,7 +1474,9 @@ return if (&is_readonly_mode());
|
||||
my $sid = &generate_random_session_id();
|
||||
return if (!$sid);
|
||||
my $t = time();
|
||||
$sessiondb{$sid} = "$username $t 127.0.0.1".($lifetime ? " ".$lifetime : "");
|
||||
# miniserv only looks up sessions by their hashed key
|
||||
$sessiondb{&hash_session_id($sid)} =
|
||||
"$username $t 127.0.0.1".($lifetime ? " ".$lifetime : "");
|
||||
dbmclose(%sessiondb);
|
||||
return $sid;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,11 @@ foreach my $u (@users) {
|
||||
$msg = "<i>".&text('convert_exists', $u->{'user'})."</i>";
|
||||
$exists++;
|
||||
}
|
||||
elsif ($u->{'user'} !~ /^[A-z0-9\-\_\.]+$/) {
|
||||
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.
|
||||
$msg = "<i>".&text('convert_invalid', $u->{'user'})."</i>";
|
||||
$invalid++;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ 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,6 +129,7 @@ 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
|
||||
@@ -201,6 +202,7 @@ 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{'pass'} || &error($text{'sql_ecannot'});
|
||||
$access{'sql'} || &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{'pass'} || &error($text{'sql_ecannot'});
|
||||
$access{'sql'} || &error($text{'sql_ecannot'});
|
||||
&ReadParse();
|
||||
&error_setup($text{'make_err'});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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'}) {
|
||||
@@ -30,6 +31,7 @@ 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,6 +9,7 @@ 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{'pass'} || &error($text{'sql_ecannot'});
|
||||
$access{'sql'} || &error($text{'sql_ecannot'});
|
||||
&ReadParse();
|
||||
&error_setup($text{'sql_err'});
|
||||
my %miniserv;
|
||||
|
||||
@@ -56,6 +56,14 @@ 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{'pass'} || &error($text{'sql_ecannot'});
|
||||
$access{'sql'} || &error($text{'sql_ecannot'});
|
||||
|
||||
&ui_print_unbuffered_header(undef, $text{'schema_title'}, "");
|
||||
|
||||
|
||||
@@ -1265,6 +1265,31 @@ 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
|
||||
@@ -1449,6 +1474,40 @@ 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,29 +1991,56 @@ return @rv;
|
||||
# or undef.
|
||||
sub test_config
|
||||
{
|
||||
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 ($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");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
256
apache/t/test-config.t
Normal file
@@ -0,0 +1,256 @@
|
||||
#!/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();
|
||||
@@ -1,12 +0,0 @@
|
||||
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
|
||||
@@ -1,61 +0,0 @@
|
||||
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,6 +48,10 @@ 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'} ] ]));
|
||||
@@ -79,6 +83,7 @@ $_[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'};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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,6 +1079,8 @@ 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 {
|
||||
@@ -1092,6 +1094,11 @@ 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++) {
|
||||
@@ -1168,6 +1175,11 @@ 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) };
|
||||
@@ -1308,6 +1320,43 @@ 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,6 +6,7 @@ command=1
|
||||
create=1
|
||||
delete=1
|
||||
move=1
|
||||
env=1
|
||||
kill=1
|
||||
hourly=2
|
||||
stop=1
|
||||
|
||||
@@ -6,9 +6,10 @@ require './cron-lib.pl';
|
||||
&ReadParse();
|
||||
@jobs = &list_cron_jobs();
|
||||
$job = $jobs[$in{'idx'}];
|
||||
&lock_file($job->{'file'});
|
||||
$job && !$job->{'name'} || &error($text{'edit_egone'});
|
||||
$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,11 +4,13 @@
|
||||
|
||||
require './cron-lib.pl';
|
||||
&ReadParse();
|
||||
$access{'env'} || &error($text{'env_ecannot'});
|
||||
@jobs = &list_cron_jobs();
|
||||
$job = $jobs[$in{'idx'}];
|
||||
&lock_file($job->{'file'});
|
||||
&can_edit_user(\%access, $job->{'user'}) ||
|
||||
$job && $job->{'name'} || &error($text{'env_egone'});
|
||||
$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", "env", $job->{'user'}, $job);
|
||||
|
||||
@@ -9,13 +9,20 @@ require './cron-lib.pl';
|
||||
@d || &error($text{'delete_enone'});
|
||||
|
||||
if ($in{'delete'}) {
|
||||
# Delete selected jobs
|
||||
# Validate all selected jobs before changing any of them
|
||||
@selected = ( );
|
||||
foreach $d (sort { $b <=> $a } @d) {
|
||||
($job) = grep { $_->{'index'} eq $d } @jobs;
|
||||
$job || next;
|
||||
&lock_file($job->{'file'});
|
||||
!$job->{'name'} || $access{'env'} ||
|
||||
&error($text{'env_ecannot'});
|
||||
$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;
|
||||
}
|
||||
@@ -23,13 +30,20 @@ if ($in{'delete'}) {
|
||||
&webmin_log("delete", "crons", scalar(@d));
|
||||
}
|
||||
elsif ($in{'disable'} || $in{'enable'}) {
|
||||
# Disable selected
|
||||
# Validate all selected jobs before changing any of them
|
||||
@selected = ( );
|
||||
foreach $d (@d) {
|
||||
($job) = grep { $_->{'index'} eq $d } @jobs;
|
||||
$job || next;
|
||||
&lock_file($job->{'file'});
|
||||
!$job->{'name'} || $access{'env'} ||
|
||||
&error($text{'env_ecannot'});
|
||||
&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 || &error($text{'edit_egone'});
|
||||
$job && !$job->{'name'} || &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 || &error($text{'edit_egone'});
|
||||
$clone && !$clone->{'name'} || &error($text{'edit_egone'});
|
||||
&can_edit_user(\%access, $clone->{'user'}) ||
|
||||
&error($text{'edit_ecannot'});
|
||||
$job = { %$clone };
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -68,8 +71,9 @@ if ($in{'new'}) {
|
||||
print &ui_form_end([ [ undef, $text{'create'} ] ]);
|
||||
}
|
||||
else {
|
||||
print &ui_form_end([ [ undef, $text{'save'} ],
|
||||
[ "delete", $text{'delete'} ] ]);
|
||||
my @buttons = ( [ undef, $text{'save'} ] );
|
||||
push(@buttons, [ "delete", $text{'delete'} ]) if ($access{'delete'});
|
||||
print &ui_form_end(\@buttons);
|
||||
}
|
||||
|
||||
&ui_print_footer("", $text{'index_return'});
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,7 +52,8 @@ 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);
|
||||
$text{'index_ecreate'}) )
|
||||
if ($env_support && $access{'env'});
|
||||
}
|
||||
if ($config{cron_allow_file} && $config{cron_deny_file} && $access{'allow'}) {
|
||||
push(@crlinks, &ui_link("edit_allow.cgi", $text{'index_allow'}) );
|
||||
@@ -106,9 +107,11 @@ foreach $u (@ulist) {
|
||||
if ($job->{'name'}) {
|
||||
# An environment variable - show the name only
|
||||
$cmdidx = scalar(@cols);
|
||||
push(@cols, &ui_link("edit_env.cgi?idx=".$idx,
|
||||
"<i>$text{'index_env'}</i> ".
|
||||
"<tt>@{[&html_escape($job->{'name'})]} = @{[&html_escape($job->{'value'})]}</tt>") );
|
||||
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);
|
||||
$donelink = 1;
|
||||
}
|
||||
elsif (@exp && $access{'command'}) {
|
||||
@@ -219,6 +222,7 @@ foreach $u (@ulist) {
|
||||
push(@cols, $job->{'command'}.' '.$job->{'name'}.' '.
|
||||
$job->{'comment'});
|
||||
push(@rows, \@cols);
|
||||
$noenv{$idx}++ if ($job->{'name'} && !$access{'env'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,8 +268,10 @@ 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]);
|
||||
\@tds, "d", $r->[0], 0,
|
||||
$noenv{$r->[0]});
|
||||
}
|
||||
print &ui_columns_end();
|
||||
print &ui_links_row(\@links);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,6 +81,11 @@ 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
|
||||
@@ -88,6 +93,7 @@ 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.
|
||||
@@ -113,6 +119,8 @@ 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
|
||||
@@ -132,6 +140,7 @@ 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,35 +8,32 @@ require './cron-lib.pl';
|
||||
|
||||
@jobs = &list_cron_jobs();
|
||||
$job = $jobs[$in{'idx'}];
|
||||
$job || &error($text{'edit_egone'});
|
||||
if ($in{'up'}) {
|
||||
$swap = $jobs[$in{'idx'}-1];
|
||||
$direction = "up";
|
||||
}
|
||||
elsif ($in{'down'}) {
|
||||
$swap = $jobs[$in{'idx'}+1];
|
||||
$direction = "down";
|
||||
}
|
||||
elsif ($in{'top'}) {
|
||||
for(my $i=$in{'idx'};
|
||||
$i && $jobs[$i]->{'file'} eq $job->{'file'}; $i--) {
|
||||
$swap = $jobs[$i];
|
||||
}
|
||||
$direction = "top";
|
||||
}
|
||||
elsif ($in{'bottom'}) {
|
||||
for(my $i=$in{'idx'};
|
||||
$i < @jobs && $jobs[$i]->{'file'} eq $job->{'file'}; $i++) {
|
||||
$swap = $jobs[$i];
|
||||
}
|
||||
$direction = "bottom";
|
||||
}
|
||||
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->{'type'} == 0 || $job->{'type'} == 3) &&
|
||||
($swap->{'type'} == 0 || $swap->{'type'} == 3) || &error($text{'move_etype'});
|
||||
($job->{'name'} || $job->{'type'} == 0 || $job->{'type'} == 3) &&
|
||||
($swap->{'name'} || $swap->{'type'} == 0 || $swap->{'type'} == 3) ||
|
||||
&error($text{'move_etype'});
|
||||
&lock_file($job->{'file'});
|
||||
&swap_cron_jobs($job, $swap);
|
||||
&unlock_file($job->{'file'});
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
stop=0
|
||||
env=0
|
||||
|
||||
@@ -4,5 +4,6 @@ command=1
|
||||
create=1
|
||||
delete=1
|
||||
move=1
|
||||
env=0
|
||||
kill=1
|
||||
hourly=0
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'};
|
||||
@@ -37,6 +38,10 @@ 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'});
|
||||
@@ -70,6 +75,7 @@ if (defined($in{'range_def'})) {
|
||||
&unconvert_range($job);
|
||||
}
|
||||
|
||||
$in{'comment'} =~ /\r|\n/ && &error($text{'save_ecomment'});
|
||||
$job->{'comment'} = $in{'comment'};
|
||||
&unconvert_comment($job);
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
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'};
|
||||
}
|
||||
@@ -38,6 +41,9 @@ 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'});
|
||||
}
|
||||
|
||||
299
cron/t/run-tests.t
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/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,6 +481,11 @@ 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";
|
||||
@@ -496,6 +501,30 @@ 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,10 +18,9 @@ if ($file !~ /^\//) {
|
||||
|
||||
# Set environment variables for parameters
|
||||
($env, $export, $str, $displayfile) = &set_parameter_envs($edit, $file);
|
||||
|
||||
if ($edit->{'envs'} || @{$edit->{'args'}}) {
|
||||
# Do environment variable substitution
|
||||
chop($file = `echo "$file"`);
|
||||
# Expand variables without invoking a shell
|
||||
$file = &resolve_editor_file($file);
|
||||
}
|
||||
|
||||
# Run the before-command
|
||||
@@ -37,14 +36,15 @@ $in{'data'} =~ s/\r//g;
|
||||
&print_tempfile(FILE, $in{'data'});
|
||||
&close_tempfile(FILE);
|
||||
|
||||
# Set permissions
|
||||
# Set file ownership and permissions without invoking a shell
|
||||
if ($edit->{'user'}) {
|
||||
&system_logged("chown $edit->{'user'}:$edit->{'group'} ".
|
||||
"$file >/dev/null 2>&1");
|
||||
# Apply the configured owner and group
|
||||
&set_ownership_permissions($edit->{'user'}, $edit->{'group'},
|
||||
undef, $file);
|
||||
}
|
||||
if ($edit->{'perms'}) {
|
||||
&system_logged("chmod $edit->{'perms'} ".
|
||||
"$file >/dev/null 2>&1");
|
||||
# Apply the configured file permissions
|
||||
&set_ownership_permissions(undef, undef, oct($edit->{'perms'}), $file);
|
||||
}
|
||||
|
||||
# Run the after-command
|
||||
|
||||
@@ -17,12 +17,11 @@ if ($file !~ /^\//) {
|
||||
|
||||
# Set environment variables for parameters
|
||||
($env, $export, $str, $displayfile) = &set_parameter_envs($edit, $file);
|
||||
$displayfile = &html_escape($displayfile);
|
||||
|
||||
if ($edit->{'envs'} || @{$edit->{'args'}}) {
|
||||
# Do environment variable substitution
|
||||
chop($file = `echo "$file"`);
|
||||
# Expand variables without invoking a shell
|
||||
$file = &resolve_editor_file($file);
|
||||
}
|
||||
$displayfile = &html_escape($displayfile);
|
||||
|
||||
# Run any before-edit command
|
||||
if ($edit->{'beforeedit'}) {
|
||||
|
||||
@@ -142,6 +142,8 @@ while(1) {
|
||||
$rawarg .= $got;
|
||||
}
|
||||
print STDERR "fastrpc[$$]: raw $rawarg\n" if ($gconfig{'rpcdebug'});
|
||||
|
||||
# Mirror the request's wire format in the response
|
||||
my $dumper = substr($rawarg, 0, 5) eq '$VAR1' ? 1 : 0;
|
||||
my $arg = &unserialise_variable($rawarg);
|
||||
|
||||
|
||||
@@ -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 ".
|
||||
quotemeta($full).
|
||||
" -d ".quotemeta($cwd);
|
||||
$cmd = "unzip $uu -q -o".
|
||||
" -d ".quotemeta($cwd).
|
||||
" -- ".quotemeta($full);
|
||||
}
|
||||
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, $head);
|
||||
$rename_icon, $extract_icon, $goto_icon, $root_icon);
|
||||
our (%access, %userconfig, %allowed_for_edit);
|
||||
our (@allowed_paths, @allowed_for_edit, @remote_user_info);
|
||||
|
||||
@@ -413,124 +413,9 @@ if ($tinfo{'bootstrap'}) {
|
||||
"unauthenticated/templates/dialogs.html");
|
||||
}
|
||||
|
||||
# Interface for legacy themes
|
||||
# Interface for other themes, built from the core widgets
|
||||
else {
|
||||
|
||||
# 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");
|
||||
print &plain_interface_head();
|
||||
}
|
||||
my $info_total;
|
||||
my $info_files = scalar @files;
|
||||
@@ -801,6 +686,18 @@ 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()
|
||||
@@ -811,10 +708,8 @@ $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 (@$bookmarks) {
|
||||
foreach $bookmark (&list_bookmarks()) {
|
||||
$result .=
|
||||
"<li><a href='index.cgi?path=" .
|
||||
&urlize($bookmark) . "'>" .
|
||||
@@ -824,6 +719,486 @@ foreach $bookmark (@$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
|
||||
@@ -1110,10 +1485,9 @@ foreach my $fref (@{$files_to_extract}) {
|
||||
my $uu = ($unzip_out =~ /-UU/
|
||||
? '-UU' : undef);
|
||||
$status = system(
|
||||
"$unzip_cmd $uu -q -o " .
|
||||
quotemeta($archive) .
|
||||
" -d " .
|
||||
quotemeta($extract_to));
|
||||
"$unzip_cmd $uu -q -o".
|
||||
" -d " .quotemeta($extract_to).
|
||||
" -- ".quotemeta($archive));
|
||||
}
|
||||
}
|
||||
elsif ($archive_type =~ /\/x-rar|\/vnd\.rar/) {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 734 B |
|
Before Width: | Height: | Size: 631 B |
|
Before Width: | Height: | Size: 956 B |
|
Before Width: | Height: | Size: 788 B |
|
Before Width: | Height: | Size: 738 B |
|
Before Width: | Height: | Size: 529 B |
|
Before Width: | Height: | Size: 864 B |
|
Before Width: | Height: | Size: 589 B |
|
Before Width: | Height: | Size: 981 B |
|
Before Width: | Height: | Size: 884 B |
|
Before Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 660 B |
|
Before Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 537 B |
|
Before Width: | Height: | Size: 522 B |
|
Before Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 838 B |
|
Before Width: | Height: | Size: 736 B |
|
Before Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 754 B |
@@ -13,7 +13,6 @@ $query = $in{'query'};
|
||||
$text{'search_results'} . " '" .
|
||||
&html_escape($query) . "'", "");
|
||||
|
||||
print $head;
|
||||
if ($in{'caseins'}) {
|
||||
$criteria = '-iname';
|
||||
}
|
||||
|
||||
85
filemin/unauthenticated/css/plain.css
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/* 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; }
|
||||
|
||||
@@ -1,648 +0,0 @@
|
||||
/*--------------------------------------------------------------------
|
||||
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;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
2
filemin/unauthenticated/jquery/jquery.min.js
vendored
@@ -1,475 +0,0 @@
|
||||
$( 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;
|
||||
}
|
||||
*/
|
||||
183
filemin/unauthenticated/js/plain.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
})();
|
||||
@@ -1,246 +0,0 @@
|
||||
<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>
|
||||
@@ -1,29 +0,0 @@
|
||||
<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>
|
||||
@@ -1,67 +0,0 @@
|
||||
<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 rspaldar
|
||||
dump_dirs=Directorios a respaldar
|
||||
dump_fs=Sistema de archivos
|
||||
dump_sched=¿Programado?
|
||||
dump_when=Horas programadas para respaldar
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
}
|
||||
html:not([data-scheme="light"]) body:not([data-scheme="light"])
|
||||
:is(.ui_page, .ui_multi, .ui_toggle) {
|
||||
:is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
/* The root follows its body, so the scrollbars and the canvas behind
|
||||
@@ -261,8 +261,8 @@ html[data-scheme="dark"] body, body[data-scheme="dark"] {
|
||||
--toggle-thumb-color: #e6e8ea;
|
||||
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
}
|
||||
html[data-scheme="dark"] body :is(.ui_page, .ui_multi, .ui_toggle),
|
||||
body[data-scheme="dark"] :is(.ui_page, .ui_multi, .ui_toggle) {
|
||||
html[data-scheme="dark"] body :is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog),
|
||||
body[data-scheme="dark"] :is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
html[data-scheme="dark"], html:has(> body[data-scheme="dark"]) {
|
||||
@@ -613,7 +613,14 @@ tr.ui_buttons_row > td {
|
||||
border-top: 1px solid var(--border-color-lighter);
|
||||
vertical-align: middle;
|
||||
}
|
||||
tr.ui_buttons_row:first-child > td { border-top: 0; }
|
||||
/* Each row is a form with a table of its own, as a form cannot wrap table
|
||||
* rows. Flattening the wrappers lets all rows share the outer columns */
|
||||
tr.ui_buttons_form_row,
|
||||
tr.ui_buttons_form_row > td,
|
||||
form.ui_buttons_form,
|
||||
form.ui_buttons_form > table,
|
||||
form.ui_buttons_form > table > tbody { display: contents; }
|
||||
tr.ui_buttons_form_row:first-child tr.ui_buttons_row > td { border-top: 0; }
|
||||
td.ui_buttons_label {
|
||||
width: 1%;
|
||||
padding-right: 16px;
|
||||
|
||||
@@ -28,6 +28,9 @@ function ts_makeSortable(table) {
|
||||
// We have a first row: assume it's the header, and make its contents clickable links
|
||||
for (var i=0;i<firstRow.cells.length;i++) {
|
||||
var cell = firstRow.cells[i];
|
||||
// A heading holding a control, such as a select-all checkbox, is
|
||||
// left as it is : rewriting it as a sort link would drop the control
|
||||
if (cell.querySelector && cell.querySelector('input, select, button, textarea')) continue;
|
||||
var txt = ts_getInnerText(cell);
|
||||
cell.innerHTML = '<b><a href="#" class="sortheader" '+
|
||||
'onclick="ts_resortTable(this, '+i+');return false;">' +
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
htaccess=.htaccess
|
||||
htpasswd=.htpasswd
|
||||
htgroups=.htgroups
|
||||
sort=0
|
||||
md5=0
|
||||
sha1=0
|
||||
digest=1
|
||||
extra_directives=
|
||||
@@ -1,8 +0,0 @@
|
||||
show_empty=1
|
||||
services_file=${systemroot}/system32/drivers/etc/services
|
||||
extended_inetd=0
|
||||
rpc_inetd=0
|
||||
sort_mode=0
|
||||
ipv6=0
|
||||
qm_mode=0
|
||||
display_max=100
|
||||
@@ -1,5 +0,0 @@
|
||||
reboot_command=shutdown.exe -r -t 1 -f
|
||||
shutdown_command=shutdown.exe -s -t 1 -f
|
||||
status_check=0
|
||||
order=0
|
||||
sort_mode=0
|
||||
@@ -16,7 +16,8 @@ while(-r $newfile) {
|
||||
else { $ac = $ac."_1"; }
|
||||
$newfile = &action_filename($ac);
|
||||
}
|
||||
`mv $oldfile $newfile`;
|
||||
&rename_logged($oldfile, $newfile);
|
||||
&add_rl_action($ac, $rl, $ss, $num);
|
||||
&webmin_log("fix", "action", $ac);
|
||||
&redirect("edit_action.cgi?0+$ac");
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ log_rename=Renamed action $1 to $2
|
||||
log_modify=Modified action $1
|
||||
log_create=Created action $1
|
||||
log_delete=Deleted action $1
|
||||
log_fix=Fixed action $1
|
||||
log_start=Started action $1
|
||||
log_stop=Stopped action $1
|
||||
log_restart=Restarted action $1
|
||||
|
||||
@@ -23,6 +23,9 @@ elsif ($action eq 'create') {
|
||||
elsif ($action eq 'delete') {
|
||||
return &text('log_delete', "<tt>$object</tt>");
|
||||
}
|
||||
elsif ($action eq 'fix') {
|
||||
return &text('log_fix', "<tt>$object</tt>");
|
||||
}
|
||||
elsif ($type eq 'action') {
|
||||
return &text('log_'.$action, "<tt>$object</tt>");
|
||||
}
|
||||
|
||||
6
lang/en
@@ -346,6 +346,12 @@ ui_page_help=Help
|
||||
ui_empty_state=Nothing to display
|
||||
ui_search=Search
|
||||
ui_dismiss=Dismiss
|
||||
ui_close=Close
|
||||
ui_confirm=Please confirm
|
||||
ui_breadcrumb=Breadcrumb
|
||||
ui_pager=Pages
|
||||
ui_pager_prev=Previous page
|
||||
ui_pager_next=Next page
|
||||
|
||||
header_statusmsg=$1 logged into $2 $3 on $4 ($5)
|
||||
|
||||
|
||||
@@ -982,7 +982,14 @@ else {
|
||||
$fromaddr .= '@'.&get_system_hostname();
|
||||
}
|
||||
local $qfromaddr = quotemeta($fromaddr);
|
||||
local $esmtp = $flags ? 1 : 0;
|
||||
|
||||
# Resolve SMTP credentials and the authentication method before choosing the
|
||||
# greeting because authenticated sessions require EHLO.
|
||||
$user ||= $userconfig{'smtp_user'} || $config{'smtp_user'};
|
||||
$pass ||= $userconfig{'smtp_pass'} || $config{'smtp_pass'};
|
||||
$auth ||= $userconfig{'smtp_auth'} ||
|
||||
$config{'smtp_auth'} || "Cram-MD5";
|
||||
local $esmtp = $flags || $user || $ssl ? 1 : 0;
|
||||
my $h = { 'fh' => 'mailboxes::MAIL' };
|
||||
if ($file) {
|
||||
# Just append the email to a file using mbox format
|
||||
@@ -1004,28 +1011,25 @@ elsif ($sm) {
|
||||
|
||||
&smtp_command($h, undef, 0);
|
||||
|
||||
# Send SMTP HELO
|
||||
# Send EHLO for ESMTP features, or HELO for basic SMTP
|
||||
my $helo = $config{'helo_name'} || &get_system_hostname();
|
||||
my $helocmd = $esmtp ? "ehlo $helo\r\n" : "helo $helo\r\n";
|
||||
&smtp_command($h, $helocmd, 0);
|
||||
|
||||
if ($ssl == 2) {
|
||||
# Switch to SSL with STARTTLS, if possible
|
||||
# Request a TLS upgrade with STARTTLS
|
||||
my $rv = &smtp_command($h, "starttls\r\n", 1);
|
||||
if ($rv =~ /^2\d+/) {
|
||||
# Enable TLS and repeat the greeting on the secure connection
|
||||
&switch_smtp_to_ssl($h);
|
||||
&smtp_command($h, $helocmd, 0);
|
||||
}
|
||||
else {
|
||||
# Continue without TLS if STARTTLS is unavailable
|
||||
$ssl = 0;
|
||||
}
|
||||
}
|
||||
|
||||
# Get username and password from parameters, or from module config
|
||||
$user ||= $userconfig{'smtp_user'} || $config{'smtp_user'};
|
||||
$pass ||= $userconfig{'smtp_pass'} || $config{'smtp_pass'};
|
||||
$auth ||= $userconfig{'smtp_auth'} ||
|
||||
$config{'smtp_auth'} || "Cram-MD5";
|
||||
if ($user) {
|
||||
# Send authentication commands
|
||||
eval "use Authen::SASL";
|
||||
|
||||
@@ -52,7 +52,7 @@ foreach $a (@attach) {
|
||||
# Make and output the zip
|
||||
$zip = &transname("$$.zip");
|
||||
$out = &backquote_command(
|
||||
"cd ".quotemeta($temp)." && zip ".quotemeta($zip)." * 2>&1");
|
||||
"cd ".quotemeta($temp)." && zip ".quotemeta($zip)." -- * 2>&1");
|
||||
if ($?) {
|
||||
&error(&text('detachall_ezip', "<tt>".&html_escape($out)."</tt>"));
|
||||
}
|
||||
|
||||