Compare commits

...

111 Commits

Author SHA1 Message Date
Ilia Ross
91d3dd843b Fix unsafe fast RPC deserialization without dropping Data::Dumper support
https://github.com/webmin/webmin/pull/2886#issuecomment-6207458026
2026-10-19 01:49:05 +02:00
Ilia Ross
5231d6de0f Drop certificate checks from RPC deserialization fix 2026-10-16 05:16:40 +02:00
Ilia Ross
ffa551a24b Fix RPC deserialization and certificate checks
This PR fixes unsafe RPC deserialization by dropping `Data::Dumper` from fast RPC, rejecting `$VAR1` input, and keeping remote objects as plain data.

Original report by Adam emailed October 11, 2026 at 05:34.
2026-10-15 15:02:39 +02:00
Jamie Cameron
dda9b972b9 Protect against SVG attachments in the mail queue
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-14 21:21:22 -07:00
Jamie Cameron
cce4d69a09 Disallow use of null bytes in PAM conversation results 2026-10-14 21:02:21 -07:00
Jamie Cameron
6a9e0afa7b Fix a couple of ordering errors
https://github.com/virtualmin/virtualmin-gpl/issues/1311
2026-10-14 20:57:09 -07:00
Jamie Cameron
658f930d69 ensure RPM output is always in english
https://github.com/virtualmin/virtualmin-gpl/issues/1311
2026-10-14 20:53:45 -07:00
Jamie Cameron
1c7781299d Merge branch 'master' of github.com:webmin/webmin 2026-10-14 20:41:34 -07:00
Jamie Cameron
6c674def60 Use standard function to split commands 2026-10-14 20:41:28 -07:00
Jamie Cameron
692cb6ea70 Merge pull request #2876 from webmin/dev/fix-symlink-loops
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Fix symlink resolution loops
2026-10-13 20:51:55 -07:00
Ilia Ross
200444d8d6 Fix SMTP EHLO handling for authentication and TLS
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
This patch fixes SMTP authentication when credentials come from the module configuration by loading them before choosing `HELO` or `EHLO`.

It also fixes the SMTP monitor's `STARTTLS` check and sends `EHLO` again after the TLS connection is established.

Jamie, I made this digging based on your latest change https://github.com/webmin/webmin/commit/65de483f3 to fix issue #2879.
2026-10-13 22:27:55 +02:00
Ilia Ross
01e454414b Fix to remove Cron env check when moving jobs
c23b6b5b48 (commitcomment-204659847)
2026-10-13 19:02:54 +02:00
Ilia Ross
b5ce991eb3 Fix non-root MySQL backup hooks
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
This patch addresses non-root MySQL backup hooks remaining unreachable after original patch https://github.com/webmin/webmin/commit/6873c65.

Now it makes before and after commands available when backups run as a configured non-root Unix user.

Root hook execution remains limited to cron-eligible Webmin users, and Usermin remains excluded from backup hooks.
2026-10-13 16:40:45 +02:00
Ilia Ross
045c7abfdf Fix privileged RPC username conversion checks
Original fix 616547c26f protected normal user creation, but conversion could still create `root`, `admin`, or `sysadm`.

Similarly, conversion now alost skips these usernames unless the admin can edit ACLs.
2026-10-13 14:31:33 +02:00
Ilia Ross
5e4b937410 Fix SQL and LDAP backend permission checks
This patch fixes permission checks on the remaining SQL and LDAP backend endpoints.

Jamie, your original fix 3d0a9aeea8 onnly updated `save_sql.cgi`, but three related files still were accepting the unrelated password-edit permission.
2026-10-13 14:17:55 +02:00
Ilia Ross
3d22baaeea Fix sandboxed MySQL and Virtualmin restores
This patch fixes a horrible regression introduced by the original sandbox patch 166cfefa28.

MariaDB sandbox mode blocks the `source` command used by Webmin and Virtualmin restores, breaking all restores.

SQL files are now passed directly to the client while preserving binary-log suppression and compatibility with older clients.
2026-10-13 13:51:44 +02:00
Jamie Cameron
502a54b6df Windows is no longer supported, so drop all config-windows files
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-12 22:26:39 -07:00
Jamie Cameron
6fec16bbd2 Merge branch 'master' of github.com:webmin/webmin 2026-10-12 21:25:16 -07:00
Jamie Cameron
65de483f37 Use EHLO when sending mail with authentication or SSL
https://github.com/webmin/webmin/issues/2879
2026-10-12 21:17:11 -07:00
Ilia Ross
222a2fd31b Fix convert to JSON to support canonical flag for independent key sorting 2026-10-13 05:26:42 +02:00
Ilia Ross
2d014758a2 Fix Cron job input validation
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
This patch fixes unsafe validation of Cron job fields before crontab entries are created.

