Compare commits
202 Commits
dev/minise
...
dev/fix-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925af91ce8 | ||
|
|
e3e6a9b44f | ||
|
|
29cf93863d | ||
|
|
b502deaddf | ||
|
|
0d4c4b7582 | ||
|
|
a272f94d93 | ||
|
|
9026459a53 | ||
|
|
24cd92a061 | ||
|
|
8417d5c279 | ||
|
|
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 | ||
|
|
ee5009a47f | ||
|
|
1bfd92c21c | ||
|
|
97ed7cef25 | ||
|
|
ea691b895e | ||
|
|
98641c73bd | ||
|
|
c8a6a0f69a | ||
|
|
e90b946de6 | ||
|
|
8a09ca5342 | ||
|
|
abf05faa20 | ||
|
|
47d2411e2b | ||
|
|
f98a6a31dc | ||
|
|
6b7832acd0 | ||
|
|
e70bf0ffaa | ||
|
|
9458057978 | ||
|
|
bbdd520807 | ||
|
|
c85e0b8c50 | ||
|
|
d78dcb7625 | ||
|
|
8f79fbd0fd | ||
|
|
8c56e266d1 | ||
|
|
a68dacad64 | ||
|
|
cc0f8134da | ||
|
|
0055cccfef | ||
|
|
f9b818c0b0 | ||
|
|
678c50ed3c | ||
|
|
da34454d9b | ||
|
|
5a806c48c1 | ||
|
|
9faa2b4323 | ||
|
|
66f5f22320 | ||
|
|
623a5f0fa0 | ||
|
|
6dfaa54616 | ||
|
|
3a2b2b8671 | ||
|
|
fcd0f80ad9 | ||
|
|
78d307aa77 | ||
|
|
b0cca9edf2 | ||
|
|
eb93c7aac3 | ||
|
|
290ab1aaea | ||
|
|
bfa733907e | ||
|
|
dedf55cba2 | ||
|
|
2402ed97d7 | ||
|
|
cc95d6285a | ||
|
|
5a03eafaa8 | ||
|
|
6a53df97ef | ||
|
|
1867e9a069 | ||
|
|
dc558631ef | ||
|
|
6b80935024 | ||
|
|
95c974edc4 | ||
|
|
dc0b2ff04e | ||
|
|
a0a20063fa | ||
|
|
ef22c05115 | ||
|
|
1b029fb2c3 | ||
|
|
c5cf9242c7 | ||
|
|
08d9ef3cd0 | ||
|
|
5f020599de | ||
|
|
c134426e2a | ||
|
|
fa98ace56d | ||
|
|
991840e890 | ||
|
|
a8a1b70015 | ||
|
|
afb0929dd9 | ||
|
|
1c80e8f298 | ||
|
|
efe1dddca8 | ||
|
|
beb843bb2b | ||
|
|
dcd929e7e3 | ||
|
|
faaef530f2 | ||
|
|
3d27aca04a | ||
|
|
b0e8115db7 | ||
|
|
ee420bef24 | ||
|
|
9766789a94 | ||
|
|
3eb7493989 | ||
|
|
0c7fc411aa | ||
|
|
d3c0409f0e | ||
|
|
f206cbb23a | ||
|
|
09e6a57f28 | ||
|
|
8a20f7ea34 | ||
|
|
49019cb2db | ||
|
|
267b6bc07f | ||
|
|
29f595dbdf | ||
|
|
7242171885 | ||
|
|
afc256831c | ||
|
|
d4c14d8d5e | ||
|
|
a06590c17e | ||
|
|
3291429123 | ||
|
|
a840b54685 | ||
|
|
cf9c101e99 | ||
|
|
7f44387028 | ||
|
|
2aab9aa69f | ||
|
|
3e91407f81 | ||
|
|
e0b8113348 | ||
|
|
515879dcc8 |
49
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
## Changelog
|
||||
#### 2.670 (September 12, 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
|
||||
* Fix the logged in users list in Users and Groups module showing nobody when `who` prints ISO dates under a non-C locale or `sshd pts/N` lines from the logind backend [forum.virtualmin.com/t/138162](https://forum.virtualmin.com/t/138162)
|
||||
* Fix mail date parsing of ISO dates like `2026-10-15 02:33` returning the wrong minutes
|
||||
* Add optional address-family selection to HTTP downloads and socket connections, preserved across redirects [#1300](https://github.com/virtualmin/virtualmin-gpl/issues/1300)
|
||||
* Update the Authentic theme to the latest version with various improvements:
|
||||
- Add support for Terminal module color preferences
|
||||
- Add support for the new Cloudmin navigation, graphical console and live virtual machine controls
|
||||
- Fix spacing and borders in widgets, tables and tab panes
|
||||
|
||||
#### 2.670 (September 20, 2026)
|
||||
* Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details
|
||||
* Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562)
|
||||
* Add webserver logging controls to Usermin Configuration module
|
||||
@@ -7,6 +29,7 @@
|
||||
* Add DNF 4 and 5 package hold management to the Software Package Updates module
|
||||
* Add `return` based redirects with 301, 302, 303, 307 and 308 codes to the URL Re-Writing pages in the Nginx Webserver module [forum.virtualmin.com/t/137940](https://forum.virtualmin.com/t/137940)
|
||||
* Fix DNF update confirmations by previewing packages and dependencies that will be installed or updated
|
||||
* Fix reboot-required detection on DNF systems without a standalone `needs-restarting` command [forum.virtualmin.com/t/137973](https://forum.virtualmin.com/t/137973)
|
||||
* Fix Postfix map updates failing when CIDR tables are configured
|
||||
* Fix PostgreSQL initialization on EL systems to use SCRAM-SHA-256 authentication by default
|
||||
* Fix IPsec host key generation with modern Libreswan [#2132](https://github.com/webmin/webmin/issues/2132)
|
||||
@@ -17,6 +40,30 @@
|
||||
* Fix Nginx Webserver module lock files not being released, causing saves to hang during long-running operations
|
||||
* Fix module name validation to prevent loading files outside installed modules
|
||||
* Fix ACL operations bypassing configured allowed paths in File Manager
|
||||
* Fix arbitrary file reads in Software Packages module
|
||||
* Fix TLS client certificate verification
|
||||
* Fix Apache Webserver module to enforce the allowed directory ACL when manually editing `.htaccess` files
|
||||
* Fix Custom Commands module to properly quote parameter values passed to commands and to reject parameters with multiple values
|
||||
* Fix DNSSEC NSEC3 zone signing in BIND DNS Server module to use zero additional hash iterations, as recommended by RFC 9276
|
||||
* Fix System Logs module to create a new log file only after the log entry passes validation
|
||||
* Fix file chooser to lift the root directory restriction only when the chroot is at or under the allowed root
|
||||
* Fix password changes from the login page using a configured password change command to fail unless the command prompts for the old password
|
||||
* Fix File Manager search results to only include files under the allowed directories
|
||||
* Update Backup Configuration module's destination selector to use the new select-based UI
|
||||
* Update the Authentic theme to the latest version with various improvements:
|
||||
- Add support for HTML signatures in Usermin
|
||||
- Fix light palette layout gaps to be slightly tighter to match dark palette better
|
||||
- Fix data tables and their filter to be restored fully functional on history back navigation
|
||||
- Fix history back navigation after a full page reload
|
||||
- Fix disabled checked checkboxes showing a cross instead of a checkmark
|
||||
- Fix theme authorization to use explicit Webmin module ACLs for privileged actions
|
||||
- Fix Cloudmin shell history file path validation
|
||||
- Fix module name validation in configuration pages
|
||||
- Fix ACL operations bypassing configured allowed paths in File Manager
|
||||
- Fix mail server credentials being exposed in Usermin responses
|
||||
- Drop server-side Perl customizations
|
||||
- Drop the built-in theme updater
|
||||
- Drop dashboard-managed CSF update checks
|
||||
|
||||
#### 2.660 (August 20, 2026)
|
||||
* Add support for creating `vfsv1` Linux quota files for limits above 4 TiB, while preserving existing quota file formats
|
||||
|
||||
@@ -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,8 +56,16 @@ 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-z0-9\-\_\.\@]+$/ && $in{'name'} !~ /^\@/ ||
|
||||
$in{'name'} =~ /^[A-Za-z0-9\-\_\.\@]+$/ && $in{'name'} !~ /^\@/ ||
|
||||
&error(&text('save_ename', &html_escape($in{'name'})));
|
||||
$in{'name'} eq 'webmin' && &error($text{'save_enamewebmin'});
|
||||
if (!$in{'old'} || $in{'old'} ne $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.
|
||||
|
||||
@@ -7,8 +7,8 @@ no warnings 'once';
|
||||
our $module_name;
|
||||
$main::no_acl_check = 1;
|
||||
$main::no_referers_check = 1;
|
||||
$ENV{'WEBMIN_CONFIG'} = "/etc/webmin";
|
||||
$ENV{'WEBMIN_VAR'} = "/var/webmin";
|
||||
$ENV{'WEBMIN_CONFIG'} ||= "/etc/webmin";
|
||||
$ENV{'WEBMIN_VAR'} ||= "/var/webmin";
|
||||
if ($0 =~ /^(.*\/)[^\/]+$/) {
|
||||
chdir($1);
|
||||
}
|
||||
|
||||
@@ -126,9 +126,10 @@ if (&read_file($site_file, \%site)) {
|
||||
# value - Value (possibly with spaces)
|
||||
# members - For type 1, a reference to the array of members
|
||||
# indent - Number of spaces before the name
|
||||
# comments - Full comment lines immediately before this directive
|
||||
sub parse_config_file
|
||||
{
|
||||
local($fh, @rv, $line, %dummy);
|
||||
local($fh, @rv, $line, %dummy, @comments);
|
||||
$fh = $_[0];
|
||||
$dummy{'line'} = $dummy{'eline'} = $_[1]-1;
|
||||
$dummy{'file'} = $_[2];
|
||||
@@ -146,8 +147,13 @@ foreach my $d (&get_httpd_defines()) {
|
||||
}
|
||||
while($line = <$fh>) {
|
||||
$line =~ s/\r|\n//g;
|
||||
$line =~ s/^\s*#.*$//g;
|
||||
if ($line =~ /^\s*<\/(\S+)\s*(.*)>/) {
|
||||
if ($line =~ /^\s*#/) {
|
||||
# Attach comments to the next directive without reusing the
|
||||
# dummy entry that marks the start of this block
|
||||
push(@comments, $line);
|
||||
$_[1]++;
|
||||
}
|
||||
elsif ($line =~ /^\s*<\/(\S+)\s*(.*)>/) {
|
||||
# end of a container directive. This can only happen in a
|
||||
# recursive call to this function
|
||||
$_[1]++;
|
||||
@@ -178,16 +184,20 @@ while($line = <$fh>) {
|
||||
$not && !$httpd_modules{$mpmmod}
|
||||
) {
|
||||
# use the directives..
|
||||
push(@rv, { 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfModule $not$mod>" });
|
||||
local %open = ( 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfModule $not$mod>" );
|
||||
$open{'comments'} = [ @comments ] if (@comments);
|
||||
push(@rv, \%open);
|
||||
push(@rv, @dirs);
|
||||
push(@rv, { 'line', $_[1]-1,
|
||||
'eline', $_[1]-1,
|
||||
'file', $_[2],
|
||||
'name', "</IfModule>" });
|
||||
}
|
||||
# These comments belong to the conditional, even when skipped.
|
||||
@comments = ( );
|
||||
}
|
||||
elsif ($line =~ /^\s*<IfDefine\s+(\!?)(\S+)>/i) {
|
||||
# start of an IfDefine block. Read it, and if the define
|
||||
@@ -199,16 +209,20 @@ while($line = <$fh>) {
|
||||
if (!$not && defined($defs{$def}) ||
|
||||
$not && !defined($defs{$def})) {
|
||||
# use the directives..
|
||||
push(@rv, { 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfDefine $not$def>" });
|
||||
local %open = ( 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfDefine $not$def>" );
|
||||
$open{'comments'} = [ @comments ] if (@comments);
|
||||
push(@rv, \%open);
|
||||
push(@rv, @dirs);
|
||||
push(@rv, { 'line', $_[1]-1,
|
||||
'eline', $_[1]-1,
|
||||
'file', $_[2],
|
||||
'name', "</IfDefine>" });
|
||||
}
|
||||
# These comments belong to the conditional, even when skipped.
|
||||
@comments = ( );
|
||||
}
|
||||
elsif ($line =~ /^\s*<IfVersion\s+(\!?)(\S*)\s*(\S+)>/i) {
|
||||
# Start of an IfVersion block. Read it, and if the version
|
||||
@@ -247,16 +261,20 @@ while($line = <$fh>) {
|
||||
$match = !$match if ($not);
|
||||
if ($match) {
|
||||
# use the directives..
|
||||
push(@rv, { 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfVersion $not$op $ver>" });
|
||||
local %open = ( 'line', $oldline,
|
||||
'eline', $oldline,
|
||||
'file', $_[2],
|
||||
'name', "<IfVersion $not$op $ver>" );
|
||||
$open{'comments'} = [ @comments ] if (@comments);
|
||||
push(@rv, \%open);
|
||||
push(@rv, @dirs);
|
||||
push(@rv, { 'line', $_[1]-1,
|
||||
'eline', $_[1]-1,
|
||||
'file', $_[2],
|
||||
'name', "</IfVersion>" });
|
||||
}
|
||||
# These comments belong to the conditional, even when skipped.
|
||||
@comments = ( );
|
||||
}
|
||||
elsif ($line =~ /^(\s*)<(\S+)\s*(.*)>/) {
|
||||
# start of a container directive. The first member is a dummy
|
||||
@@ -276,6 +294,8 @@ while($line = <$fh>) {
|
||||
$dir{'eline'} = $_[1]-1;
|
||||
$indent =~ s/\t/ /g;
|
||||
$dir{'indent'} = length($indent);
|
||||
$dir{'comments'} = [ @comments ] if (@comments);
|
||||
@comments = ( );
|
||||
push(@rv, \%dir);
|
||||
}
|
||||
elsif ($line =~ /^(\s*)(\S+)\s*(.*)$/) {
|
||||
@@ -309,11 +329,15 @@ while($line = <$fh>) {
|
||||
}
|
||||
}
|
||||
$dir{'words'} = &wsplit($dir{'value'});
|
||||
$dir{'comments'} = [ @comments ] if (@comments);
|
||||
@comments = ( );
|
||||
push(@rv, \%dir);
|
||||
$_[1]++;
|
||||
}
|
||||
else {
|
||||
# blank or comment line
|
||||
# Keep blank lines that separate a pending comment block from
|
||||
# the directive it describes
|
||||
push(@comments, $line) if (@comments);
|
||||
$_[1]++;
|
||||
}
|
||||
}
|
||||
@@ -569,7 +593,7 @@ return \@conf;
|
||||
# to be updated in the directives array and in the file.
|
||||
sub save_directive
|
||||
{
|
||||
local($i, @old, $lref, $change, $len, $v);
|
||||
my ($i, @old, $lref, $change, $len, $v);
|
||||
@old = &find_directive_struct($_[0], $_[2]);
|
||||
local @files;
|
||||
for($i=0; $i<@old || $i<@{$_[1]}; $i++) {
|
||||
@@ -636,9 +660,12 @@ for($i=0; $i<@old || $i<@{$_[1]}; $i++) {
|
||||
push(@files, $old[$i]->{'file'});
|
||||
$idx = &indexof($old[$i], @{$_[2]});
|
||||
splice(@{$_[2]}, $idx, 1);
|
||||
$len = $old[$i]->{'eline'} - $old[$i]->{'line'} + 1;
|
||||
splice(@$lref, $old[$i]->{'line'}, $len);
|
||||
&renumber($_[3], $old[$i]->{'line'}, $old[$i]->{'file'}, -$len);
|
||||
local $comments = $old[$i]->{'comments'} ?
|
||||
scalar(@{$old[$i]->{'comments'}}) : 0;
|
||||
local $start = $old[$i]->{'line'}-$comments;
|
||||
$len = $old[$i]->{'eline'}-$start+1;
|
||||
splice(@$lref, $start, $len);
|
||||
&renumber($_[3], $start, $old[$i]->{'file'}, -$len);
|
||||
}
|
||||
else {
|
||||
# just changing the value
|
||||
@@ -668,14 +695,21 @@ return @files;
|
||||
# Updates, creates or removes only multi-line directive like a <virtualhost>
|
||||
sub save_directive_struct
|
||||
{
|
||||
local ($olddir, $newdir, $pconf, $conf, $first) = @_;
|
||||
my ($olddir, $newdir, $pconf, $conf, $first) = @_;
|
||||
return if (!$olddir && !$newdir); # Nothing to do
|
||||
local $file = $olddir ? $olddir->{'file'} :
|
||||
$newdir->{'file'} ? $newdir->{'file'} : $pconf->[0]->{'file'};
|
||||
local $lref = &read_file_lines($file);
|
||||
local $oldlen = $olddir ? $olddir->{'eline'}-$olddir->{'line'}+1 : undef;
|
||||
local $oldcomments = $olddir && $olddir->{'comments'} ?
|
||||
scalar(@{$olddir->{'comments'}}) : 0;
|
||||
local $oldline = $olddir ? $olddir->{'line'}-$oldcomments : undef;
|
||||
local $oldlen = $olddir ? $olddir->{'eline'}-$oldline+1 : undef;
|
||||
local @newlines;
|
||||
if ($newdir) {
|
||||
if ($olddir && !exists($newdir->{'comments'}) && $oldcomments) {
|
||||
# A replacement inherits the comments attached to the old block
|
||||
$newdir->{'comments'} = [ @{$olddir->{'comments'}} ];
|
||||
}
|
||||
my $isrc = $olddir ? $olddir :
|
||||
@$pconf ? $pconf->[0] : undef;
|
||||
if ($isrc) {
|
||||
@@ -689,7 +723,9 @@ if ($olddir && $newdir) {
|
||||
$newdir->{'words'} = &wsplit($newdir->{'value'});
|
||||
if ($first) {
|
||||
# Just changing first and last line, like virtualhost IP
|
||||
$lref->[$olddir->{'line'}] = $newlines[0];
|
||||
local $comments = $newdir->{'comments'} ?
|
||||
scalar(@{$newdir->{'comments'}}) : 0;
|
||||
$lref->[$olddir->{'line'}] = $newlines[$comments];
|
||||
$lref->[$olddir->{'eline'}] = $newlines[$#newlines];
|
||||
$olddir->{'name'} = $newdir->{'name'};
|
||||
$olddir->{'value'} = $newdir->{'value'};
|
||||
@@ -702,9 +738,11 @@ if ($olddir && $newdir) {
|
||||
local $idx = &indexof($olddir, @$pconf);
|
||||
$pconf->[$idx] = $newdir if ($idx >= 0);
|
||||
$newdir->{'file'} = $olddir->{'file'};
|
||||
$newdir->{'line'} = $olddir->{'line'};
|
||||
$newdir->{'eline'} = $olddir->{'line'}+scalar(@newlines)-1;
|
||||
splice(@$lref, $olddir->{'line'}, $oldlen, @newlines);
|
||||
local $comments = $newdir->{'comments'} ?
|
||||
scalar(@{$newdir->{'comments'}}) : 0;
|
||||
$newdir->{'line'} = $oldline+$comments;
|
||||
$newdir->{'eline'} = $oldline+scalar(@newlines)-1;
|
||||
splice(@$lref, $oldline, $oldlen, @newlines);
|
||||
|
||||
# Update sub-directive lines and files too
|
||||
if ($newdir->{'type'}) {
|
||||
@@ -716,10 +754,10 @@ if ($olddir && $newdir) {
|
||||
}
|
||||
elsif ($olddir && !$newdir) {
|
||||
# Remove
|
||||
splice(@$lref, $olddir->{'line'}, $oldlen);
|
||||
splice(@$lref, $oldline, $oldlen);
|
||||
local $idx = &indexof($olddir, @$pconf);
|
||||
splice(@$pconf, $idx, 1) if ($idx >= 0);
|
||||
&renumber($conf, $olddir->{'line'}, $olddir->{'file'}, -$oldlen);
|
||||
&renumber($conf, $oldline, $olddir->{'file'}, -$oldlen);
|
||||
}
|
||||
elsif (!$olddir && $newdir) {
|
||||
# Add to file, at end of specific file or parent section
|
||||
@@ -739,7 +777,9 @@ elsif (!$olddir && $newdir) {
|
||||
}
|
||||
$newdir->{'words'} = &wsplit($newdir->{'value'});
|
||||
$newdir->{'file'} = $file;
|
||||
$newdir->{'line'} = $addline;
|
||||
local $comments = $newdir->{'comments'} ?
|
||||
scalar(@{$newdir->{'comments'}}) : 0;
|
||||
$newdir->{'line'} = $addline+$comments;
|
||||
$newdir->{'eline'} = $addline + scalar(@newlines) - 1;
|
||||
&renumber($conf, $addline, $file, scalar(@newlines));
|
||||
splice(@$pconf, $addpos, 0, $newdir);
|
||||
@@ -776,13 +816,14 @@ sub recursive_set_lines_files
|
||||
{
|
||||
my ($dirs, $line, $file) = @_;
|
||||
foreach my $dir (@$dirs) {
|
||||
# A directive's line follows any comments attached to it
|
||||
$line += scalar(@{$dir->{'comments'}}) if ($dir->{'comments'});
|
||||
$dir->{'line'} = $line;
|
||||
$dir->{'file'} = $file;
|
||||
if ($dir->{'type'}) {
|
||||
# Do sub-members too
|
||||
&recursive_set_lines_files($dir->{'members'}, $line+1, $file);
|
||||
$line += scalar(grep { $_->{'name'} ne 'dummy' }
|
||||
@{$dir->{'members'}})+1;
|
||||
# Continue after every line used by nested members
|
||||
$line = &recursive_set_lines_files($dir->{'members'},
|
||||
$line+1, $file);
|
||||
}
|
||||
$dir->{'eline'} = $line;
|
||||
$line++ if ($dir->{'name'} ne 'dummy');
|
||||
@@ -1872,15 +1913,14 @@ return @rv;
|
||||
}
|
||||
|
||||
# allowed_auth_file(file)
|
||||
# Returns 1 if a .htaccess file can be edited by the current user
|
||||
sub allowed_auth_file
|
||||
{
|
||||
local $_;
|
||||
my ($file) = @_;
|
||||
return 0 if ($file =~ /\.\./);
|
||||
my $f = &server_root($file);
|
||||
return 1 if ($access{'dir'} eq '/');
|
||||
return 0 if ($_[0] =~ /\.\./);
|
||||
local $f = &server_root($_[0]);
|
||||
return 0 if (-l $f && !&allowed_auth_file(readlink($f)));
|
||||
local $l = length($access{'dir'});
|
||||
return length($f) >= $l && substr($f, 0, $l) eq $access{'dir'};
|
||||
return &is_under_directory($access{'dir'}, $f);
|
||||
}
|
||||
|
||||
# directory_exists(file)
|
||||
@@ -1932,6 +1972,7 @@ sub directive_lines
|
||||
my @rv;
|
||||
foreach my $d (@_) {
|
||||
next if ($d->{'name'} eq 'dummy');
|
||||
push(@rv, @{$d->{'comments'}}) if ($d->{'comments'});
|
||||
my $indent = (" " x $d->{'indent'});
|
||||
if ($d->{'type'}) {
|
||||
push(@rv, $indent."<$d->{'name'} $d->{'value'}>");
|
||||
@@ -1950,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;
|
||||
}
|
||||
@@ -2029,7 +2097,7 @@ foreach $f (keys %before_changing) {
|
||||
# (without any translation).
|
||||
sub find_httpd_conf
|
||||
{
|
||||
local $conf = $config{'httpd_conf'};
|
||||
my $conf = $config{'httpd_conf'};
|
||||
return ( -f &translate_filename($conf) ? $conf : undef, $conf ) if ($conf);
|
||||
$conf = "$config{'httpd_dir'}/conf/httpd.conf";
|
||||
$conf = "$config{'httpd_dir'}/conf/httpd2.conf"
|
||||
|
||||
@@ -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
|
||||
@@ -35,6 +35,7 @@ if (defined($in{'virt'})) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
&allowed_auth_file($in{'file'}) || &error($text{'htindex_ecannot'});
|
||||
if (defined($in{'idx'})) {
|
||||
# files within .htaccess file
|
||||
$hconf = &get_htaccess_config($in{'file'});
|
||||
|
||||
@@ -34,6 +34,7 @@ if (defined($in{'virt'})) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
&allowed_auth_file($in{'file'}) || &error($text{'htindex_ecannot'});
|
||||
if (defined($in{'idx'})) {
|
||||
# files within .htaccess file
|
||||
$hconf = &get_htaccess_config($in{'file'});
|
||||
|
||||
244
apache/t/directive-comments.t
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/perl
|
||||
# Full-line comments must survive Apache block rewrites.
|
||||
|
||||
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 $apache_root = File::Spec->catdir($tmp, 'apache2');
|
||||
my $apache_conf = File::Spec->catfile($apache_root, 'apache2.conf');
|
||||
|
||||
make_path($webmin_config, $webmin_var, "$webmin_config/apache",
|
||||
"$webmin_var/apache", $apache_root);
|
||||
|
||||
sub write_text
|
||||
{
|
||||
my ($file, $text) = @_;
|
||||
open(my $fh, '>', $file) || die "Failed to write $file: $!";
|
||||
print $fh $text;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
}
|
||||
|
||||
sub read_text
|
||||
{
|
||||
my ($file) = @_;
|
||||
open(my $fh, '<', $file) || die "Failed to read $file: $!";
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
return $text;
|
||||
}
|
||||
|
||||
# Load the Apache module with an isolated Webmin configuration.
|
||||
write_text(File::Spec->catfile($webmin_config, 'config'),
|
||||
"os_type=debian-linux\n".
|
||||
"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=/bin/true\n".
|
||||
"httpd_conf=$apache_conf\n".
|
||||
"apachectl_path=/bin/true\n".
|
||||
"httpd_version=2.4.57\n");
|
||||
write_text($apache_conf, "Listen 80\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';
|
||||
|
||||
unshift(@INC, $root);
|
||||
require File::Spec->catfile($root, 'apache', 'apache-lib.pl');
|
||||
|
||||
# Comments at multiple indentation levels must round-trip with the directives.
|
||||
my $roundtrip = File::Spec->catfile($tmp, 'comments.conf');
|
||||
my $roundtrip_text =
|
||||
"# outer comment\n".
|
||||
"<Directory /srv/example>\n".
|
||||
" # nested comment\n".
|
||||
"\n".
|
||||
" Require all granted\n".
|
||||
"</Directory>\n";
|
||||
write_text($roundtrip, $roundtrip_text);
|
||||
open(my $fh, '<', $roundtrip) || die "Failed to read $roundtrip: $!";
|
||||
my $line = 0;
|
||||
my @roundtrip_config = main::parse_config_file($fh, $line, $roundtrip);
|
||||
close($fh) || die "Failed to close $roundtrip: $!";
|
||||
my ($directory) =
|
||||
main::find_directive_struct('Directory', \@roundtrip_config);
|
||||
my ($require) =
|
||||
main::find_directive_struct('Require', $directory->{'members'});
|
||||
is_deeply($directory->{'comments'}, [ '# outer comment' ],
|
||||
'outer comment is attached to the directory');
|
||||
is_deeply($require->{'comments'}, [ ' # nested comment', '' ],
|
||||
'nested comment block is attached to the following directive');
|
||||
ok(!$roundtrip_config[0]->{'comments'} &&
|
||||
!$directory->{'members'}->[0]->{'comments'},
|
||||
'dummy block markers do not store comments');
|
||||
is(join("\n", main::directive_lines(@roundtrip_config))."\n",
|
||||
$roundtrip_text, 'comments survive parsing and serialization');
|
||||
|
||||
# Rewriting and removing virtual hosts must handle attached comments with them.
|
||||
my $vhosts_file = File::Spec->catfile($tmp, 'vhosts.conf');
|
||||
my $first_vhost =
|
||||
"# first virtual host\n".
|
||||
"<VirtualHost *:80>\n".
|
||||
" # first comment\n".
|
||||
"\n".
|
||||
" ServerName first.example\n".
|
||||
"</VirtualHost>\n";
|
||||
my $second_vhost =
|
||||
"# second virtual host\n".
|
||||
"<VirtualHost *:443>\n".
|
||||
" # second comment\n".
|
||||
" ServerName second.example\n".
|
||||
"</VirtualHost>\n";
|
||||
write_text($vhosts_file, $first_vhost.$second_vhost);
|
||||
open($fh, '<', $vhosts_file) || die "Failed to read $vhosts_file: $!";
|
||||
$line = 0;
|
||||
my @vhost_config = main::parse_config_file($fh, $line, $vhosts_file);
|
||||
close($fh) || die "Failed to close $vhosts_file: $!";
|
||||
my @vhosts = main::find_directive_struct('VirtualHost', \@vhost_config);
|
||||
|
||||
main::recursive_set_lines_files(\@vhost_config, 0, $vhosts_file);
|
||||
is($vhosts[0]->{'line'}, 1, 'block line follows its attached comment');
|
||||
is($vhosts[0]->{'eline'}, 5, 'member comment is included in block line count');
|
||||
is($vhosts[1]->{'line'}, 7, 'next block follows its attached comment');
|
||||
|
||||
my %replacement = %{$vhosts[0]};
|
||||
delete($replacement{'comments'});
|
||||
$replacement{'value'} = '*:8080';
|
||||
my $rewritten_first = $first_vhost;
|
||||
$rewritten_first =~ s/\*:80>/*:8080>/;
|
||||
main::save_directive_struct($vhosts[0], \%replacement,
|
||||
\@vhost_config, \@vhost_config);
|
||||
main::flush_file_lines($vhosts_file);
|
||||
is(read_text($vhosts_file), $rewritten_first.$second_vhost,
|
||||
'replacement block preserves surrounding comments');
|
||||
is_deeply($replacement{'comments'}, [ '# first virtual host' ],
|
||||
'replacement block inherits attached comments');
|
||||
|
||||
main::save_directive_struct($vhosts[1], undef,
|
||||
\@vhost_config, \@vhost_config);
|
||||
main::flush_file_lines($vhosts_file);
|
||||
is(read_text($vhosts_file),
|
||||
$rewritten_first,
|
||||
'deleting a block removes its attached comment');
|
||||
|
||||
# Normal directive edits preserve comments, while deletion removes their owner.
|
||||
my $directives_file = File::Spec->catfile($tmp, 'directives.conf');
|
||||
my $directives_text =
|
||||
"# listen comment\n".
|
||||
"Listen 80\n".
|
||||
"# server name comment\n".
|
||||
"ServerName old.example\n";
|
||||
write_text($directives_file, $directives_text);
|
||||
open($fh, '<', $directives_file) ||
|
||||
die "Failed to read $directives_file: $!";
|
||||
$line = 0;
|
||||
my @directive_config =
|
||||
main::parse_config_file($fh, $line, $directives_file);
|
||||
close($fh) || die "Failed to close $directives_file: $!";
|
||||
|
||||
main::save_directive('ServerName', [ 'new.example' ],
|
||||
\@directive_config, \@directive_config);
|
||||
main::flush_file_lines($directives_file);
|
||||
my $updated_directives =
|
||||
"# listen comment\n".
|
||||
"Listen 80\n".
|
||||
"# server name comment\n".
|
||||
"ServerName new.example\n";
|
||||
is(read_text($directives_file), $updated_directives,
|
||||
'comments survive a normal directive edit');
|
||||
|
||||
main::save_directive('Listen', [ ], \@directive_config, \@directive_config);
|
||||
main::flush_file_lines($directives_file);
|
||||
my $deleted_directive =
|
||||
"# server name comment\n".
|
||||
"ServerName new.example\n";
|
||||
is(read_text($directives_file), $deleted_directive,
|
||||
'deleting a directive removes its attached comment');
|
||||
|
||||
open($fh, '<', $directives_file) ||
|
||||
die "Failed to read $directives_file: $!";
|
||||
$line = 0;
|
||||
@directive_config = main::parse_config_file($fh, $line, $directives_file);
|
||||
close($fh) || die "Failed to close $directives_file: $!";
|
||||
my ($servername) =
|
||||
main::find_directive_struct('ServerName', \@directive_config);
|
||||
is_deeply($servername->{'comments'},
|
||||
[ '# server name comment' ],
|
||||
'remaining comment stays attached to its directive');
|
||||
|
||||
# Nested block positions must include comments at every level.
|
||||
my $inner = {
|
||||
'name' => 'Files', 'value' => '*.php', 'type' => 1,
|
||||
'comments' => [ ' # files comment' ],
|
||||
'members' => [
|
||||
{ 'name' => 'dummy', 'type' => 0 },
|
||||
{ 'name' => 'Require', 'value' => 'all denied',
|
||||
'comments' => [ ' # access comment' ] },
|
||||
],
|
||||
};
|
||||
my $outer = {
|
||||
'name' => 'Directory', 'value' => '/srv/example', 'type' => 1,
|
||||
'members' => [
|
||||
{ 'name' => 'dummy', 'type' => 0 },
|
||||
$inner,
|
||||
{ 'name' => 'Options', 'value' => 'Indexes' },
|
||||
],
|
||||
};
|
||||
my @nested = ($outer);
|
||||
my @nested_lines = main::directive_lines(@nested);
|
||||
my $next_line = main::recursive_set_lines_files(\@nested, 20, $vhosts_file);
|
||||
is($inner->{'line'}, 22, 'nested block line follows its comment');
|
||||
is($inner->{'members'}->[1]->{'line'}, 24,
|
||||
'nested member line follows its comment');
|
||||
is($outer->{'members'}->[2]->{'line'}, 26,
|
||||
'directive after nested block has the correct line');
|
||||
is($outer->{'eline'}, 27, 'outer block ends after all nested lines');
|
||||
is($next_line, 20 + scalar(@nested_lines),
|
||||
'line count matches nested serialized output');
|
||||
|
||||
# Comments on skipped conditionals must not move to a later directive.
|
||||
my @skipped_conditionals = (
|
||||
[ 'IfModule',
|
||||
'<IfModule webmin_test_missing_module.c>', '</IfModule>' ],
|
||||
[ 'IfDefine',
|
||||
'<IfDefine WEBMIN_TEST_MISSING_DEFINE>', '</IfDefine>' ],
|
||||
[ 'IfVersion', '<IfVersion >= 999.0>', '</IfVersion>' ],
|
||||
);
|
||||
foreach my $conditional (@skipped_conditionals) {
|
||||
my ($name, $opening, $closing) = @{$conditional};
|
||||
my $conditional_file =
|
||||
File::Spec->catfile($tmp, "skipped-$name.conf");
|
||||
write_text($conditional_file,
|
||||
"# skipped $name comment\n".
|
||||
"$opening\n".
|
||||
" IgnoredDirective value\n".
|
||||
"$closing\n".
|
||||
"ServerName example.test\n");
|
||||
open($fh, '<', $conditional_file) ||
|
||||
die "Failed to read $conditional_file: $!";
|
||||
$line = 0;
|
||||
my @conditional_config =
|
||||
main::parse_config_file($fh, $line, $conditional_file);
|
||||
close($fh) || die "Failed to close $conditional_file: $!";
|
||||
my ($conditional_servername) =
|
||||
main::find_directive_struct('ServerName', \@conditional_config);
|
||||
ok(!$conditional_servername->{'comments'},
|
||||
"skipped $name comment is not attached to ServerName");
|
||||
}
|
||||
|
||||
done_testing();
|
||||
91
apache/t/directive-lines.t
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/perl
|
||||
# Apache directive line numbers must match their serialized positions.
|
||||
|
||||
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 $apache_root = File::Spec->catdir($tmp, 'apache2');
|
||||
my $apache_conf = File::Spec->catfile($apache_root, 'apache2.conf');
|
||||
|
||||
make_path($webmin_config, $webmin_var, "$webmin_config/apache",
|
||||
"$webmin_var/apache", $apache_root);
|
||||
|
||||
sub write_text
|
||||
{
|
||||
my ($file, $text) = @_;
|
||||
open(my $fh, '>', $file) || die "Failed to write $file: $!";
|
||||
print $fh $text;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
}
|
||||
|
||||
# Load the Apache module with an isolated Webmin configuration.
|
||||
write_text(File::Spec->catfile($webmin_config, 'config'),
|
||||
"os_type=debian-linux\n".
|
||||
"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=/bin/true\n".
|
||||
"httpd_conf=$apache_conf\n".
|
||||
"apachectl_path=/bin/true\n".
|
||||
"httpd_version=2.4.57\n");
|
||||
write_text($apache_conf, "Listen 80\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';
|
||||
|
||||
unshift(@INC, $root);
|
||||
require File::Spec->catfile($root, 'apache', 'apache-lib.pl');
|
||||
|
||||
# Model a template containing <Files> inside <Directory>, followed by a
|
||||
# directive whose position must include every line in both nested blocks.
|
||||
my $inner_require = {
|
||||
'name' => 'Require', 'value' => 'all denied', 'indent' => 8,
|
||||
};
|
||||
my $files = {
|
||||
'name' => 'Files', 'value' => '*.php', 'type' => 1, 'indent' => 4,
|
||||
'members' => [
|
||||
{ 'name' => 'dummy', 'type' => 0 },
|
||||
$inner_require,
|
||||
],
|
||||
};
|
||||
my $directory = {
|
||||
'name' => 'Directory', 'value' => '/srv/example', 'type' => 1,
|
||||
'indent' => 0,
|
||||
'members' => [
|
||||
{ 'name' => 'dummy', 'type' => 0 },
|
||||
{ 'name' => 'Require', 'value' => 'all granted', 'indent' => 4 },
|
||||
$files,
|
||||
],
|
||||
};
|
||||
my $alias = {
|
||||
'name' => 'ScriptAlias', 'value' => '/cgi-bin/ /srv/cgi-bin/',
|
||||
'indent' => 0,
|
||||
};
|
||||
my @directives = ($directory, $alias);
|
||||
my @lines = main::directive_lines(@directives);
|
||||
my $next = main::recursive_set_lines_files(\@directives, 10, '/tmp/test.conf');
|
||||
|
||||
is($directory->{'line'}, 10, 'outer block starts at the first line');
|
||||
is($files->{'line'}, 12, 'nested block starts after the outer directive');
|
||||
is($inner_require->{'line'}, 13, 'nested member has its serialized line');
|
||||
is($files->{'eline'}, 14, 'nested block ends after all of its members');
|
||||
is($directory->{'eline'}, 15, 'outer block includes the nested closing line');
|
||||
is($alias->{'line'}, 16, 'following directive starts after the outer block');
|
||||
is($next, 10 + scalar(@lines), 'returned line follows serialized output');
|
||||
|
||||
done_testing();
|
||||
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();
|
||||
@@ -147,81 +147,69 @@ else {
|
||||
|
||||
=head2 show_backup_destination(name, value, [local-mode])
|
||||
|
||||
Returns HTML for a field for selecting a local or FTP file.
|
||||
Returns HTML for selecting a backup destination.
|
||||
|
||||
=cut
|
||||
sub show_backup_destination
|
||||
{
|
||||
my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($_[1]);
|
||||
my $rv;
|
||||
$rv .= "<table id='show_backup_destination' cellpadding=1 cellspacing=0>";
|
||||
|
||||
# Local file field
|
||||
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 0, undef, $mode == 0)."</td>\n";
|
||||
$rv .= "<td>".&ui_tag('strong', $text{'backup_mode0'})." </td><td colspan='3'>".
|
||||
&ui_textbox("$_[0]_file", $mode == 0 ? $path : "", 60, undef, undef,
|
||||
($_[2] != 1 && $config{'date_subs'}) ?
|
||||
'placeholder="/backups/configs-%y-%m-%d-%H-%M-%S.tar.gz"' : undef).
|
||||
" ".&file_chooser_button("$_[0]_file")."</td> </tr>\n";
|
||||
|
||||
# FTP file fields
|
||||
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 1, undef, $mode == 1)."</td>\n";
|
||||
$rv .= "<td>".&ui_tag('strong', $text{'backup_mode1'})." </td><td>".
|
||||
&ui_textbox("$_[0]_server", $mode == 1 ? $server : undef, 20).
|
||||
"</td>\n";
|
||||
$rv .= "<td> $text{'backup_path'} </td><td> ".
|
||||
&ui_textbox("$_[0]_path", $mode == 1 ? $path : undef, 20).
|
||||
"</td> </tr>\n";
|
||||
$rv .= "<tr> <td></td>\n";
|
||||
$rv .= "<td>$text{'backup_login'} </td><td> ".
|
||||
&ui_textbox("$_[0]_user", $mode == 1 ? $user : undef, 20).
|
||||
"</td>\n";
|
||||
$rv .= "<td> $text{'backup_pass'} </td><td> ".
|
||||
&ui_password("$_[0]_pass", $mode == 1 ? $pass : undef, 20).
|
||||
"</td> </tr>\n";
|
||||
$rv .= "<tr> <td></td>\n";
|
||||
$rv .= "<td colspan='4'>$text{'backup_port'} ".
|
||||
&ui_opt_textbox("$_[0]_port", $mode == 1 ? $port : undef, 5,
|
||||
$text{'default'})."</td> </tr>\n";
|
||||
|
||||
# SCP file fields
|
||||
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 2, undef, $mode == 2)."</td>\n";
|
||||
$rv .= "<td>".&ui_tag('strong', $text{'backup_mode2'})." </td><td>".
|
||||
&ui_textbox("$_[0]_sserver", $mode == 2 ? $server : undef, 20).
|
||||
"</td>\n";
|
||||
$rv .= "<td> $text{'backup_path'} </td><td> ".
|
||||
&ui_textbox("$_[0]_spath", $mode == 2 ? $path : undef, 20).
|
||||
"</td> </tr>\n";
|
||||
$rv .= "<tr> <td></td>\n";
|
||||
$rv .= "<td>$text{'backup_login'} </td><td> ".
|
||||
&ui_textbox("$_[0]_suser", $mode == 2 ? $user : undef, 20).
|
||||
"</td>\n";
|
||||
$rv .= "<td> $text{'backup_pass'} </td><td> ".
|
||||
&ui_password("$_[0]_spass", $mode == 2 ? $pass : undef, 20).
|
||||
"</td> </tr>\n";
|
||||
$rv .= "<tr> <td></td>\n";
|
||||
$rv .= "<td colspan='4'>$text{'backup_port'} ".
|
||||
&ui_opt_textbox("$_[0]_sport", $mode == 2 ? $port : undef, 5,
|
||||
$text{'default'})."</td> </tr>\n";
|
||||
# Keep the existing input names while showing only the selected fields
|
||||
my @options = (
|
||||
# Local file path and chooser
|
||||
{ 'value' => 0, 'label' => $text{'backup_mode0'},
|
||||
'content' =>
|
||||
&ui_textbox("$_[0]_file", $mode == 0 ? $path : "", 60,
|
||||
undef, undef,
|
||||
($_[2] != 1 && $config{'date_subs'}) ?
|
||||
'placeholder="/backups/configs-%y-%m-%d-%H-%M-%S.tar.gz"' :
|
||||
undef).
|
||||
" ".&file_chooser_button("$_[0]_file") },
|
||||
# FTP connection and remote path
|
||||
{ 'value' => 1, 'label' => $text{'backup_mode1'},
|
||||
'content' => &ui_textbox("$_[0]_server",
|
||||
$mode == 1 ? $server : undef, 30),
|
||||
'fields' => [
|
||||
[ $text{'backup_path'}, &ui_textbox("$_[0]_path",
|
||||
$mode == 1 ? $path : undef, 30) ],
|
||||
[ $text{'backup_login'}, &ui_textbox("$_[0]_user",
|
||||
$mode == 1 ? $user : undef, 15) ],
|
||||
[ $text{'backup_pass'}, &ui_password("$_[0]_pass",
|
||||
$mode == 1 ? $pass : undef, 15, 0,
|
||||
undef, 'data-password-meter-skip') ],
|
||||
[ $text{'backup_port'}, &ui_opt_textbox("$_[0]_port",
|
||||
$mode == 1 ? $port : undef, 5,
|
||||
$text{'default'}) ],
|
||||
] },
|
||||
# SSH connection and remote path
|
||||
{ 'value' => 2, 'label' => $text{'backup_mode2'},
|
||||
'content' => &ui_textbox("$_[0]_sserver",
|
||||
$mode == 2 ? $server : undef, 30),
|
||||
'fields' => [
|
||||
[ $text{'backup_path'}, &ui_textbox("$_[0]_spath",
|
||||
$mode == 2 ? $path : undef, 30) ],
|
||||
[ $text{'backup_login'}, &ui_textbox("$_[0]_suser",
|
||||
$mode == 2 ? $user : undef, 15) ],
|
||||
[ $text{'backup_pass'}, &ui_password("$_[0]_spass",
|
||||
$mode == 2 ? $pass : undef, 15, 0,
|
||||
undef, 'data-password-meter-skip') ],
|
||||
[ $text{'backup_port'}, &ui_opt_textbox("$_[0]_sport",
|
||||
$mode == 2 ? $port : undef, 5,
|
||||
$text{'default'}) ],
|
||||
] },
|
||||
);
|
||||
|
||||
if ($_[2] == 1) {
|
||||
# Uploaded file field
|
||||
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 3, undef, $mode == 3).
|
||||
"</td>\n";
|
||||
$rv .= "<td colspan=4>".&ui_tag('strong', $text{'backup_mode3'})." ".
|
||||
&ui_upload("$_[0]_upload", 40).
|
||||
"</td> </tr>\n";
|
||||
push(@options, { 'value' => 3, 'label' => $text{'backup_mode3'},
|
||||
'content' => &ui_upload("$_[0]_upload", 40) });
|
||||
}
|
||||
elsif ($_[2] == 2) {
|
||||
# Output to browser option
|
||||
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 4, undef, $mode == 4).
|
||||
"</td>\n";
|
||||
$rv .= "<td colspan=4>".&ui_tag('strong', $text{'backup_mode4'}).
|
||||
"</td> </tr>\n";
|
||||
push(@options, { 'value' => 4, 'label' => $text{'backup_mode4'} });
|
||||
}
|
||||
|
||||
$rv .= "</table>\n";
|
||||
return $rv;
|
||||
return &ui_select_switch("$_[0]_mode", $mode, \@options);
|
||||
}
|
||||
|
||||
=head2 parse_backup_destination(name, &in)
|
||||
|
||||
@@ -29,6 +29,7 @@ else {
|
||||
$backup = &get_backup($in{'id'});
|
||||
}
|
||||
|
||||
print &ui_page_start();
|
||||
print &ui_form_start("save.cgi", "post");
|
||||
print &ui_hidden("new", $in{'new'});
|
||||
print &ui_hidden("id", $in{'id'});
|
||||
@@ -107,6 +108,7 @@ else {
|
||||
[ 'delete', $text{'delete'} ] ], "100%");
|
||||
}
|
||||
|
||||
print &ui_page_end();
|
||||
&ui_print_footer("", $text{'index_return'});
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ my @mods = &list_backup_modules();
|
||||
if (!@mods) {
|
||||
&ui_print_endpage($text{'index_emods'});
|
||||
}
|
||||
print &ui_page_start();
|
||||
my %mods = map { $_->{'dir'}, $_ } @mods;
|
||||
|
||||
# Show tabs
|
||||
@@ -64,7 +65,10 @@ my $hostname = &get_system_hostname() || "localhost";
|
||||
$hostname =~ s/\./-/g;
|
||||
my $filename = $hostname."+configuration_backup-webmin-".
|
||||
strftime("%Y-%m-%d-%H-%M", localtime);
|
||||
print &ui_form_start("backup.cgi/$filename.tar.gz", "post");
|
||||
# Let themes bypass AJAX when the current destination is a browser download
|
||||
print &ui_form_start("backup.cgi/$filename.tar.gz", "post", undef,
|
||||
"onsubmit=\"this.dataset.download = ".
|
||||
"this.elements.dest_mode.value == '4' ? 'true' : 'false'\"");
|
||||
print &ui_table_start($text{'index_header'}, undef, 2);
|
||||
|
||||
my @dmods = split(/\s+/, $config{'mods'} || "");
|
||||
@@ -114,5 +118,6 @@ print &ui_form_end([ [ 'restore', $text{'index_now2'} ] ]);
|
||||
print &ui_tabs_end_tab();
|
||||
print &ui_tabs_end(1);
|
||||
|
||||
print &ui_page_end();
|
||||
&ui_print_footer("/", $text{'index'});
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -3734,7 +3734,8 @@ while($tries++ < 10) {
|
||||
$out = &backquote_logged(
|
||||
"cd ".quotemeta($dir)." && ".
|
||||
"$config{'signzone'} -o ".quotemeta($dom).
|
||||
($alg =~ /^(NSEC3|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA256|ECDSAP384SHA384|ED25519|ED448)/ ? " -3 - -u" : "").
|
||||
($alg =~ /^(NSEC3|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA256|ECDSAP384SHA384|ED25519|ED448)/ ?
|
||||
" -3 - -H 0 -u" : "").
|
||||
" -f ".quotemeta($signed)." ".
|
||||
quotemeta($chrootfn)." 2>&1");
|
||||
last if (!$?);
|
||||
|
||||
@@ -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
|
||||
@@ -394,9 +394,14 @@ else {
|
||||
my $dmarc = $r ? &parse_dmarc(@{$r->{'values'}}) : { };
|
||||
$dmarc->{'p'} = $in{'dmarcp'};
|
||||
|
||||
$in{'dmarcpct'} =~ /^\d+$/ && $in{'dmarcpct'} >= 0 &&
|
||||
$in{'dmarcpct'} <= 100 || &error($text{'edit_edmarcpct'});
|
||||
$dmarc->{'pct'} = $in{'dmarcpct'};
|
||||
if ($in{'dmarcpct'} ne '') {
|
||||
$in{'dmarcpct'} =~ /^\d+$/ && $in{'dmarcpct'} >= 0 &&
|
||||
$in{'dmarcpct'} <= 100 || &error($text{'edit_edmarcpct'});
|
||||
$dmarc->{'pct'} = $in{'dmarcpct'};
|
||||
}
|
||||
else {
|
||||
delete($dmarc->{'pct'});
|
||||
}
|
||||
|
||||
if ($in{'dmarcsp'}) {
|
||||
$dmarc->{'sp'} = $in{'dmarcsp'};
|
||||
|
||||
@@ -60,7 +60,7 @@ if (&supports_users()) {
|
||||
# If a chroot is forced which is under the allowed root, there is no need for
|
||||
# a restrictred root
|
||||
if ($in{'chroot'} && $in{'chroot'} ne '/' && $rootdir && $rootdir ne '/' &&
|
||||
$in{'chroot'} =~ /^\Q$rootdir\E/) {
|
||||
($in{'chroot'} =~ /^\Q$rootdir\E\// || $in{'chroot'} eq $rootdir)) {
|
||||
$rootdir = undef;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -480,8 +480,14 @@ foreach my $a (@{$cmd->{'args'}}) {
|
||||
if ($rv eq '' && $a->{'must'} && $a->{'type'} != 7) {
|
||||
&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='$rv'\n";
|
||||
$env .= "$n=".quotemeta($rv)."\n";
|
||||
$export .= " $n";
|
||||
if ($a->{'quote'}) {
|
||||
$str =~ s/\$$n/"\$$n"/g;
|
||||
@@ -495,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
|
||||
|
||||
@@ -95,6 +95,7 @@ run_ecmduser=User '$1' to run command as does not exist
|
||||
run_eopt=Invalid option selected
|
||||
run_eupload=No file to upload selected
|
||||
run_emust=Missing value for parameter '$1'
|
||||
run_ezero=Multiple values for parameters are not allowed
|
||||
run_timeout=Command failed to complete before $1 second timeout.
|
||||
run_failed=Command failed with exit status $1
|
||||
|
||||
|
||||
@@ -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'}) {
|
||||
|
||||
@@ -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 |
@@ -41,8 +41,7 @@ else {
|
||||
if (&test_allowed_paths()) {
|
||||
for $path (@allowed_paths) {
|
||||
my $slashed = $path;
|
||||
$slashed .= "/"
|
||||
if ($slashed !~ /\/$/);
|
||||
$slashed .= "/" if ($slashed !~ /\/$/);
|
||||
push @tmp_list,
|
||||
grep { $slashed =~ /^\Q$_\E\// ||
|
||||
$_ =~ /\Q$slashed\E/ } @list;
|
||||
|
||||
@@ -13,7 +13,6 @@ $query = $in{'query'};
|
||||
$text{'search_results'} . " '" .
|
||||
&html_escape($query) . "'", "");
|
||||
|
||||
print $head;
|
||||
if ($in{'caseins'}) {
|
||||
$criteria = '-iname';
|
||||
}
|
||||
@@ -24,6 +23,16 @@ else {
|
||||
"find ".quotemeta($cwd).
|
||||
" $criteria ".
|
||||
quotemeta("*$in{'query'}*")));
|
||||
if (&test_allowed_paths()) {
|
||||
my @alist;
|
||||
foreach my $path (@allowed_paths) {
|
||||
my $slashed = $path;
|
||||
$slashed .= "/" if ($slashed !~ /\/$/);
|
||||
push(@alist, grep { $_ eq $path ||
|
||||
$_ =~ /^\Q$slashed\E/ } @list);
|
||||
}
|
||||
@list = &unique(@alist);
|
||||
}
|
||||
@list = map {
|
||||
[$_, stat($_), &clean_mimetype($_), -d $_]
|
||||
} @list;
|
||||
|
||||
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>
|
||||