Compare commits

..

1 Commits

Author SHA1 Message Date
Ilia Ross
34336186f8 Add WebminCron pause during module post-install
This PR stops Miniserv from starting scheduled jobs while run-postinstalls.pl is running, so jobs never execute against half-updated module code or config.

Miniserv defers all jobs, including startup jobs, while any marker belongs to a live process, and discards markers left by runners that died or that are older than an hour.

The pause is best-effort, as a failure to create the marker is reported but never blocks post-installs.

By the way, this issue was not reported by anyone. I discovered it while running many other extensive tests.
2026-09-12 19:13:10 +02:00
276 changed files with 4603 additions and 14885 deletions

View File

@@ -1,27 +1,5 @@
## Changelog
#### 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)
#### 2.670 (September 12, 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
@@ -29,7 +7,6 @@
* 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)
@@ -40,30 +17,6 @@
* 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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -126,10 +126,9 @@ 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, @comments);
local($fh, @rv, $line, %dummy);
$fh = $_[0];
$dummy{'line'} = $dummy{'eline'} = $_[1]-1;
$dummy{'file'} = $_[2];
@@ -147,13 +146,8 @@ foreach my $d (&get_httpd_defines()) {
}
while($line = <$fh>) {
$line =~ s/\r|\n//g;
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*(.*)>/) {
$line =~ s/^\s*#.*$//g;
if ($line =~ /^\s*<\/(\S+)\s*(.*)>/) {
# end of a container directive. This can only happen in a
# recursive call to this function
$_[1]++;
@@ -184,20 +178,16 @@ while($line = <$fh>) {
$not && !$httpd_modules{$mpmmod}
) {
# use the directives..
local %open = ( 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfModule $not$mod>" );
$open{'comments'} = [ @comments ] if (@comments);
push(@rv, \%open);
push(@rv, { 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfModule $not$mod>" });
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
@@ -209,20 +199,16 @@ while($line = <$fh>) {
if (!$not && defined($defs{$def}) ||
$not && !defined($defs{$def})) {
# use the directives..
local %open = ( 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfDefine $not$def>" );
$open{'comments'} = [ @comments ] if (@comments);
push(@rv, \%open);
push(@rv, { 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfDefine $not$def>" });
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
@@ -261,20 +247,16 @@ while($line = <$fh>) {
$match = !$match if ($not);
if ($match) {
# use the directives..
local %open = ( 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfVersion $not$op $ver>" );
$open{'comments'} = [ @comments ] if (@comments);
push(@rv, \%open);
push(@rv, { 'line', $oldline,
'eline', $oldline,
'file', $_[2],
'name', "<IfVersion $not$op $ver>" });
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
@@ -294,8 +276,6 @@ 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*(.*)$/) {
@@ -329,15 +309,11 @@ while($line = <$fh>) {
}
}
$dir{'words'} = &wsplit($dir{'value'});
$dir{'comments'} = [ @comments ] if (@comments);
@comments = ( );
push(@rv, \%dir);
$_[1]++;
}
else {
# Keep blank lines that separate a pending comment block from
# the directive it describes
push(@comments, $line) if (@comments);
# blank or comment line
$_[1]++;
}
}
@@ -593,7 +569,7 @@ return \@conf;
# to be updated in the directives array and in the file.
sub save_directive
{
my ($i, @old, $lref, $change, $len, $v);
local($i, @old, $lref, $change, $len, $v);
@old = &find_directive_struct($_[0], $_[2]);
local @files;
for($i=0; $i<@old || $i<@{$_[1]}; $i++) {
@@ -660,12 +636,9 @@ for($i=0; $i<@old || $i<@{$_[1]}; $i++) {
push(@files, $old[$i]->{'file'});
$idx = &indexof($old[$i], @{$_[2]});
splice(@{$_[2]}, $idx, 1);
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);
$len = $old[$i]->{'eline'} - $old[$i]->{'line'} + 1;
splice(@$lref, $old[$i]->{'line'}, $len);
&renumber($_[3], $old[$i]->{'line'}, $old[$i]->{'file'}, -$len);
}
else {
# just changing the value
@@ -695,21 +668,14 @@ return @files;
# Updates, creates or removes only multi-line directive like a <virtualhost>
sub save_directive_struct
{
my ($olddir, $newdir, $pconf, $conf, $first) = @_;
local ($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 $oldcomments = $olddir && $olddir->{'comments'} ?
scalar(@{$olddir->{'comments'}}) : 0;
local $oldline = $olddir ? $olddir->{'line'}-$oldcomments : undef;
local $oldlen = $olddir ? $olddir->{'eline'}-$oldline+1 : undef;
local $oldlen = $olddir ? $olddir->{'eline'}-$olddir->{'line'}+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) {
@@ -723,9 +689,7 @@ if ($olddir && $newdir) {
$newdir->{'words'} = &wsplit($newdir->{'value'});
if ($first) {
# Just changing first and last line, like virtualhost IP
local $comments = $newdir->{'comments'} ?
scalar(@{$newdir->{'comments'}}) : 0;
$lref->[$olddir->{'line'}] = $newlines[$comments];
$lref->[$olddir->{'line'}] = $newlines[0];
$lref->[$olddir->{'eline'}] = $newlines[$#newlines];
$olddir->{'name'} = $newdir->{'name'};
$olddir->{'value'} = $newdir->{'value'};
@@ -738,11 +702,9 @@ if ($olddir && $newdir) {
local $idx = &indexof($olddir, @$pconf);
$pconf->[$idx] = $newdir if ($idx >= 0);
$newdir->{'file'} = $olddir->{'file'};
local $comments = $newdir->{'comments'} ?
scalar(@{$newdir->{'comments'}}) : 0;
$newdir->{'line'} = $oldline+$comments;
$newdir->{'eline'} = $oldline+scalar(@newlines)-1;
splice(@$lref, $oldline, $oldlen, @newlines);
$newdir->{'line'} = $olddir->{'line'};
$newdir->{'eline'} = $olddir->{'line'}+scalar(@newlines)-1;
splice(@$lref, $olddir->{'line'}, $oldlen, @newlines);
# Update sub-directive lines and files too
if ($newdir->{'type'}) {
@@ -754,10 +716,10 @@ if ($olddir && $newdir) {
}
elsif ($olddir && !$newdir) {
# Remove
splice(@$lref, $oldline, $oldlen);
splice(@$lref, $olddir->{'line'}, $oldlen);
local $idx = &indexof($olddir, @$pconf);
splice(@$pconf, $idx, 1) if ($idx >= 0);
&renumber($conf, $oldline, $olddir->{'file'}, -$oldlen);
&renumber($conf, $olddir->{'line'}, $olddir->{'file'}, -$oldlen);
}
elsif (!$olddir && $newdir) {
# Add to file, at end of specific file or parent section
@@ -777,9 +739,7 @@ elsif (!$olddir && $newdir) {
}
$newdir->{'words'} = &wsplit($newdir->{'value'});
$newdir->{'file'} = $file;
local $comments = $newdir->{'comments'} ?
scalar(@{$newdir->{'comments'}}) : 0;
$newdir->{'line'} = $addline+$comments;
$newdir->{'line'} = $addline;
$newdir->{'eline'} = $addline + scalar(@newlines) - 1;
&renumber($conf, $addline, $file, scalar(@newlines));
splice(@$pconf, $addpos, 0, $newdir);
@@ -816,14 +776,13 @@ 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'}) {
# Continue after every line used by nested members
$line = &recursive_set_lines_files($dir->{'members'},
$line+1, $file);
# Do sub-members too
&recursive_set_lines_files($dir->{'members'}, $line+1, $file);
$line += scalar(grep { $_->{'name'} ne 'dummy' }
@{$dir->{'members'}})+1;
}
$dir->{'eline'} = $line;
$line++ if ($dir->{'name'} ne 'dummy');
@@ -1913,14 +1872,15 @@ return @rv;
}
# allowed_auth_file(file)
# Returns 1 if a .htaccess file can be edited by the current user
sub allowed_auth_file
{
my ($file) = @_;
return 0 if ($file =~ /\.\./);
my $f = &server_root($file);
local $_;
return 1 if ($access{'dir'} eq '/');
return &is_under_directory($access{'dir'}, $f);
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'};
}
# directory_exists(file)
@@ -1972,7 +1932,6 @@ 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'}>");
@@ -1991,56 +1950,29 @@ return @rv;
# or undef.
sub test_config
{
return undef if ($httpd_modules{'core'} < 1.301);
my $okre = qr/(syntax|Checking).*\s+ok/i;
# Build the httpd command from the configured paths
my $httpd = &find_httpd();
my $httpdcmd = "\"$httpd\" -d \"$config{'httpd_dir'}\"";
if ($config{'httpd_conf'}) {
$httpdcmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach my $d (&get_httpd_defines()) {
$httpdcmd .= " -D$d";
}
# Pick the standard test command
my ($cmd, $apachectl);
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl, which may set up environment the config needs
$apachectl = "\"$config{'apachectl_path'}\"";
$cmd = "$apachectl configtest";
}
else {
# Test with httpd
$cmd = "$httpdcmd -t";
}
# Without a global ServerName, Apache looks up the system hostname in DNS
# while loading the config, which stalls the test when DNS is slow. So first
# try the test with a placeholder ServerName. Apache applies -C directives
# before reading the config files, so a ServerName that this check misses,
# such as one inside an IfModule section, still takes effect.
my $conf = &get_config();
if ($conf && !&find_directive("ServerName", $conf)) {
my $noname = "-C \"ServerName localhost\"";
my $out = &backquote_command(
($apachectl ? "$apachectl $noname -t" : "$httpdcmd $noname -t").
" 2>&1");
if ($apachectl && $httpd && $out =~ /no\s+longer\s+supported/i) {
# Red Hat's apachectl refuses extra arguments, and its
# configtest just runs httpd -t, so test with httpd instead
$out = &backquote_command("$httpdcmd $noname -t 2>&1");
if ($httpd_modules{'core'} >= 1.301) {
# Test the configuration with the available command
local $cmd;
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl
$cmd = "\"$config{'apachectl_path'}\" configtest";
}
else {
# Test with httpd
local $httpd = &find_httpd();
$cmd = "\"$httpd\" -d \"$config{'httpd_dir'}\" -t";
if ($config{'httpd_conf'}) {
$cmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach $d (&get_httpd_defines()) {
$cmd .= " -D$d";
}
}
local $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ /(syntax|Checking).*\s+ok/i) {
return $out;
}
return undef if ($out =~ $okre);
}
# Run the standard test. This also runs when the quick test failed, in case
# the extra arguments, rather than the config, caused the failure.
my $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ $okre) {
return $out;
}
return undef;
}
@@ -2097,7 +2029,7 @@ foreach $f (keys %before_changing) {
# (without any translation).
sub find_httpd_conf
{
my $conf = $config{'httpd_conf'};
local $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"

20
apache/config-windows Normal file
View File

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

View File

@@ -35,7 +35,6 @@ 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'});

View File

@@ -34,7 +34,6 @@ 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'});

View File

@@ -1,244 +0,0 @@
#!/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();

View File

@@ -1,91 +0,0 @@
#!/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();

View File

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

View File

@@ -147,69 +147,81 @@ else {
=head2 show_backup_destination(name, value, [local-mode])
Returns HTML for selecting a backup destination.
Returns HTML for a field for selecting a local or FTP file.
=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>";
# 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'}) ],
] },
);
# Local file field
$rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 0, undef, $mode == 0)."</td>\n";
$rv .= "<td>".&ui_tag('strong', $text{'backup_mode0'})."&nbsp;</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'})."&nbsp;</td><td>".
&ui_textbox("$_[0]_server", $mode == 1 ? $server : undef, 20).
"</td>\n";
$rv .= "<td>&nbsp;$text{'backup_path'}&nbsp;</td><td> ".
&ui_textbox("$_[0]_path", $mode == 1 ? $path : undef, 20).
"</td> </tr>\n";
$rv .= "<tr> <td></td>\n";
$rv .= "<td>$text{'backup_login'}&nbsp;</td><td> ".
&ui_textbox("$_[0]_user", $mode == 1 ? $user : undef, 20).
"</td>\n";
$rv .= "<td>&nbsp;$text{'backup_pass'}&nbsp;</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'})."&nbsp;</td><td>".
&ui_textbox("$_[0]_sserver", $mode == 2 ? $server : undef, 20).
"</td>\n";
$rv .= "<td>&nbsp;$text{'backup_path'}&nbsp;</td><td> ".
&ui_textbox("$_[0]_spath", $mode == 2 ? $path : undef, 20).
"</td> </tr>\n";
$rv .= "<tr> <td></td>\n";
$rv .= "<td>$text{'backup_login'}&nbsp;</td><td> ".
&ui_textbox("$_[0]_suser", $mode == 2 ? $user : undef, 20).
"</td>\n";
$rv .= "<td>&nbsp;$text{'backup_pass'}&nbsp;</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";
if ($_[2] == 1) {
# Uploaded file field
push(@options, { 'value' => 3, 'label' => $text{'backup_mode3'},
'content' => &ui_upload("$_[0]_upload", 40) });
$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";
}
elsif ($_[2] == 2) {
# Output to browser option
push(@options, { 'value' => 4, 'label' => $text{'backup_mode4'} });
$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";
}
return &ui_select_switch("$_[0]_mode", $mode, \@options);
$rv .= "</table>\n";
return $rv;
}
=head2 parse_backup_destination(name, &in)

View File

@@ -29,7 +29,6 @@ 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'});
@@ -108,7 +107,6 @@ else {
[ 'delete', $text{'delete'} ] ], "100%");
}
print &ui_page_end();
&ui_print_footer("", $text{'index_return'});

View File

@@ -15,7 +15,6 @@ my @mods = &list_backup_modules();
if (!@mods) {
&ui_print_endpage($text{'index_emods'});
}
print &ui_page_start();
my %mods = map { $_->{'dir'}, $_ } @mods;
# Show tabs
@@ -65,10 +64,7 @@ my $hostname = &get_system_hostname() || "localhost";
$hostname =~ s/\./-/g;
my $filename = $hostname."+configuration_backup-webmin-".
strftime("%Y-%m-%d-%H-%M", localtime);
# 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_form_start("backup.cgi/$filename.tar.gz", "post");
print &ui_table_start($text{'index_header'}, undef, 2);
my @dmods = split(/\s+/, $config{'mods'} || "");
@@ -118,6 +114,5 @@ 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'});

View File

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

View File

@@ -3734,8 +3734,7 @@ while($tries++ < 10) {
$out = &backquote_logged(
"cd ".quotemeta($dir)." && ".
"$config{'signzone'} -o ".quotemeta($dom).
($alg =~ /^(NSEC3|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA256|ECDSAP384SHA384|ED25519|ED448)/ ?
" -3 - -H 0 -u" : "").
($alg =~ /^(NSEC3|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA256|ECDSAP384SHA384|ED25519|ED448)/ ? " -3 - -u" : "").
" -f ".quotemeta($signed)." ".
quotemeta($chrootfn)." 2>&1");
last if (!$?);

61
bind8/config-windows Normal file
View File

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

View File

@@ -394,14 +394,9 @@ else {
my $dmarc = $r ? &parse_dmarc(@{$r->{'values'}}) : { };
$dmarc->{'p'} = $in{'dmarcp'};
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'});
}
$in{'dmarcpct'} =~ /^\d+$/ && $in{'dmarcpct'} >= 0 &&
$in{'dmarcpct'} <= 100 || &error($text{'edit_edmarcpct'});
$dmarc->{'pct'} = $in{'dmarcpct'};
if ($in{'dmarcsp'}) {
$dmarc->{'sp'} = $in{'dmarcsp'};

View File

@@ -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'} eq $rootdir)) {
$in{'chroot'} =~ /^\Q$rootdir\E/) {
$rootdir = undef;
}

View File

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

14
cron/config-windows Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -480,14 +480,8 @@ 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=".quotemeta($rv)."\n";
$env .= "$n='$rv'\n";
$export .= " $n";
if ($a->{'quote'}) {
$str =~ s/\$$n/"\$$n"/g;
@@ -501,30 +495,6 @@ foreach my $a (@{$cmd->{'args'}}) {
return ($env, $export, $str, $displaystr, \@vals);
}
# resolve_editor_file(template)
# Expands environment variables and keeps the path in its fixed directory.
sub resolve_editor_file
{
local ($file) = @_;
# Leave fixed paths unchanged
return $file if ($file !~ /\$/);
local $template = $file;
# Expand each supported variable once, but not variables inside its value
$file =~ s{\$\{([A-Za-z_][A-Za-z0-9_]*)\}|
\$([A-Za-z_][A-Za-z0-9_]*)}{
my $name = defined($1) ? $1 : $2;
defined($ENV{$name}) ? $ENV{$name} : "";
}gex;
# Resolve the fixed directory and full path before checking their locations
$template =~ s/\$.*$//;
$template =~ s/\/[^\/]*$// if ($template ne "/");
$template = &simplify_path(&resolve_links($template));
$file = &simplify_path(&resolve_links($file));
$template && $file && &is_under_directory($template, $file) ||
&error($text{'view_ecannot'});
return $file;
}
# list_dbi_drivers()
# Returns a list of DBI driver details, which are actually installed
sub list_dbi_drivers

View File

@@ -95,7 +95,6 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

View File

@@ -41,7 +41,8 @@ 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;

View File

@@ -13,6 +13,7 @@ $query = $in{'query'};
$text{'search_results'} . " '" .
&html_escape($query) . "'", "");
print $head;
if ($in{'caseins'}) {
$criteria = '-iname';
}
@@ -23,16 +24,6 @@ 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;

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

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