It rejects line breaks and invalid schedule, special-time, and date values that could add unexpected fields, commands, or environment variables.

It also adds tests for malicious inputs and valid schedules.

Additional improvements to accompany. c23b6b5b48.
2026-10-12 23:13:34 +02:00
Jamie Cameron
01ce0a70d1 Make sure comments don't contain newlines
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-11 21:57:03 -07:00
Jamie Cameron
166cfefa28 If the sandbox flag is supported, use it when restoring a backup or executing an SQL file to prevent use of external commands 2026-10-11 19:42:24 -07:00
Jamie Cameron
3d0a9aeea8 Fix permissions check 2026-10-11 19:07:54 -07:00
Jamie Cameron
616547c26f Protect against creation of root-capable RPC users 2026-10-11 19:05:06 -07:00
Jamie Cameron
24db25a6c5 Harden checks for uid_def and gid_def params, and add proper translatable strings 2026-10-11 18:41:06 -07:00
Jamie Cameron
072bf2bda3 Support searching by hashed session ID 2026-10-11 18:03:51 -07:00
Jamie Cameron
8bc89757a7 Only log hashed session ID, so it cannot be used to login 2026-10-11 18:03:22 -07:00
Ilia Ross
02d2ce697f Update changelog
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-11 18:04:13 +02:00
Ilia Ross
bc9d7e67a8 Rebuild File Manager's non-Bootstrap interface on the core widgets 2026-10-11 17:57:36 +02:00
Ilia Ross
7b6c3b3857 Add dark color scheme for menus and dialogs to the standard theme 2026-10-11 17:56:47 +02:00
Ilia Ross
ba0d06743d Fix standard theme sort script dropping controls from table headings 2026-10-11 17:56:04 +02:00
Ilia Ross
759359db48 Add drop-down menu, dialog, breadcrumb and pager widgets to UI lib 2026-10-11 17:52:04 +02:00
Ilia Ross
31680372d2 Add menus tab to UI demo with menu, dialog, breadcrumb and pager examples 2026-10-11 17:46:56 +02:00
Ilia Ross
c23b6b5b48 Fix Cron environment variable ACL checks
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
This patch completes the ACL added in [17ba87cbc](https://github.com/webmin/webmin/commit/17ba87cbc).

That change protected only the edit and save pages.

Users without environment-variable access could still change variables through delete, bulk actions, and moves.

This applies the permission everywhere and hides blocked controls.

It also applies create and delete permissions to variables and prevents job and variable endpoints from being mixed.
2026-10-11 15:31:06 +02:00
Ilia Ross
0f71dac9cf Fix one-time login session keys
This patch fixes a regression from [42850e579](https://github.com/webmin/webmin/commit/42850e579).

That commit stopped miniserv from accepting raw session keys, but these helpers still stored them that way. They now store hashed keys, so one-time login links work again.
2026-10-11 15:16:04 +02:00
Ilia Ross
6b78f7d8b5 Fix Filemin archive option handling
Fixes a regression from [1e1f9982d](https://github.com/webmin/webmin/commit/1e1f9982d). That change put `--` in the wrong place for Zip, causing compression to fail.

Tar could still treat filenames as command options. This puts `--` in the correct place for both.
2026-10-11 15:12:35 +02:00
Jamie Cameron
c57711a3b5 Add missing ACL check
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-10 16:01:11 -07:00
Jamie Cameron
6c008506d9 Add missing ACL check 2026-10-10 15:57:08 -07:00
Jamie Cameron
32d20fe18c If the DBM belongs to a user, open it with that user's permissions 2026-10-10 15:51:39 -07:00
Jamie Cameron
2259de1962 Fix errors in params to execute_before/after functions 2026-10-10 15:42:00 -07:00
Jamie Cameron
6873c65c42 If MySQL backups are being done as a non-root user, also run pre and post backup commands as that user 2026-10-10 15:37:12 -07:00
Jamie Cameron
f2b94348dc Use proper variables 2026-10-10 15:28:09 -07:00
Jamie Cameron
6fecd282b8 Merge branch 'master' of github.com:webmin/webmin 2026-10-10 15:20:05 -07:00
Jamie Cameron
1e1f9982d7 Properly handle filenames starting with - 2026-10-10 15:18:44 -07:00
Ilia Ross
19e86a9657 Fix to wrap bytebox 2026-10-10 23:53:43 +02:00
Jamie Cameron
42850e5795 We never store session IDs unhashed anymore, so remove this old fallback path
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-10 13:09:45 -07:00
Ilia Ross
e99841ebc6 Fix directory symlink handling in temporary file helpers 2026-10-10 21:33:46 +02:00
Jamie Cameron
c8e06cb06c Add missing bracket 2026-10-10 10:39:43 -07:00
Jamie Cameron
2f8e484829 future changelog entry to give credit where credit is due 2026-10-10 10:28:07 -07:00
Jamie Cameron
17ba87cbc6 Add new ACL option to limit use of environment variables 2026-10-10 10:23:06 -07:00
Ilia Ross
22c31d206f Fix symlink loops when opening temporary files
This change fixes infinite loops in `open_tempfile()` and `open_lock_tempfile()` when opening cyclic symlinks.
2026-10-10 17:45:52 +02:00
Ilia Ross
f7f2fcbdb8 Fix growing symlink loops
This change fixes memory exhaustion from symlinks such as link -> link/child.
2026-10-10 17:42:51 +02:00
Ilia Ross
1a88fcdb74 Fix Netplan command expectations in tests 2026-10-10 17:33:30 +02:00
Ilia Ross
34040bd0df Fix false symlink loop detection
This patch fixes valid symlink paths being mistaken for loops by [c51f0ed4](https://github.com/webmin/webmin/commit/c51f0ed4).

It simplifies only the checked path prefix, preserving the unresolved remainder.

Original loop cases still stop quickly, while long valid chains remain supported.

I reproduced incorrect PHP selection in Virtualmin and WP Workbench, and GoAccess choosing its root log-creation branch instead of the domain-user helper.
2026-10-10 16:51:51 +02:00
Jamie Cameron
89ab870924 Merge branch 'master' of github.com:webmin/webmin
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-09 21:11:54 -07:00
Jamie Cameron
bc87da9048 Make sure variable doesn't contain a new line 2026-10-09 21:11:45 -07:00
Jamie Cameron
f9cac4c4a4 Merge pull request #2873 from webmin/dev/fix-monitor-command-hosts
Fix to show monitor hosts
2026-10-09 21:06:56 -07:00
Jamie Cameron
c51f0ed465 Ensure uniqueness by simplifying the path being checked for repitition 2026-10-09 20:58:39 -07:00
Jamie Cameron
63a07308ab No need for extra brackets around a whole command
https://github.com/webmin/webmin/issues/2875
2026-10-09 20:53:28 -07:00
Ilia Ross
51cf5c0798 Fix to show monitor hosts
This PR fixes confusing host and command options on the monitor page.

Now the host selector is always visible, even when only the local server is available.

Updated labels explain that checks run on the selected hosts and groups, while commands can run locally or on the host running the check.

Fixes: https://github.com/webmin/webmin/issues/2872
2026-10-09 16:41:22 +02:00
Ilia Ross
270ef90703 Fix page spacing around hidden elements
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-09 16:15:57 +02:00
Ilia Ross
2bf3f4834c Fix extra spacing in action log details 2026-10-09 15:41:27 +02:00
Jamie Cameron
82e7e343f5 Adding logging and use proper rename function
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-08 22:28:28 -07:00
Jamie Cameron
dea3af005f Merge branch 'master' of github.com:webmin/webmin
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-08 21:26:25 -07:00
Jamie Cameron
b4c79b1348 Exit from resolve_links if a link goes back to itself 2026-10-08 21:25:31 -07:00
Ilia Ross
8f9663fd9c Add stat tile shapes and consistent spacing 2026-10-09 04:27:59 +02:00
Ilia Ross
3bb7eeb144 Fix UI action alignment and neutral colors 2026-10-09 03:22:15 +02:00
Ilia Ross
51b5e9c15a Fix systemd validation with dependency warnings 2026-10-09 03:14:44 +02:00
Ilia Ross
5e90e1438a Fix module name validation (once again)
This patch fixes `get_module_info()` accepting module names with a trailing newline after the regex anchors were changed.

It preserves the strict validation introduced in my `2f4f017ac` to prevent loading files outside installed modules.

The check directly rejects undefined or empty names and any character outside ASCII letters, digits, underscores, and hyphens, including newlines anywhere in the name.

Regression tests pass and verify that invalid names are rejected before module lookup while valid names remain accepted.
2026-10-08 22:40:36 +02:00
Jamie Cameron
9f9a54ed51 Merge pull request #2871 from ciltocruz/fix-typo-lang-es
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
fix: typo
2026-10-08 12:56:26 -07:00
Ilia Ross
0befd2663c Update changelog
[no-build]
2026-10-08 18:30:26 +02:00
Ilia Ross
f759798e7f Add tests for terminal appearance preferences and per-user settings
[no-build]
2026-10-08 17:50:13 +02:00
Ilia Ross
cfe8d72d85 Add configurable terminal colors and per-user preferences
[no-build]
2026-10-08 17:49:54 +02:00
Ilia Ross
1fed13ba32 FIx alignment for non-standard small trash icon 2026-10-08 15:53:06 +02:00
Ilia Ross
39786bae17 Add ui_svg_icon sub to support CSS class for targeted styling 2026-10-08 15:28:28 +02:00
ciltocruz
593fb94f11 fix: typo 2026-10-08 11:47:52 +02:00
Jamie Cameron
37296f5b22 Quote params to vuserinfo
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-07 22:14:46 -07:00
Jamie Cameron
51e3dccb1d Quote DB name in commands 2026-10-07 22:07:22 -07:00
Jamie Cameron
f354238c24 Make sure module exists 2026-10-07 21:58:53 -07:00
Jamie Cameron
223b0f331f Merge branch 'master' of github.com:webmin/webmin 2026-10-07 21:57:01 -07:00
Jamie Cameron
851883c41d Use standard start and end markers, since this isn't a multi-line regexp 2026-10-07 21:56:56 -07:00
Ilia Ross
95b74cc45f Fix to drop workaround for icon alignment
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-10-08 05:58:52 +02:00
Ilia Ross
e327940bd6 Add action buttons with badge styling 2026-10-08 05:37:20 +02:00
Ilia Ross
18d5344043 Fix empty action log details
This patch fixes blank sections in action details when multiple actions are logged in one process.
2026-10-08 04:53:32 +02:00
Jamie Cameron
fd7760d2e1 Merge pull request #2870 from ste94pz/feature/dashboard-filesystem-filters
Add filesystem exclusion filters for dashboard disk usage
2026-10-07 18:32:34 -07:00
ste94pz
8b9cb8c603 feat(dashboard): add filesystem filters for dashboard visibility 2026-10-07 21:25:24 +02:00
Ilia Ross
77ffa83f8f Fix MariaDB listings with stray directories
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
ⓘ Skip invalid `#mysql50#.` entries in SQL and mysqlshow database listings so stray directories don't break the MariaDB page. Preserve valid dot-prefixed, hash-prefixed, and legacy database names.

Reproduce by creating an ordinary `.config` directory in MariaDB's data directory and opening the Webmin MariaDB module. On AlmaLinux, starting `user@27.service` also reproduces the issue because the globally enabled Podman user restart service creates `.config` and `.local` in the mysql account's home. Podman is one possible source, and the failure doesn't depend on it.

Fixes https://forum.virtualmin.com/t/error-accessing-mariadb-server-through-webmin/138118
2026-10-07 16:30:56 +02:00
Jamie Cameron
32fb7f2462 Merge pull request #2866 from webmin/dev/fix-webmin-restart-race
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Fix restart signal race
2026-10-06 20:35:56 -07:00
Ilia Ross
afd6fad51e Add Cloudmin virtualization host firewall profile 2026-10-06 23:06:08 +02:00
Ilia Ross
0be0ba411a Fix restart signal race
This PR fixes Webmin being killed when package updates trigger overlapping restarts.

I found this while upgrading packages on a Debian 13 Pro, with the upgrade finished, but Webmin stopped responding, and tracing showed a second `SIGHUP` killing it during startup.

The fix keeps restart and reload signals pending across `exec()` and installs their handlers early in startup.
2026-10-06 03:56:47 +02:00
Jamie Cameron
11be3726f7 Separate 'reporting' enabled and 'reporting mode' fields
Some checks failed
Close inactive / close-inactive (push) Has been cancelled
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-04 21:37:24 -07:00
Ilia Ross
1726be59e6 Fix XFS quota alias handling
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
This patch fixes detection and editing of native XFS quota options.

It recognizes short aliases before reboot and saves them without triggering systemd's generic quota handling.

It removes conflicting options when enabling quotas, while preserving noquota when both user and group quotas remain off.

Rel.: https://github.com/virtualmin/Virtualmin-Config/commit/cadb0db
2026-10-05 03:21:26 +02:00
Ilia Ross
61d3618cc5 Fix external IP checks to use requested address family #1300 2026-10-04 22:06:49 +02:00
Jamie Cameron
84ea802947 Merge pull request #2864 from webmin/dev/fix-button-row-forms
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Fix button row forms crossing table rows
2026-10-02 16:44:33 -07:00
Jamie Cameron
2dea2f8090 Merge pull request #2865 from webmin/dev/fix-apache-test-dns-lookup
Fix Apache config test stalling on hostname DNS lookups
2026-10-02 16:35:38 -07:00
Ilia Ross
22c580aea1 Update Apache config test to keep old behavior when ServerName is set 2026-10-02 16:35:15 +02:00
Ilia Ross
8f880ab366 Fix Squid ownership command escaping
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-02 15:14:47 +02:00
Ilia Ross
f8f35b26c6 Fix MySQL manual editor ACL bypass
This fixes missing permission checks in the MySQL manual configuration editor, with both read and write endpoints now require full configuration permissions to match the existing checks used by the configuration editor.
2026-10-02 14:16:59 +02:00
Ilia Ross
875c973658 Change button rows to use one form and table per row 2026-10-02 05:26:15 +02:00
Ilia Ross
8742ccd312 Fix Custom Commands editor path injection
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
This fixes path traversal and command injection in Custom Commands file editors.

It replaces shell-based path expansion with confined Perl expansion and validates non-path parameters.

It also applies ownership and permissions without invoking a shell.
2026-10-02 04:58:53 +02:00
Ilia Ross
94436938d0 Fix newline injection in Webmin user records 2026-10-02 03:21:58 +02:00
Ilia Ross
cec6e6074d Fix mailbox attachment option injection
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-10-02 01:24:35 +02:00
Ilia Ross
aeee5edd15 Fix Apache config test stalling on hostname DNS lookups
This PR fixes the Apache config test hanging for about 20 seconds when the server's own hostname doesn't resolve.

Without a global `ServerName`, Apache looks up the system hostname in DNS before it finishes the test, and Virtualmin runs this test every time its server list and dashboard load, so those pages look frozen.

The test now passes a placeholder `ServerName` with `-C` and runs `httpd` directly where Red Hat's `apachectl` refuses extra arguments. If this quick test fails, the original test runs, so only real config errors are reported.

On an Alma 10 VM with broken DNS, the check dropped from 20 seconds to 0.03 seconds.
2026-10-01 23:42:30 +02:00
Ilia Ross
359eb13f0e Fix button row forms crossing table rows
This PR fixes #2853, where buttons made by `ui_buttons_row` do nothing in strict browsers like Pale Moon.

Each form was placed between table rows, which HTML doesn't allow, so the browser closed it right away and left the button outside it.

Now the form sits inside the button cell, and any fields in the description cell are linked to it with the HTML5 form attribute.

The page looks exactly the same.

Fixes https://github.com/webmin/webmin/issues/2853
2026-10-01 23:35:12 +02:00
Jamie Cameron
316f1330de Differentiate between 9750 3ware controllers and other drives with 9750 in the serial number
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
https://github.com/webmin/webmin/issues/2859
2026-09-30 20:11:55 -07:00
Jamie Cameron
ff83f05768 Merge pull request #2861 from webmin/dev/fix-websocket-transfers
Fix stalled WebSocket transfers
2026-09-30 19:55:09 -07:00
Jamie Cameron
97ba9cbd46 Merge pull request #2862 from webmin/dev/full-os-version
Add full OS version detection
2026-09-30 19:53:10 -07:00
Ilia Ross
1bfd92c21c Add full OS version detection
This PR adds `real_os_version_full` so the dashboard can show Debian point releases such as 12.15.

It preserves existing version comparisons and updates the field through installation, OS refreshes, and manual settings.

Older detection caches are refreshed automatically.

Fixes: https://forum.virtualmin.com/t/os-detection-version-with-point-release-on-dashboard/138091
2026-09-30 23:40:01 +02:00
Ilia Ross
97ed7cef25 Add provenance for bundled WebSocket fixes 2026-09-30 23:00:57 +02:00
Ilia Ross
ea691b895e Fix stalled WebSocket transfers
This PR fixes WebSocket transfers that stall when unread data remains buffered.

Miniserv now drains buffered browser data before waiting for more network traffic.

The receive library avoids extra blocking reads and handles temporary read errors without disconnecting.

Tested and verified over HTTPS with Terminal and live stats on Rocky 10 and Ubuntu 26.04.

I discovered these issues while working on and testing new Cloudmin's SSH-based guest File Manager integration, which I decided to drop for now.
2026-09-30 22:46:47 +02:00
186 changed files with 7089 additions and 2314 deletions

View File

@@ -1,7 +1,23 @@
## Changelog
#### 2.671 (October, 2026)
#### 2.680 (October, 2026)
* Add color schemes to the Terminal module
* Add per-user Terminal preferences for color scheme, font size, screen reader mode and terminal dimensions
* Add protection against infinite loops resolving recursive symlinks (thanks to Carl Zuzart, Zedden I.T.)
* Add a Cloudmin virtualization host profile to the nftables module
* Add drop-down menu, dialog, breadcrumb and page link widgets to the UI library, built on native `details` and `dialog` elements
* Add confirmation dialogs for buttons and links carrying `data-ui-confirm`, replacing the browser's own prompt
* Update File Manager to use the new UI widgets in the standard theme, dropping legacy templates
* Fix standard theme sortable tables dropping controls from table headings, such as the select-all checkbox in File Manager
* Fix two-factor authentication validation to use the Webmin configuration and data directories passed by the caller
* Fix HTML escaping of module, script and session fields on the Webmin Actions Log details page
* Fix mailbox attachment option injection
* Fix path traversal and command injection in parameterized Custom Commands file editors
* Fix MySQL/MariaDB manual editor ACL bypass
* Add optional address-family selection to HTTP downloads and socket connections, preserved across redirects [#1300](https://github.com/virtualmin/virtualmin-gpl/issues/1300)
* Update the Authentic theme to the latest version with various improvements:
- Add support for Terminal module color preferences
- Add support for the new Cloudmin navigation, graphical console and live virtual machine controls
- Fix spacing and borders in widgets, tables and tab panes
#### 2.670 (September 20, 2026)
* Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

256
apache/t/test-config.t Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

299
cron/t/run-tests.t Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -142,6 +142,8 @@ while(1) {
$rawarg .= $got;
}
print STDERR "fastrpc[$$]: raw $rawarg\n" if ($gconfig{'rpcdebug'});
# Mirror the request's wire format in the response
my $dumper = substr($rawarg, 0, 5) eq '$VAR1' ? 1 : 0;
my $arg = &unserialise_variable($rawarg);

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 ".
quotemeta($full).
" -d ".quotemeta($cwd);
$cmd = "unzip $uu -q -o".
" -d ".quotemeta($cwd).
" -- ".quotemeta($full);
}
elsif ($archive_type =~ /\/x-rar|\/vnd\.rar/) {
$cmd = "unrar x -r -y ".

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 B

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

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

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

View File

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

View File

@@ -1,246 +0,0 @@
<div id="nothingSelected" class="modal">
<h3>$text{'nothing_selected'}</h3>
</div>
<div id="createFolderDialog" class="modal">
<form id="createFolderForm" action="create_folder.cgi" method="post">
<div class="form-group">
<label>$text{'new_folder_name'}</label>
<input id="name" name="name" type="text" title="$text{'provide_folder_name'}">
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="createFileDialog" class="modal">
<form id="createFileForm" action="create_file.cgi" method="post">
<div class="form-group">
<label>$text{'new_file_name'}</label>
<input name="name" type="text">
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="renameDialog" class="modal">
<form id="renameForm" action="rename.cgi" method="post">
<div class="form-group">
<label>$text{'new_name'}</label>
<input name="name" type="text">
<input type='hidden' name='file' value='' required>
<input type='hidden' name='path' value='$quote_escaped_path'>
</div>
</form>
</div>
<div id="compressDialog" class="modal">
<form id="compressSelectedForm" action="compress.cgi" method="post">
<div class="form-group">
<label>$text{'archive_name'}</label>
<input name="filename" type="text">
<select name="method">
<option value="plain-tar">.tar</option>
<option value="xz-tar">.tar.xz</option>
<option selected value="tar">.tar.gz</option>
<option value="zstd-tar">.zstd</option>
<option value="zip">.zip</option>
</select>
</div>
</form>
</div>
<div id="removeDialog" class="modal">
<p>$text{'about_to_delete'}</p>
<pre id="items-to-remove">
</pre>
$text{'are_you_sure'}
</div>
<div id="chownDialog" class="modal">
<form id="chownForm">
<div class="form-group">
<label>$text{'user_name'}</label><br>
<input name="owner" type="text"><br>
<label>$text{'group_name'}</label><br>
<input name="group" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chown-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="chattrDialog" class="modal">
<form id="chattrForm">
<div class="form-group">
<label>$text{'attr_label'}</label><br>
<input name="label" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chattr-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="chconDialog" class="modal">
<form id="chconForm">
<div class="form-group">
<label>$text{'context_label'}</label><br>
<input name="label" type="text"><br><br>
<label>$text{'dialog_recursive'}</label>
<input id="chcon-recursive" type="checkbox" name="recursive">
</div>
</form>
</div>
<div id="aclsDialog" class="modal">
<form id="aclsForm">
<div class="form-group">
<label>$text{'acls_action'}</label><br>
<select name="action">
<option value=""></option>
<option selected value="-m">Modify (-m)</option>
<option value="-x">Remove (-x)</option>
<option value="-b">Remove all extended (-b)</option>
<option value="-k">Remove default (-k)</option>
</select><br><br>
<label>$text{'acls_applyto'}</label><br>
<select multiple size=4 name="apply_to">
<option value="u">$text{'acls_user'}</option>
<option value="g">$text{'acls_group'}</option>
<option value="m">$text{'acls_mask'}</option>
<option value="o">$text{'acls_other'}</option>
</select><br><br>
<label>$text{'acls_perms'}</label><br>
<select name="perms">
<option value=""></option>
<option value="---">---</option>
<option value="r--">r--</option>
<option value="-w-">-w-</option>
<option value="--x">--x</option>
<option value="r-x">r-x</option>
<option value="rw-">rw-</option>
<option value="-wx">-wx</option>
<option value="rwx">rwx</option>
</select><br><br>
<label>$text{'acls_user'}</label><br>
$acl_user_select<br><br>
<label>$text{'acls_group'}</label><br>
$acl_group_select<br><br>
<label>$text{'dialog_recursive'}</label>
<input id="acls-recursive" type="checkbox" name="recursive">
<br><br>
$acl_manual
</div>
</form>
</div>
<div id="downFromUrlDialog" class="modal">
<form id="downFromUrlForm" action="http_download.cgi" method="post">
<div class="form-group">
<label style="display: block;">$text{'dialog_file_url'} <sup class="text-danger">&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

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

View File

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

View File

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

View File

@@ -186,7 +186,7 @@
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
html:not([data-scheme="light"]) body:not([data-scheme="light"])
:is(.ui_page, .ui_multi, .ui_toggle) {
:is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog) {
color-scheme: dark;
}
/* The root follows its body, so the scrollbars and the canvas behind
@@ -261,8 +261,8 @@ html[data-scheme="dark"] body, body[data-scheme="dark"] {
--toggle-thumb-color: #e6e8ea;
--select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3.5l3 3 3-3' fill='none' stroke='%2399a3ae' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
}
html[data-scheme="dark"] body :is(.ui_page, .ui_multi, .ui_toggle),
body[data-scheme="dark"] :is(.ui_page, .ui_multi, .ui_toggle) {
html[data-scheme="dark"] body :is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog),
body[data-scheme="dark"] :is(.ui_page, .ui_multi, .ui_toggle, .ui_dropdown, .ui_dialog) {
color-scheme: dark;
}
html[data-scheme="dark"], html:has(> body[data-scheme="dark"]) {
@@ -613,7 +613,14 @@ tr.ui_buttons_row > td {
border-top: 1px solid var(--border-color-lighter);
vertical-align: middle;
}
tr.ui_buttons_row:first-child > td { border-top: 0; }
/* Each row is a form with a table of its own, as a form cannot wrap table
* rows. Flattening the wrappers lets all rows share the outer columns */
tr.ui_buttons_form_row,
tr.ui_buttons_form_row > td,
form.ui_buttons_form,
form.ui_buttons_form > table,
form.ui_buttons_form > table > tbody { display: contents; }
tr.ui_buttons_form_row:first-child tr.ui_buttons_row > td { border-top: 0; }
td.ui_buttons_label {
width: 1%;
padding-right: 16px;

View File

@@ -28,6 +28,9 @@ function ts_makeSortable(table) {
// We have a first row: assume it's the header, and make its contents clickable links
for (var i=0;i<firstRow.cells.length;i++) {
var cell = firstRow.cells[i];
// A heading holding a control, such as a select-all checkbox, is
// left as it is : rewriting it as a sort link would drop the control
if (cell.querySelector && cell.querySelector('input, select, button, textarea')) continue;
var txt = ts_getInnerText(cell);
cell.innerHTML = '<b><a href="#" class="sortheader" '+
'onclick="ts_resortTable(this, '+i+');return false;">' +

View File

@@ -1,8 +0,0 @@
htaccess=.htaccess
htpasswd=.htpasswd
htgroups=.htgroups
sort=0
md5=0
sha1=0
digest=1
extra_directives=

View File

@@ -1,8 +0,0 @@
show_empty=1
services_file=${systemroot}/system32/drivers/etc/services
extended_inetd=0
rpc_inetd=0
sort_mode=0
ipv6=0
qm_mode=0
display_max=100

View File

@@ -1,5 +0,0 @@
reboot_command=shutdown.exe -r -t 1 -f
shutdown_command=shutdown.exe -s -t 1 -f
status_check=0
order=0
sort_mode=0

View File

@@ -16,7 +16,8 @@ while(-r $newfile) {
else { $ac = $ac."_1"; }
$newfile = &action_filename($ac);
}
`mv $oldfile $newfile`;
&rename_logged($oldfile, $newfile);
&add_rl_action($ac, $rl, $ss, $num);
&webmin_log("fix", "action", $ac);
&redirect("edit_action.cgi?0+$ac");

View File

@@ -131,6 +131,7 @@ log_rename=Renamed action $1 to $2
log_modify=Modified action $1
log_create=Created action $1
log_delete=Deleted action $1
log_fix=Fixed action $1
log_start=Started action $1
log_stop=Stopped action $1
log_restart=Restarted action $1

View File

@@ -23,6 +23,9 @@ elsif ($action eq 'create') {
elsif ($action eq 'delete') {
return &text('log_delete', "<tt>$object</tt>");
}
elsif ($action eq 'fix') {
return &text('log_fix', "<tt>$object</tt>");
}
elsif ($type eq 'action') {
return &text('log_'.$action, "<tt>$object</tt>");
}

View File

@@ -346,6 +346,12 @@ ui_page_help=Help
ui_empty_state=Nothing to display
ui_search=Search
ui_dismiss=Dismiss
ui_close=Close
ui_confirm=Please confirm
ui_breadcrumb=Breadcrumb
ui_pager=Pages
ui_pager_prev=Previous page
ui_pager_next=Next page
header_statusmsg=$1 logged into $2 $3 on $4 ($5)

View File

@@ -982,7 +982,14 @@ else {
$fromaddr .= '@'.&get_system_hostname();
}
local $qfromaddr = quotemeta($fromaddr);
local $esmtp = $flags ? 1 : 0;
# Resolve SMTP credentials and the authentication method before choosing the
# greeting because authenticated sessions require EHLO.
$user ||= $userconfig{'smtp_user'} || $config{'smtp_user'};
$pass ||= $userconfig{'smtp_pass'} || $config{'smtp_pass'};
$auth ||= $userconfig{'smtp_auth'} ||
$config{'smtp_auth'} || "Cram-MD5";
local $esmtp = $flags || $user || $ssl ? 1 : 0;
my $h = { 'fh' => 'mailboxes::MAIL' };
if ($file) {
# Just append the email to a file using mbox format
@@ -1004,28 +1011,25 @@ elsif ($sm) {
&smtp_command($h, undef, 0);
# Send SMTP HELO
# Send EHLO for ESMTP features, or HELO for basic SMTP
my $helo = $config{'helo_name'} || &get_system_hostname();
my $helocmd = $esmtp ? "ehlo $helo\r\n" : "helo $helo\r\n";
&smtp_command($h, $helocmd, 0);
if ($ssl == 2) {
# Switch to SSL with STARTTLS, if possible
# Request a TLS upgrade with STARTTLS
my $rv = &smtp_command($h, "starttls\r\n", 1);
if ($rv =~ /^2\d+/) {
# Enable TLS and repeat the greeting on the secure connection
&switch_smtp_to_ssl($h);
&smtp_command($h, $helocmd, 0);
}
else {
# Continue without TLS if STARTTLS is unavailable
$ssl = 0;
}
}
# Get username and password from parameters, or from module config
$user ||= $userconfig{'smtp_user'} || $config{'smtp_user'};
$pass ||= $userconfig{'smtp_pass'} || $config{'smtp_pass'};
$auth ||= $userconfig{'smtp_auth'} ||
$config{'smtp_auth'} || "Cram-MD5";
if ($user) {
# Send authentication commands
eval "use Authen::SASL";

View File

@@ -52,7 +52,7 @@ foreach $a (@attach) {
# Make and output the zip
$zip = &transname("$$.zip");
$out = &backquote_command(
"cd ".quotemeta($temp)." && zip ".quotemeta($zip)." * 2>&1");
"cd ".quotemeta($temp)." && zip ".quotemeta($zip)." -- * 2>&1");
if ($?) {
&error(&text('detachall_ezip', "<tt>".&html_escape($out)."</tt>"));
}

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