Compare commits

..

94 Commits
2.2.1 ... 2.2.6

Author SHA1 Message Date
Michael Kaufmann
bbda491e82 Merge remote-tracking branch 'origin/main' into v2.2 2025-03-08 11:44:18 +01:00
Michael Kaufmann
a0f179a7e7 fix typo in domain.js
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-03-08 11:43:08 +01:00
Michael Kaufmann
5afc5272d1 add missing js changes when editing domains; set version to 2.2.6 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-03-08 11:37:50 +01:00
Michael Kaufmann
8f5bd789a4 do not check for possible existing certificate in case of issue for froxlor-vhost
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-03-07 20:40:05 +01:00
Michael Kaufmann
ff64740880 Merge remote-tracking branch 'origin/main' into v2.2 2025-03-07 09:21:41 +01:00
Michael Kaufmann
0a221d0479 only show 'move to another admin' if current admin can see other admin-resources; append domainid to pagination of email-domain adresses list; check for invalid lockfile in cron management
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-03-06 09:08:51 +01:00
Michael Kaufmann
097cde13ad Merge remote-tracking branch 'origin/main' into v2.2 2025-03-05 17:12:05 +01:00
kissgyula
f90dc5854d Hungarian translation (#1310) 2025-03-03 18:29:15 +01:00
Maurice Preuß (envoyr)
8eb38a8a28 correction of autocomplete when entering passwords or access data; prevents saving of auto fill data for smtp credentials in system settings
Signed-off-by: Maurice Preuß (envoyr) <envoyr@froxlor.org>
2025-02-27 02:01:51 +01:00
Maurice Preuß (envoyr)
e17135f0c3 fix text formatting; add composer dev command for local development
Signed-off-by: Maurice Preuß (envoyr) <envoyr@froxlor.org>
2025-02-27 01:42:06 +01:00
Maurice Preuß (envoyr)
c5017786e0 audit of the npm packages and upgrade of the versions
Signed-off-by: Maurice Preuß (envoyr) <envoyr@froxlor.org>
2025-02-27 01:11:33 +01:00
Maurice Preuß (envoyr)
2e18d7c581 do not renew domains when ssl_redirect is 2 because we have already added a task to do it later when redirect is temporarily disabled
Signed-off-by: Maurice Preuß (envoyr) <envoyr@froxlor.org>
2025-02-27 00:30:04 +01:00
Michael Kaufmann
ceb7f5b23d ignore diff as well as patch files
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-08 18:42:14 +01:00
Michael Kaufmann
ebed800dec allow admins without change-serversettings to adjust dkim flag of domains, hide webserver-ssl-options for new domains if no default ssl-ip-addresses are selected in the settings; adjust visibility of possibly required dns entries for admins (domain edit)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-08 18:38:59 +01:00
Michael Kaufmann
32344e39cf fix updating of std.subdomains when changing default ip-addresses (empty value!)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-08 17:46:22 +01:00
Michael Kaufmann
e35092c31f add more detailed info in case of only suggestions on installation; correct display of required dns entries for admins
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-08 16:22:13 +01:00
Michael Kaufmann
606377f1d9 fix 'show necessary dns entries for mail/antispan also in admin-view of domain' if bind is enabled but domain is not using nameserver
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-05 13:57:00 +01:00
Michael Kaufmann
b9baeb76d5 actually insert task to reconfigure let's encrypt enabled services when triggered
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-02-05 13:33:33 +01:00
Michael Kaufmann
2f2d72851b fix plaintext-mail content, thx to AlexL
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-31 00:03:54 +01:00
Michael Kaufmann
a2925af73a fix deletion of webserver-logfiles when customer gehts deleted, thx to irisdina
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-30 13:16:06 +01:00
Michael Kaufmann
1008c015a5 Merge remote-tracking branch 'origin/main' into v2.2 2025-01-22 09:17:31 +01:00
Michael Kaufmann
2eda4ae972 adjust unit-test to new unique-admin-mail-rule
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-22 09:14:23 +01:00
Michael Kaufmann
38b2dbd81b Merge remote-tracking branch 'origin/main' into v2.2 2025-01-22 09:10:31 +01:00
Michael Kaufmann
aab98e4dae fix 1046 No database selected issue when adding customer
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-22 09:09:14 +01:00
dependabot[bot]
4b930375b7 Bump vite from 4.5.5 to 4.5.9 (#1306)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.5 to 4.5.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.9/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 08:53:37 +01:00
Michael Kaufmann
c7245d0b9b Merge remote-tracking branch 'origin/main' into v2.2 2025-01-17 08:48:56 +01:00
Michael Kaufmann
ec42003367 add safety when unsetting isemaildomain flag in domain, fixes #1305
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-12 16:53:05 +01:00
Michael Kaufmann
fde43f8060 do not output potentially unsafe content, fixes GHSA-26xq-m8xw-6373
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-12 10:27:44 +01:00
Michael Kaufmann
a43d53d540 force admin email addresses to be unique and not be used for customers, fixes GHSA-7j6w-p859-464f
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2025-01-12 10:27:02 +01:00
Michael Kaufmann
3638dc08ea add new task to (re)configure mail/ftp services with let's encrypt; refs #1297
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-24 09:34:59 +01:00
Michael Kaufmann
c2d166c866 corrected regex for dns CAA entries; fixes #1300
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-19 08:59:00 +01:00
Michael Kaufmann
0fb9357e87 set cookie SameSite option to 'Lax' for loginlinks to work as intended; fixes #1299
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-10 20:15:41 +01:00
Michael Kaufmann
26c3c87d28 Merge branch 'main' of github.com:froxlor/Froxlor 2024-12-10 08:21:14 +01:00
dependabot[bot]
0aa3e2f7b1 Bump league/commonmark from 2.5.3 to 2.6.0 (#1298)
Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.5.3 to 2.6.0.
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.6/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.5.3...2.6.0)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 08:20:53 +01:00
Michael Kaufmann
9dec83fff2 can-edit-domain is not required to create subdomains of that domain if subdomains are allowed
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-09 08:57:18 +01:00
Michael Kaufmann
a839d76d1f adjust permissions for customer global mysql user to access existing databases
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 21:20:26 +01:00
Michael Kaufmann
079047b9fe fix permissions of global mysql-user for customers; fixes #1286
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 17:01:28 +01:00
Michael Kaufmann
2bb863baac fix regex for spf entry; refs #1295
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 16:15:32 +01:00
Michael Kaufmann
8be7372d73 Merge branch 'main' of github.com:froxlor/Froxlor 2024-12-03 15:00:27 +01:00
Michael Kaufmann
dcaff3f7de set sender-address of emails which were sent using an admin/a reseller to the global settings email so sending it using provided smtp settings will not fail antispam checks; fixes #1289
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 15:00:11 +01:00
dependabot[bot]
b6dadc0d8f Bump vite from 4.5.3 to 4.5.5 (#1296)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 14:55:58 +01:00
Michael Kaufmann
665b879ac5 correctly create ssl-redirect if let's encrypt is already activated; fixes #1294
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 14:45:39 +01:00
Michael Kaufmann
60f51fd746 allow cidr (forward slash) in spf settings-regex; fixes #1295
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-03 14:35:45 +01:00
Michael Kaufmann
5bb450bccc fix empty firstname/name but set company when editing a customer via API
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-12-02 22:04:14 +01:00
Michael Kaufmann
604078ddc6 show necessary dns entries for mail/antispan also in admin-view of domain
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-11-30 11:19:25 +01:00
dependabot[bot]
b018319b8a Bump twig/twig from 3.10.3 to 3.11.2 (#1292)
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.10.3 to 3.11.2.
- [Changelog](https://github.com/twigphp/Twig/blob/v3.11.2/CHANGELOG)
- [Commits](https://github.com/twigphp/Twig/compare/v3.10.3...v3.11.2)

---
updated-dependencies:
- dependency-name: twig/twig
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 08:39:40 +01:00
Michael Kaufmann
13aa07ed1a add new settings to set default values for customer antispam options for new email addresses (settings advanced-mode)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-11-20 16:53:28 +01:00
Michael Kaufmann
4db5b09111 Merge remote-tracking branch 'origin/main' into v2.2 2024-11-02 08:53:33 +01:00
Michael Kaufmann
4f114738e7 set version to 2.2.5 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-11-02 08:53:09 +01:00
Michael Kaufmann
2c9f9ebfe2 Merge remote-tracking branch 'origin/main' into v2.2 2024-10-29 13:40:19 +01:00
Michael Kaufmann
ee986e519e corrected wrong settings-index-name, fixes #1290
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-29 13:37:05 +01:00
Michael Kaufmann
103d321003 do not use /var/run/nginx as directory for php-fpm sockets by default as it usually does not exist
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-29 13:36:31 +01:00
Michael Kaufmann
99b3deda91 fix editing email-address catchall-flag, fixes #1288 2024-10-24 12:11:39 +02:00
Michael Kaufmann
a47f8ed7ee Merge remote-tracking branch 'origin/main' into v2.2 2024-10-09 16:38:42 +02:00
Michael Kaufmann
ce841e8aa4 set version to 2.2.4 for upcoming bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-09 16:38:21 +02:00
Michael Kaufmann
86130616dd Merge remote-tracking branch 'origin/main' into v2.2 2024-10-09 16:36:25 +02:00
Michael Kaufmann
5622ce5011 add 'rewrite_subject' field to select query for rspamd config
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-09 16:21:59 +02:00
Michael Kaufmann
00bdadb6e5 Merge remote-tracking branch 'origin/main' into v2.2 2024-10-09 15:43:05 +02:00
Michael Kaufmann
05223369c5 forgot to adjust another d.domain field in email-overview
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-09 15:42:34 +02:00
Michael Kaufmann
b45ac3de3c Merge remote-tracking branch 'origin/main' into v2.2 2024-10-09 15:33:24 +02:00
Michael Kaufmann
11a5c38476 use correct field-name in email-domain-overview; set version to 2.2.3 for upcoming bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-09 15:32:57 +02:00
Michael Kaufmann
8c48c5a840 Merge remote-tracking branch 'origin/main' into v2.2 2024-10-09 10:57:40 +02:00
Michael Kaufmann
4f4abada6f set version to 2.2.2 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-09 10:55:31 +02:00
Michael Kaufmann
289d59f531 Merge remote-tracking branch 'origin/main' into v2.2 2024-10-07 14:40:26 +02:00
Lukas Bableck
f652017c1a fix APCu memory usage (#1284) 2024-10-07 14:39:56 +02:00
Michael Kaufmann
2a50eb43b3 Merge remote-tracking branch 'origin/main' into v2.2 2024-10-02 15:40:42 +02:00
Michael Kaufmann
f31c032508 add possibility to ask for potential update question in CLI updater and also pass them as options to override them; check whether mysql-user exists prior to DROP USER for mysql < 5.7 (as it is missing IF EXISTS options)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-10-02 15:27:42 +02:00
Lukas Bableck
228eb244fa Fix incorrect width of APCu Hit/Miss bar (#1283) 2024-10-01 15:57:00 +02:00
Michael Kaufmann
143d8d42b3 Merge remote-tracking branch 'origin/main' into v2.2 2024-09-28 14:51:55 +02:00
Michael Kaufmann
4ce739667d add rewrite-subject flag to email-edit form; hide spam-related settings if 'bypass_spam' is activated; add possibility to disable rejection of spam-mails, refs #1282
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-28 14:49:32 +02:00
Michael Kaufmann
eb3568fca2 Merge remote-tracking branch 'origin/main' into v2.2 2024-09-27 09:10:18 +02:00
dependabot[bot]
dda4c7a846 Bump rollup from 3.29.4 to 3.29.5 (#1280)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 09:05:37 +02:00
Michael Kaufmann
53b7c501bc Merge remote-tracking branch 'origin/main' into v2.2 2024-09-27 09:04:29 +02:00
Michael Kaufmann
c9e15bf897 do not issue let's encrypt for email_only domains (in case they were web-enabled prior, we do not unset former settings to ease reverting back when disabling email_only)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-27 09:04:01 +02:00
Michael Kaufmann
140c6c9549 store IDN email-usernames in ACE, as dovecot/postfix need them this way
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-26 09:22:11 +02:00
Michael Kaufmann
ddc439d32f Merge remote-tracking branch 'origin/main' into v2.2 2024-09-18 09:24:47 +02:00
Michael Kaufmann
40aa48a6d4 exchange toggler-links with checkboxes in email edit form to be able to adjust all parameters at once, fixes #1277
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-18 09:10:29 +02:00
Michael Kaufmann
c69b38bc42 [config-services] add validation for empty or non-existing configuration template xml files; [php-fpm] remove 'date.timezone' from php_admin_values (superfluous as it is in php_values); [antispam] set rewrite_subject to a slighty higher score then used for add_header, fixes #1275
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-10 14:11:26 +02:00
Michael Kaufmann
1fd8b88ed8 fix language replacement and fix 'sending messages' after successfully sending prior, thx to Davidd
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-09-01 13:49:27 +02:00
Michael Kaufmann
0318223fec Merge remote-tracking branch 'origin/main' into v2.2 2024-08-31 16:47:55 +02:00
Michael Kaufmann
53c414be6d fix timestamp matching regex, add lmtp to receving service regex and skip lines not including the main target service name in maillog parser
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-31 08:42:09 +02:00
Michael Kaufmann
2f7a2a32ba do not overwrite needed userinfo to avoid successful login when using email 2fa
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-31 08:41:59 +02:00
Michael Kaufmann
16d77a03cb fix timestamp matching regex, add lmtp to receving service regex and skip lines not including the main target service name in maillog parser
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-31 08:41:09 +02:00
Michael Kaufmann
05ca08c5c3 do not overwrite needed userinfo to avoid successful login when using email 2fa
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-30 16:26:55 +02:00
Michael Kaufmann
4ea7e10304 set version to 2.2.1 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-30 10:06:26 +02:00
Michael Kaufmann
3367f6dbd8 add condition to the remember-me checkbox for updaters when the token-table does not exist yet
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-29 08:19:45 +02:00
Michael Kaufmann
197eb7954a dont generate dhparam file as fallback but use defined FFDHE4096 group; fixes #1270
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-29 08:17:54 +02:00
Florian Moker
a1b6125c14 Fix missing proftpd-mod-wrap installation (#1272)
Missing Package Installation on Ubuntu Noble 24.04 - proftpd-mod-wrap, fixes #1271
2024-08-29 08:10:59 +02:00
Michael Kaufmann
d18a9c9d87 show antispam options for email-editing only if enabled
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-24 19:08:24 +02:00
Michael Kaufmann
94046ae6c7 fix storing multiple-choice-select values, thx to 21MILEX on Discord, fixes #1269
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-24 13:03:47 +02:00
Lukas Bableck
bacc6fe073 Add |raw to h5 in formfields template (#1268) 2024-08-23 11:04:30 +02:00
Michael Kaufmann
15d3dd4234 set version to 2.2.0 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-23 09:21:54 +02:00
Michael Kaufmann
54cda098c1 correctly get target filename for jqSpeciallogfileNote action call via ajax, fixes #1267
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-15 20:11:28 +02:00
Michael Kaufmann
19995f4345 fix typo in varchar length of selector field of new panel_2fa_tokens table, thx to Davidd
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2024-08-14 20:24:24 +02:00
84 changed files with 5336 additions and 1201 deletions

View File

@@ -15,7 +15,8 @@ assignees: ''
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
**System information** **System information**
* Froxlor version: $version/$gitSHA1 * Froxlor version: \$version/\$gitSHA1
* PHP sapi & version: php-fpm 8.3 / fcgid 8.0 / etc.
* Web server: apache2/nginx/lighttpd * Web server: apache2/nginx/lighttpd
* DNS server: Bind/PowerDNS (standalone)/PowerDNS (Bind-backend) * DNS server: Bind/PowerDNS (standalone)/PowerDNS (Bind-backend)
* POP/IMAP server: Courier/Dovecot * POP/IMAP server: Courier/Dovecot

View File

@@ -72,7 +72,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.x' node-version: '22.x'
- name: Install npm dependencies - name: Install npm dependencies
run: npm install run: npm install
@@ -120,7 +120,7 @@ jobs:
- name: Deploy nightly to server - name: Deploy nightly to server
uses: easingthemes/ssh-deploy@main uses: easingthemes/ssh-deploy@main
env: with:
ARGS: "-rltDzvO --chown=${{ secrets.WEB_USER }}:${{ secrets.WEB_USER }}" ARGS: "-rltDzvO --chown=${{ secrets.WEB_USER }}:${{ secrets.WEB_USER }}"
SOURCE: "dist/" SOURCE: "dist/"
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}

2
.gitignore vendored
View File

@@ -10,6 +10,7 @@ logs/*
.settings/ .settings/
.test/ .test/
*.diff *.diff
*.patch
*~ *~
.well-known .well-known
.idea .idea
@@ -23,4 +24,5 @@ templates/*
!templates/index.html !templates/index.html
!templates/Froxlor/ !templates/Froxlor/
templates/Froxlor/build/ templates/Froxlor/build/
templates/Froxlor/hot
!templates/misc/ !templates/misc/

View File

@@ -10,6 +10,7 @@ Developed by experienced server administrators, this panel simplifies the effort
## Installation ## Installation
### Fast install ### Fast install
1. Ensure that your webserver serves /var/www/html 1. Ensure that your webserver serves /var/www/html
2. Extract froxlor into /var/www/html 2. Extract froxlor into /var/www/html
3. Point your browser to http://[ip-of-webserver]/froxlor 3. Point your browser to http://[ip-of-webserver]/froxlor
@@ -24,6 +25,7 @@ If you have chosen to do the configuration by hand during the installation, you
3. Follow the steps for your services 3. Follow the steps for your services
### Detailed installation ### Detailed installation
https://docs.froxlor.org/latest/general/installation/ https://docs.froxlor.org/latest/general/installation/
## Help ## Help
@@ -49,6 +51,7 @@ May be found in [COPYING](COPYING)
## Downloads ## Downloads
### Tarball ### Tarball
https://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](https://files.froxlor.org/releases/froxlor-latest.tar.gz.md5) [SHA1](https://files.froxlor.org/releases/froxlor-latest.tar.gz.sha1) https://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](https://files.froxlor.org/releases/froxlor-latest.tar.gz.md5) [SHA1](https://files.froxlor.org/releases/froxlor-latest.tar.gz.sha1)
### Debian / Ubuntu repository ### Debian / Ubuntu repository

View File

@@ -257,7 +257,8 @@ return [
'varname' => 'mail_smtp_user', 'varname' => 'mail_smtp_user',
'type' => 'text', 'type' => 'text',
'default' => '', 'default' => '',
'save_method' => 'storeSettingField' 'save_method' => 'storeSettingField',
'autocomplete' => 'off'
], ],
'system_mail_smtp_passwd' => [ 'system_mail_smtp_passwd' => [
'label' => lng('serversettings.mail_smtp_passwd'), 'label' => lng('serversettings.mail_smtp_passwd'),
@@ -265,7 +266,8 @@ return [
'varname' => 'mail_smtp_passwd', 'varname' => 'mail_smtp_passwd',
'type' => 'password', 'type' => 'password',
'default' => '', 'default' => '',
'save_method' => 'storeSettingField' 'save_method' => 'storeSettingField',
'autocomplete' => 'new-password'
], ],
'system_apply_specialsettings_default' => [ 'system_apply_specialsettings_default' => [
'label' => lng('serversettings.apply_specialsettings_default'), 'label' => lng('serversettings.apply_specialsettings_default'),

View File

@@ -49,7 +49,7 @@ return [
], ],
'requires_reconf' => ['http'] 'requires_reconf' => ['http']
], ],
'system_apache_24' => [ 'system_apache24' => [
'label' => lng('serversettings.apache_24'), 'label' => lng('serversettings.apache_24'),
'settinggroup' => 'system', 'settinggroup' => 'system',
'varname' => 'apache24', 'varname' => 'apache24',

View File

@@ -268,7 +268,7 @@ return [
'dovecot' => 'dovecot (imap/pop3)', 'dovecot' => 'dovecot (imap/pop3)',
'proftpd' => 'proftpd (ftp)', 'proftpd' => 'proftpd (ftp)',
], ],
'save_method' => 'storeSettingField', 'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
'advanced_mode' => true 'advanced_mode' => true
], ],
'system_le_renew_hook' => [ 'system_le_renew_hook' => [
@@ -278,7 +278,7 @@ return [
'type' => 'text', 'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => 'systemctl restart postfix dovecot proftpd', 'default' => 'systemctl restart postfix dovecot proftpd',
'save_method' => 'storeSettingField', 'save_method' => 'storeSettingFieldInsertUpdateServicesTask',
'advanced_mode' => true, 'advanced_mode' => true,
'required_otp' => true 'required_otp' => true
], ],

View File

@@ -58,6 +58,51 @@ return [
'save_method' => 'storeSettingField', 'save_method' => 'storeSettingField',
'required_otp' => true 'required_otp' => true
], ],
'antispam_default_bypass_spam' => [
'label' => lng('antispam.default_bypass_spam'),
'settinggroup' => 'antispam',
'varname' => 'default_bypass_spam',
'type' => 'select',
'default' => 2,
'select_var' => [
1 => lng('antispam.default_select.on_changeable'),
2 => lng('antispam.default_select.off_changeable'),
3 => lng('antispam.default_select.on_unchangeable'),
4 => lng('antispam.default_select.off_unchangeable'),
],
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'antispam_default_spam_rewrite_subject' => [
'label' => lng('antispam.default_spam_rewrite_subject'),
'settinggroup' => 'antispam',
'varname' => 'default_spam_rewrite_subject',
'type' => 'select',
'default' => 1,
'select_var' => [
1 => lng('antispam.default_select.on_changeable'),
2 => lng('antispam.default_select.off_changeable'),
3 => lng('antispam.default_select.on_unchangeable'),
4 => lng('antispam.default_select.off_unchangeable'),
],
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'antispam_default_policy_greylist' => [
'label' => lng('antispam.default_policy_greylist'),
'settinggroup' => 'antispam',
'varname' => 'default_policy_greylist',
'type' => 'select',
'default' => 1,
'select_var' => [
1 => lng('antispam.default_select.on_changeable'),
2 => lng('antispam.default_select.off_changeable'),
3 => lng('antispam.default_select.on_unchangeable'),
4 => lng('antispam.default_select.off_unchangeable'),
],
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'antispam_dkim_keylength' => [ 'antispam_dkim_keylength' => [
'label' => lng('antispam.dkim_keylength'), 'label' => lng('antispam.dkim_keylength'),
'settinggroup' => 'antispam', 'settinggroup' => 'antispam',
@@ -84,7 +129,7 @@ return [
'settinggroup' => 'spf', 'settinggroup' => 'spf',
'varname' => 'spf_entry', 'varname' => 'spf_entry',
'type' => 'text', 'type' => 'text',
'string_regexp' => '/^v=spf[a-z0-9:~?\s.-]+$/i', 'string_regexp' => '/^v=spf[a-z0-9:~?\s\.\-\/]+$/i',
'default' => 'v=spf1 a mx -all', 'default' => 'v=spf1 a mx -all',
'save_method' => 'storeSettingField' 'save_method' => 'storeSettingField'
], ],

View File

@@ -118,7 +118,7 @@ if ($page == 'showinfo' && $userinfo['change_serversettings'] == '1') {
'uptime' => duration($cache['start_time']) 'uptime' => duration($cache['start_time'])
]; ];
$overview['mem_used_percentage'] = number_format(($overview['mem_used'] / $overview['mem_avail']) * 100, 1); $overview['mem_used_percentage'] = number_format(($overview['mem_used'] / $overview['mem_size']) * 100, 1);
$overview['num_hits_percentage'] = number_format(($overview['num_hits'] / $overview['num_hits_and_misses']) * 100, $overview['num_hits_percentage'] = number_format(($overview['num_hits'] / $overview['num_hits_and_misses']) * 100,
1); 1);
$overview['num_misses_percentage'] = number_format(($overview['num_misses'] / $overview['num_hits_and_misses']) * 100, $overview['num_misses_percentage'] = number_format(($overview['num_misses'] / $overview['num_hits_and_misses']) * 100,

View File

@@ -307,17 +307,20 @@ if (($page == 'customers' || $page == 'overview') && $userinfo['customers'] != '
$hosting_plans[$row['id']] = $row['name']; $hosting_plans[$row['id']] = $row['name'];
} }
$available_admins_stmt = Database::prepare(" $admin_select = [];
SELECT * FROM `" . TABLE_PANEL_ADMINS . "` if ($userinfo['customers_see_all'] == '1') {
WHERE (`customers` = '-1' OR `customers` > `customers_used`) $available_admins_stmt = Database::prepare("
AND adminid <> :currentadmin SELECT * FROM `" . TABLE_PANEL_ADMINS . "`
"); WHERE (`customers` = '-1' OR `customers` > `customers_used`)
Database::pexecute($available_admins_stmt, ['currentadmin' => $result['adminid']]); AND adminid <> :currentadmin
$admin_select = [ ");
0 => "---" Database::pexecute($available_admins_stmt, ['currentadmin' => $result['adminid']]);
]; $admin_select = [
while ($available_admin = $available_admins_stmt->fetch()) { 0 => "---"
$admin_select[$available_admin['adminid']] = $available_admin['name'] . " (" . $available_admin['loginname'] . ")"; ];
while ($available_admin = $available_admins_stmt->fetch()) {
$admin_select[$available_admin['adminid']] = $available_admin['name'] . " (" . $available_admin['loginname'] . ")";
}
} }
$customer_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/customer/formfield.customer_edit.php'; $customer_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/customer/formfield.customer_edit.php';

View File

@@ -319,7 +319,7 @@ if ($page == 'domains' || $page == 'overview') {
$alias_check = $alias_check['count']; $alias_check = $alias_check['count'];
$domain_emails_result_stmt = Database::prepare(" $domain_emails_result_stmt = Database::prepare("
SELECT `email`, `email_full`, `destination`, `popaccountid` AS `number_email_forwarders` SELECT `email`, `email_full`, `destination`, `popaccountid`
FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id
"); ");
Database::pexecute($domain_emails_result_stmt, [ Database::pexecute($domain_emails_result_stmt, [
@@ -593,6 +593,23 @@ if ($page == 'domains' || $page == 'overview') {
} }
echo 0; echo 0;
exit(); exit();
} elseif ($action == 'jqEmaildomainNote') {
$domainid = intval(Request::post('id'));
$newval = intval(Request::post('newval'));
try {
$json_result = Domains::getLocal($userinfo, [
'id' => $domainid
])->get();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
$result = json_decode($json_result, true)['data'];
if ((int)$newval == 0 && $newval != $result['isemaildomain']) {
echo json_encode(['changed' => true, 'info' => lng('admin.emaildomainwarning')]);
exit();
}
echo 0;
exit();
} elseif ($action == 'import') { } elseif ($action == 'import') {
if (Request::post('send') == 'send') { if (Request::post('send') == 'send') {
$separator = Validate::validate(Request::post('separator'), 'separator'); $separator = Validate::validate(Request::post('separator'), 'separator');

View File

@@ -114,7 +114,7 @@ if ($page == 'message') {
$note_msg = lng('message.norecipients'); $note_msg = lng('message.norecipients');
} else { } else {
$note_type = 'success'; $note_type = 'success';
$note_msg = str_replace('%s', $sentitems, lng('message.success')); $note_msg = lng('message.success', [$sentitems]);
} }
} }
@@ -128,7 +128,7 @@ if ($page == 'message') {
$messages_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/messages/formfield.messages_add.php'; $messages_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/messages/formfield.messages_add.php';
UI::view('user/form-note.html.twig', [ UI::view('user/form-note.html.twig', [
'formaction' => $linker->getLink(['section' => 'message']), 'formaction' => $linker->getLink(['section' => 'message', 'action' => '']),
'formdata' => $messages_add_data['messages_add'], 'formdata' => $messages_add_data['messages_add'],
'actions_links' => [ 'actions_links' => [
[ [

View File

@@ -76,6 +76,11 @@
"ext-apcu": "*", "ext-apcu": "*",
"ext-readline": "*" "ext-readline": "*"
}, },
"config": {
"platform": {
"php": "7.4"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Froxlor\\": [ "Froxlor\\": [
@@ -84,6 +89,10 @@
} }
}, },
"scripts": { "scripts": {
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#fdba74\" \"php -S 127.0.0.1:8000\" \"npm run dev\" --names=server,vite"
],
"post-install-cmd": "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi", "post-install-cmd": "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi",
"post-update-cmd" : "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi" "post-update-cmd" : "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi"
} }

601
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -141,7 +141,6 @@ if ($page == 'overview' || $page == 'domains') {
WHERE `customerid` = :customerid WHERE `customerid` = :customerid
AND `parentdomainid` = '0' AND `parentdomainid` = '0'
AND `email_only` = '0' AND `email_only` = '0'
AND `caneditdomain` = '1'
AND `deactivated` = '0' AND `deactivated` = '0'
ORDER BY `domain` ASC"); ORDER BY `domain` ASC");
Database::pexecute($stmt, [ Database::pexecute($stmt, [

View File

@@ -30,7 +30,6 @@ use Froxlor\Api\Commands\EmailAccounts;
use Froxlor\Api\Commands\EmailDomains; use Froxlor\Api\Commands\EmailDomains;
use Froxlor\Api\Commands\EmailForwarders; use Froxlor\Api\Commands\EmailForwarders;
use Froxlor\Api\Commands\Emails; use Froxlor\Api\Commands\Emails;
use Froxlor\Cron\Mail\Rspamd;
use Froxlor\CurrentUser; use Froxlor\CurrentUser;
use Froxlor\Database\Database; use Froxlor\Database\Database;
use Froxlor\FroxlorLogger; use Froxlor\FroxlorLogger;
@@ -105,7 +104,7 @@ if ($page == 'email_domain') {
$email_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.emails.php'; $email_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.emails.php';
$collection = (new Collection(Emails::class, $userinfo, $sql_search)) $collection = (new Collection(Emails::class, $userinfo, $sql_search))
->withPagination($email_list_data['email_list']['columns'], ->withPagination($email_list_data['email_list']['columns'],
$email_list_data['email_list']['default_sorting']); $email_list_data['email_list']['default_sorting'], ['domainid=' . $email_domainid]);
} catch (Exception $e) { } catch (Exception $e) {
Response::dynamicError($e->getMessage()); Response::dynamicError($e->getMessage());
} }
@@ -247,11 +246,7 @@ if ($page == 'email_domain') {
if (isset($result['email']) && $result['email'] != '') { if (isset($result['email']) && $result['email'] != '') {
if (Request::post('send') == 'send') { if (Request::post('send') == 'send') {
try { try {
Emails::getLocal($userinfo, [ Emails::getLocal($userinfo, Request::postAll())->update();
'id' => $id,
'spam_tag_level' => Request::post('spam_tag_level', Rspamd::DEFAULT_MARK_LVL),
'spam_kill_level' => Request::post('spam_kill_level', Rspamd::DEFAULT_REJECT_LVL)
])->update();
} catch (Exception $e) { } catch (Exception $e) {
Response::dynamicError($e->getMessage()); Response::dynamicError($e->getMessage());
} }
@@ -291,88 +286,12 @@ if ($page == 'email_domain') {
$email_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_edit.php'; $email_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_edit.php';
if (Settings::Get('catchall.catchall_enabled') != '1') {
unset($email_edit_data['emails_edit']['sections']['section_a']['fields']['mail_catchall']);
}
UI::view('user/form.html.twig', [ UI::view('user/form.html.twig', [
'formaction' => $linker->getLink(['section' => 'email']), 'formaction' => $linker->getLink(['section' => 'email']),
'formdata' => $email_edit_data['emails_edit'], 'formdata' => $email_edit_data['emails_edit'],
'editid' => $id 'editid' => $id
]); ]);
} }
} elseif ($action == 'togglebypass' && $id != 0) {
try {
$json_result = Emails::getLocal($userinfo, [
'id' => $id
])->get();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
$result = json_decode($json_result, true)['data'];
try {
Emails::getLocal($userinfo, [
'id' => $id,
'bypass_spam' => ($result['bypass_spam'] == '1' ? 0 : 1)
])->update();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
Response::redirectTo($filename, [
'page' => $page,
'domainid' => $email_domainid,
'action' => 'edit',
'id' => $id,
]);
} elseif ($action == 'togglegreylist' && $id != 0) {
try {
$json_result = Emails::getLocal($userinfo, [
'id' => $id
])->get();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
$result = json_decode($json_result, true)['data'];
try {
Emails::getLocal($userinfo, [
'id' => $id,
'policy_greylist' => ($result['policy_greylist'] == '1' ? 0 : 1)
])->update();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
Response::redirectTo($filename, [
'page' => $page,
'domainid' => $email_domainid,
'action' => 'edit',
'id' => $id,
]);
} elseif ($action == 'togglecatchall' && $id != 0) {
try {
$json_result = Emails::getLocal($userinfo, [
'id' => $id
])->get();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
$result = json_decode($json_result, true)['data'];
try {
Emails::getLocal($userinfo, [
'id' => $id,
'iscatchall' => ($result['iscatchall'] == '1' ? 0 : 1)
])->update();
} catch (Exception $e) {
Response::dynamicError($e->getMessage());
}
Response::redirectTo($filename, [
'page' => $page,
'domainid' => $email_domainid,
'action' => 'edit',
'id' => $id,
]);
} }
} elseif ($page == 'accounts') { } elseif ($page == 'accounts') {
$email_domainid = Request::any('domainid', 0); $email_domainid = Request::any('domainid', 0);

View File

@@ -228,7 +228,7 @@ if ($page == 'overview' || $page == 'mysqls') {
$new_password = Crypt::validatePassword(Request::post('mysql_password')); $new_password = Crypt::validatePassword(Request::post('mysql_password'));
foreach ($allowed_mysqlservers as $dbserver) { foreach ($allowed_mysqlservers as $dbserver) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// get DbManager // get DbManager
$dbm = new DbManager($log); $dbm = new DbManager($log);
// give permission to the user on every access-host we have // give permission to the user on every access-host we have

View File

@@ -111,7 +111,7 @@ if ($action == '2fa_entercode') {
// when using email-2fa, remove the one-time-code // when using email-2fa, remove the one-time-code
if ($userinfo['type_2fa'] == '1') { if ($userinfo['type_2fa'] == '1') {
$del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid"); $del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
$userinfo = Database::pexecute_first($del_stmt, [ Database::pexecute_first($del_stmt, [
'uid' => $uid 'uid' => $uid
]); ]);
} }

View File

@@ -95,6 +95,7 @@ CREATE TABLE `mail_virtual` (
`iscatchall` tinyint(1) unsigned NOT NULL default '0', `iscatchall` tinyint(1) unsigned NOT NULL default '0',
`description` varchar(255) NOT NULL DEFAULT '', `description` varchar(255) NOT NULL DEFAULT '',
`spam_tag_level` float(4,1) NOT NULL DEFAULT 7.0, `spam_tag_level` float(4,1) NOT NULL DEFAULT 7.0,
`rewrite_subject` tinyint(1) NOT NULL default '1',
`spam_kill_level` float(4,1) NOT NULL DEFAULT 14.0, `spam_kill_level` float(4,1) NOT NULL DEFAULT 14.0,
`bypass_spam` tinyint(1) NOT NULL default '0', `bypass_spam` tinyint(1) NOT NULL default '0',
`policy_greylist` tinyint(1) NOT NULL default '1', `policy_greylist` tinyint(1) NOT NULL default '1',
@@ -390,6 +391,9 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES
('antispam', 'config_file', '/etc/rspamd/local.d/froxlor_settings.conf'), ('antispam', 'config_file', '/etc/rspamd/local.d/froxlor_settings.conf'),
('antispam', 'reload_command', 'service rspamd restart'), ('antispam', 'reload_command', 'service rspamd restart'),
('antispam', 'dkim_keylength', '1024'), ('antispam', 'dkim_keylength', '1024'),
('antispam', 'default_bypass_spam', '2'),
('antispam', 'default_spam_rewrite_subject', '1'),
('antispam', 'default_policy_greylist', '1'),
('admin', 'show_news_feed', '0'), ('admin', 'show_news_feed', '0'),
('admin', 'show_version_login', '0'), ('admin', 'show_version_login', '0'),
('admin', 'show_version_footer', '0'), ('admin', 'show_version_footer', '0'),
@@ -495,7 +499,6 @@ opcache.save_comments
opcache.use_cwd opcache.use_cwd
opcache.fast_shutdown'), opcache.fast_shutdown'),
('phpfpm', 'ini_admin_values', 'cgi.redirect_status_env ('phpfpm', 'ini_admin_values', 'cgi.redirect_status_env
date.timezone
disable_classes disable_classes
disable_functions disable_functions
error_log error_log
@@ -730,8 +733,8 @@ opcache.validate_timestamps'),
('panel', 'logo_overridecustom', '0'), ('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'), ('panel', 'settings_mode', '0'),
('panel', 'menu_collapsed', '1'), ('panel', 'menu_collapsed', '1'),
('panel', 'version', '2.2.1'), ('panel', 'version', '2.2.6'),
('panel', 'db_version', '202408140'); ('panel', 'db_version', '202412030');
DROP TABLE IF EXISTS `panel_tasks`; DROP TABLE IF EXISTS `panel_tasks`;

View File

@@ -24,7 +24,9 @@
*/ */
use Froxlor\Database\Database; use Froxlor\Database\Database;
use Froxlor\Database\DbManager;
use Froxlor\Froxlor; use Froxlor\Froxlor;
use Froxlor\FroxlorLogger;
use Froxlor\Install\Update; use Froxlor\Install\Update;
use Froxlor\Settings; use Froxlor\Settings;
@@ -169,3 +171,82 @@ if (Froxlor::isFroxlorVersion('2.2.0')) {
Update::showUpdateStep("Updating from 2.2.0 to 2.2.1", false); Update::showUpdateStep("Updating from 2.2.0 to 2.2.1", false);
Froxlor::updateToVersion('2.2.1'); Froxlor::updateToVersion('2.2.1');
} }
if (Froxlor::isDatabaseVersion('202408140')) {
Update::showUpdateStep("Adding new rewrite-subject field to email table");
Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `rewrite_subject` tinyint(1) NOT NULL default '1' AFTER `spam_tag_level`;");
Update::lastStepStatus(0);
Froxlor::updateToDbVersion('202409280');
}
if (Froxlor::isFroxlorVersion('2.2.1')) {
Update::showUpdateStep("Updating from 2.2.1 to 2.2.2", false);
Froxlor::updateToVersion('2.2.2');
}
if (Froxlor::isFroxlorVersion('2.2.2')) {
Update::showUpdateStep("Updating from 2.2.2 to 2.2.3", false);
Froxlor::updateToVersion('2.2.3');
}
if (Froxlor::isFroxlorVersion('2.2.3')) {
Update::showUpdateStep("Updating from 2.2.3 to 2.2.4", false);
Froxlor::updateToVersion('2.2.4');
}
if (Froxlor::isFroxlorVersion('2.2.4')) {
Update::showUpdateStep("Updating from 2.2.4 to 2.2.5", false);
Froxlor::updateToVersion('2.2.5');
}
if (Froxlor::isDatabaseVersion('202409280')) {
Update::showUpdateStep("Adding new antispam settings");
Settings::AddNew("antispam.default_bypass_spam", "2");
Settings::AddNew("antispam.default_spam_rewrite_subject", "1");
Settings::AddNew("antispam.default_policy_greylist", "1");
Update::lastStepStatus(0);
Froxlor::updateToDbVersion('202411200');
}
if (Froxlor::isDatabaseVersion('202411200')) {
Update::showUpdateStep("Adjusting customer mysql global user");
// get all customers that are not deactivated and that have at least one database (hence a global database-user)
$customers = Database::query("
SELECT DISTINCT c.loginname, c.allowed_mysqlserver
FROM `" . TABLE_PANEL_CUSTOMERS . "` c
LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid
WHERE c.deactivated = '0' AND d.id IS NOT NULL
");
while ($customer = $customers->fetch(\PDO::FETCH_ASSOC)) {
$current_allowed_mysqlserver = !empty($customer['allowed_mysqlserver']) ? json_decode($customer['allowed_mysqlserver'], true) : [];
foreach ($current_allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server
Database::needRoot(true, $dbserver, true);
// get DbManager
$dbm = new DbManager(FroxlorLogger::getInstanceOf());
foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
if ($dbm->getManager()->userExistsOnHost($customer['loginname'], $mysql_access_host)) {
// deactivate temporarily
$dbm->getManager()->disableUser($customer['loginname'], $mysql_access_host);
// re-enable
$dbm->getManager()->enableUser($customer['loginname'], $mysql_access_host, true);
}
}
$dbm->getManager()->flushPrivileges();
Database::needRoot(false);
}
}
Update::lastStepStatus(0);
Froxlor::updateToDbVersion('202412030');
}
if (Froxlor::isFroxlorVersion('2.2.5')) {
Update::showUpdateStep("Updating from 2.2.5 to 2.2.6", false);
Froxlor::updateToVersion('2.2.6');
}

View File

@@ -34,7 +34,7 @@ $return = [];
if (Update::versionInUpdate($current_db_version, '202004140')) { if (Update::versionInUpdate($current_db_version, '202004140')) {
$has_preconfig = true; $has_preconfig = true;
$description = 'Froxlor can now optionally validate the dns entries of domains that request Lets Encrypt certificates to reduce dns-related problems (e.g. freshly registered domain or updated a-record).'; $description = 'Froxlor can now optionally validate the dns entries of domains that request Lets Encrypt certificates to reduce dns-related problems (e.g. freshly registered domain or updated a-record).';
$question = '<strong>Validate DNS of domains when using Lets Encrypt&nbsp;'; $question = '<strong>Validate DNS of domains when using Lets Encrypt</strong>';
$return['system_le_domain_dnscheck'] = [ $return['system_le_domain_dnscheck'] = [
'type' => 'checkbox', 'type' => 'checkbox',
'value' => 1, 'value' => 1,

View File

@@ -287,6 +287,15 @@ class Admins extends ApiCommand implements ResourceEntity
'login' => $loginname 'login' => $loginname
], true, true); ], true, true);
// Check for existing email address
// do not check via api as we skip any permission checks for this task
$email_check_admin_stmt = Database::prepare("
SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email
");
$email_check_admin = Database::pexecute_first($email_check_admin_stmt, [
'email' => $email
], true, true);
if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) { if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) {
Response::standardError('loginnameexists', $loginname, true); Response::standardError('loginnameexists', $loginname, true);
} elseif (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) { } elseif (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) {
@@ -298,6 +307,8 @@ class Admins extends ApiCommand implements ResourceEntity
Response::standardError('loginnameiswrong', $loginname, true); Response::standardError('loginnameiswrong', $loginname, true);
} elseif (!Validate::validateEmail($email)) { } elseif (!Validate::validateEmail($email)) {
Response::standardError('emailiswrong', $email, true); Response::standardError('emailiswrong', $email, true);
} elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) {
Response::standardError('emailexists', $email, true);
} else { } else {
if ($customers_see_all != '1') { if ($customers_see_all != '1') {
$customers_see_all = '0'; $customers_see_all = '0';
@@ -610,8 +621,20 @@ class Admins extends ApiCommand implements ResourceEntity
'admin.email' 'admin.email'
], '', true); ], '', true);
} }
// Check for existing email address
// do not check via api as we skip any permission checks for this task
$email_check_admin_stmt = Database::prepare("
SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email and `adminid` <> :adminid
");
$email_check_admin = Database::pexecute_first($email_check_admin_stmt, [
'email' => $email,
'adminid' => $id,
], true, true);
if (!Validate::validateEmail($email)) { if (!Validate::validateEmail($email)) {
Response::standardError('emailiswrong', $email, true); Response::standardError('emailiswrong', $email, true);
} elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) {
Response::standardError('emailexists', $email, true);
} else { } else {
if ($deactivated != '1') { if ($deactivated != '1') {
$deactivated = '0'; $deactivated = '0';

View File

@@ -505,6 +505,15 @@ class Customers extends ApiCommand implements ResourceEntity
'login' => $loginname 'login' => $loginname
], true, true); ], true, true);
// Check for existing email address
// do not check via api as we skip any permission checks for this task
$email_check_admin_stmt = Database::prepare("
SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email
");
$email_check_admin = Database::pexecute_first($email_check_admin_stmt, [
'email' => $email
], true, true);
$mysql_maxlen = Database::getSqlUsernameLength() - strlen(Settings::Get('customer.mysqlprefix')); $mysql_maxlen = Database::getSqlUsernameLength() - strlen(Settings::Get('customer.mysqlprefix'));
if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) { if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) {
Response::standardError('loginnameexists', $loginname, true); Response::standardError('loginnameexists', $loginname, true);
@@ -514,6 +523,8 @@ class Customers extends ApiCommand implements ResourceEntity
} else { } else {
Response::standardError('loginnameiswrong', $loginname, true); Response::standardError('loginnameiswrong', $loginname, true);
} }
} elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) {
Response::standardError('emailexistsanon', $email, true);
} }
$guid = intval(Settings::Get('system.lastguid')) + 1; $guid = intval(Settings::Get('system.lastguid')) + 1;
@@ -738,11 +749,12 @@ class Customers extends ApiCommand implements ResourceEntity
'adminid' => $this->getUserDetail('adminid'), 'adminid' => $this->getUserDetail('adminid'),
'docroot' => $documentroot, 'docroot' => $documentroot,
'phpenabled' => $phpenabled, 'phpenabled' => $phpenabled,
'openbasedir' => '1' 'openbasedir' => '1',
'is_stdsubdomain' => 1
]; ];
$domainid = -1; $domainid = -1;
try { try {
$std_domain = $this->apiCall('Domains.add', $ins_data); $std_domain = $this->apiCall('Domains.add', $ins_data, true);
$domainid = $std_domain['id']; $domainid = $std_domain['id'];
} catch (Exception $e) { } catch (Exception $e) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage()); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage());
@@ -765,7 +777,7 @@ class Customers extends ApiCommand implements ResourceEntity
if ($mysqls != 0) { if ($mysqls != 0) {
foreach ($allowed_mysqlserver as $dbserver) { foreach ($allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// get DbManager // get DbManager
$dbm = new DbManager($this->logger()); $dbm = new DbManager($this->logger());
// give permission to the user on every access-host we have // give permission to the user on every access-host we have
@@ -827,7 +839,7 @@ class Customers extends ApiCommand implements ResourceEntity
try { try {
$this->mailer()->Subject = $mail_subject; $this->mailer()->Subject = $mail_subject;
$this->mailer()->AltBody = $mail_body; $this->mailer()->AltBody = $mail_body;
$this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body)); $this->mailer()->Body = str_replace("\n", "<br />", $mail_body);
$this->mailer()->addAddress($email, User::getCorrectUserSalutation([ $this->mailer()->addAddress($email, User::getCorrectUserSalutation([
'firstname' => $firstname, 'firstname' => $firstname,
'name' => $name, 'name' => $name,
@@ -1105,7 +1117,7 @@ class Customers extends ApiCommand implements ResourceEntity
$email = $this->getParam('email', true, $idna_convert->decode($result['email'])); $email = $this->getParam('email', true, $idna_convert->decode($result['email']));
$name = $this->getParam('name', true, $result['name']); $name = $this->getParam('name', true, $result['name']);
$firstname = $this->getParam('firstname', true, $result['firstname']); $firstname = $this->getParam('firstname', true, $result['firstname']);
$company_required = (!empty($name) && empty($firstname)) || (empty($name) && !empty($firstname)) || (empty($name) && empty($firstname)); $company_required = ((!empty($name) && empty($firstname)) || (empty($name) && !empty($firstname)) || (empty($name) && empty($firstname))) && empty($result['company']);
$company = $this->getParam('company', !$company_required, $result['company']); $company = $this->getParam('company', !$company_required, $result['company']);
$street = $this->getParam('street', true, $result['street']); $street = $this->getParam('street', true, $result['street']);
$zipcode = $this->getParam('zipcode', true, $result['zipcode']); $zipcode = $this->getParam('zipcode', true, $result['zipcode']);
@@ -1242,6 +1254,18 @@ class Customers extends ApiCommand implements ResourceEntity
], '', true); ], '', true);
} elseif (!Validate::validateEmail($email)) { } elseif (!Validate::validateEmail($email)) {
Response::standardError('emailiswrong', $email, true); Response::standardError('emailiswrong', $email, true);
} else {
// Check for existing email address
// do not check via api as we skip any permission checks for this task
$email_check_admin_stmt = Database::prepare("
SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email
");
$email_check_admin = Database::pexecute_first($email_check_admin_stmt, [
'email' => $email
], true, true);
if ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) {
Response::standardError('emailexistsanon', $email, true);
}
} }
} }
@@ -1346,7 +1370,7 @@ class Customers extends ApiCommand implements ResourceEntity
$current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : []; $current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : [];
foreach ($current_allowed_mysqlserver as $dbserver) { foreach ($current_allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// get DbManager // get DbManager
$dbm = new DbManager($this->logger()); $dbm = new DbManager($this->logger());
foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {

View File

@@ -178,7 +178,7 @@ class DomainZones extends ApiCommand implements ResourceEntity
} }
} }
} elseif ($type == 'CAA' && !empty($content)) { } elseif ($type == 'CAA' && !empty($content)) {
$re = '/(?\'critical\'\d)\h*(?\'type\'iodef|issue|issuewild)\h*(?\'value\'(?\'issuevalue\'"(?\'domain\'(?=.{3,128}$)(?>(?>[a-zA-Z0-9]+[a-zA-Z0-9-]*[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)*(?>[a-zA-Z]{2,}|[a-zA-Z0-9]{2,}\.[a-zA-Z]{2,}))[;\h]*(?\'parameters\'(?>[a-zA-Z0-9]{1,60}=[a-zA-Z0-9]{1,60}\h*)+)?")|(?\'iodefvalue\'"(?\'url\'(mailto:.*|http:\/\/.*|https:\/\/.*))"))/'; $re = '/(?\'critical\'\d+)\h*(?\'type\'iodef|issue|issuewild)\h*(?\'value\'(?\'issuevalue\'"(?\'domain\'(?=.{3,128}$)(?>(?>[a-zA-Z0-9]+[a-zA-Z0-9-]*[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)*(?>[a-zA-Z]{2,}|[a-zA-Z0-9]{2,}\.[a-zA-Z]{2,}))[;\h]*(?\'parameters\'(?>[a-zA-Z0-9]{1,60}=[a-zA-Z0-9:\.\/\-]{1,60}\h*)+)?")|(?\'iodefvalue\'"(?\'url\'(mailto:.*|http:\/\/.*|https:\/\/.*))"))/';
preg_match($re, $content, $matches); preg_match($re, $content, $matches);
if (empty($matches)) { if (empty($matches)) {

View File

@@ -201,7 +201,7 @@ class Domains extends ApiCommand implements ResourceEntity
* @param string $zonefile * @param string $zonefile
* optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated) * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated)
* @param bool $dkim * @param bool $dkim
* optional, currently not in use, default 0 (false) * optional, whether this domain should use dkim if antispam is activated, default 0 (false)
* @param string $specialsettings * @param string $specialsettings
* optional, custom webserver vhost-content which is added to the generated vhost, default empty * optional, custom webserver vhost-content which is added to the generated vhost, default empty
* @param string $ssl_specialsettings * @param string $ssl_specialsettings
@@ -274,7 +274,8 @@ class Domains extends ApiCommand implements ResourceEntity
* $override_tls is true * $override_tls is true
* @param string $description * @param string $description
* optional custom description (currently not used/shown in the frontend), default empty * optional custom description (currently not used/shown in the frontend), default empty
* * @param bool $is_stdsubdomain (internally)
* optional whether this is a standard subdomain for a customer which is being added so no usage is decreased
* @access admin * @access admin
* @return string json-encoded array * @return string json-encoded array
* @throws Exception * @throws Exception
@@ -282,7 +283,8 @@ class Domains extends ApiCommand implements ResourceEntity
public function add() public function add()
{ {
if ($this->isAdmin()) { if ($this->isAdmin()) {
if ($this->getUserDetail('domains_used') < $this->getUserDetail('domains') || $this->getUserDetail('domains') == '-1') { $is_stdsubdomain = $this->isInternal() ? $this->getBoolParam('is_stdsubdomain', true, 0) : false;
if ($is_stdsubdomain || $this->getUserDetail('domains_used') < $this->getUserDetail('domains') || $this->getUserDetail('domains') == '-1') {
// parameters // parameters
$p_domain = $this->getParam('domain'); $p_domain = $this->getParam('domain');
@@ -472,7 +474,6 @@ class Domains extends ApiCommand implements ResourceEntity
} }
$caneditdomain = '1'; $caneditdomain = '1';
$zonefile = ''; $zonefile = '';
$dkim = '0';
$specialsettings = ''; $specialsettings = '';
$ssl_specialsettings = ''; $ssl_specialsettings = '';
$include_specialsettings = 0; $include_specialsettings = 0;
@@ -548,8 +549,11 @@ class Domains extends ApiCommand implements ResourceEntity
} }
} }
if (Settings::Get('system.use_ssl') == "1" && $sslenabled == 1 && empty($ssl_ipandports)) { if (Settings::Get('system.use_ssl') == "1" && $sslenabled == 1 && empty($ssl_ipandports)) {
// enabled ssl for the domain but no ssl ip/port is selected // if this is a customer standard-subdomain, we simply ignore this and disable ssl-related settings (see if-statement below)
Response::standardError('nosslippportgiven', '', true); if (!$is_stdsubdomain) {
// enabled ssl for the domain but no ssl ip/port is selected
Response::standardError('nosslippportgiven', '', true);
}
} }
if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports)) { if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports)) {
$ssl_redirect = 0; $ssl_redirect = 0;
@@ -795,12 +799,15 @@ class Domains extends ApiCommand implements ResourceEntity
$ins_data['id'] = $domainid; $ins_data['id'] = $domainid;
unset($ins_data); unset($ins_data);
$upd_stmt = Database::prepare(" if (!$is_stdsubdomain) {
UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1 $upd_stmt = Database::prepare("
WHERE `adminid` = :adminid"); UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1
Database::pexecute($upd_stmt, [ WHERE `adminid` = :adminid
'adminid' => $adminid ");
], true, true); Database::pexecute($upd_stmt, [
'adminid' => $adminid
], true, true);
}
$ins_stmt = Database::prepare(" $ins_stmt = Database::prepare("
INSERT INTO `" . TABLE_DOMAINTOIP . "` SET INSERT INTO `" . TABLE_DOMAINTOIP . "` SET
@@ -1058,6 +1065,9 @@ class Domains extends ApiCommand implements ResourceEntity
* (default yes), 3 = always, default 0 (never) * (default yes), 3 = always, default 0 (never)
* @param bool $isemaildomain * @param bool $isemaildomain
* optional, allow email usage with this domain, default 0 (false) * optional, allow email usage with this domain, default 0 (false)
* @param bool $emaildomainverified
* optional, when setting $isemaildomain to false, this needs to be set to true to confirm the action in case email addresses exist for this domain,
* default 0 (false)
* @param bool $email_only * @param bool $email_only
* optional, restrict domain to email usage, default 0 (false) * optional, restrict domain to email usage, default 0 (false)
* @param int $selectserveralias * @param int $selectserveralias
@@ -1080,7 +1090,7 @@ class Domains extends ApiCommand implements ResourceEntity
* @param string $zonefile * @param string $zonefile
* optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated) * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated)
* @param bool $dkim * @param bool $dkim
* optional, currently not in use, default 0 (false) * optional, whether this domain should use dkim if antispam is activated, default 0 (false)
* @param string $specialsettings * @param string $specialsettings
* optional, custom webserver vhost-content which is added to the generated vhost, default empty * optional, custom webserver vhost-content which is added to the generated vhost, default empty
* @param string $ssl_specialsettings * @param string $ssl_specialsettings
@@ -1185,6 +1195,7 @@ class Domains extends ApiCommand implements ResourceEntity
$subcanemaildomain = $this->getParam('subcanemaildomain', true, $result['subcanemaildomain']); $subcanemaildomain = $this->getParam('subcanemaildomain', true, $result['subcanemaildomain']);
$isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']); $isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']);
$emaildomainverified = $this->getBoolParam('emaildomainverified', true, 0);
$email_only = $this->getBoolParam('email_only', true, $result['email_only']); $email_only = $this->getBoolParam('email_only', true, $result['email_only']);
$p_serveraliasoption = $this->getParam('selectserveralias', true, -1); $p_serveraliasoption = $this->getParam('selectserveralias', true, -1);
$speciallogfile = $this->getBoolParam('speciallogfile', true, $result['speciallogfile']); $speciallogfile = $this->getBoolParam('speciallogfile', true, $result['speciallogfile']);
@@ -1268,7 +1279,7 @@ class Domains extends ApiCommand implements ResourceEntity
// count where we are used in email-accounts // count where we are used in email-accounts
$domain_emails_result_stmt = Database::prepare(" $domain_emails_result_stmt = Database::prepare("
SELECT `email`, `email_full`, `destination`, `popaccountid` AS `number_email_forwarders` SELECT `email`, `email_full`, `destination`, `popaccountid`
FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id
"); ");
Database::pexecute($domain_emails_result_stmt, [ Database::pexecute($domain_emails_result_stmt, [
@@ -1291,6 +1302,10 @@ class Domains extends ApiCommand implements ResourceEntity
} }
} }
if ($emails > 0 && (int)$isemaildomain == 0 && (int)$result['isemaildomain'] == 1 && (int)$emaildomainverified == 0) {
Response::standardError('emaildomainstillhasaddresses', '', true);
}
// handle change of customer (move domain from customer to customer) // handle change of customer (move domain from customer to customer)
if ($customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') { if ($customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') {
// check whether target customer has enough resources // check whether target customer has enough resources
@@ -1447,7 +1462,6 @@ class Domains extends ApiCommand implements ResourceEntity
} else { } else {
$isbinddomain = $result['isbinddomain']; $isbinddomain = $result['isbinddomain'];
$zonefile = $result['zonefile']; $zonefile = $result['zonefile'];
$dkim = $result['dkim'];
$specialsettings = $result['specialsettings']; $specialsettings = $result['specialsettings'];
$ssl_specialsettings = $result['ssl_specialsettings']; $ssl_specialsettings = $result['ssl_specialsettings'];
$include_specialsettings = $result['include_specialsettings']; $include_specialsettings = $result['include_specialsettings'];
@@ -1566,7 +1580,7 @@ class Domains extends ApiCommand implements ResourceEntity
} }
// Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated
if (($result['letsencrypt'] != $letsencrypt || $result['ssl_redirect'] != $ssl_redirect) && $ssl_redirect > 0 && $letsencrypt == 1) { if ($result['letsencrypt'] != $letsencrypt && $ssl_redirect > 0 && $letsencrypt == 1) {
$ssl_redirect = 2; $ssl_redirect = 2;
} }

View File

@@ -260,10 +260,12 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
$_mailerror = false; $_mailerror = false;
$mailerr_msg = ""; $mailerr_msg = "";
try { try {
$this->mailer()->setFrom($admin['email'], User::getCorrectUserSalutation($admin)); $this->mailer()->setFrom(Settings::Get('panel.adminmail'), User::getCorrectUserSalutation($admin));
$this->mailer()->clearReplyTos();
$this->mailer()->addReplyTo($admin['email'], User::getCorrectUserSalutation($admin));
$this->mailer()->Subject = $mail_subject; $this->mailer()->Subject = $mail_subject;
$this->mailer()->AltBody = $mail_body; $this->mailer()->AltBody = $mail_body;
$this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body)); $this->mailer()->Body = str_replace("\n", "<br />", $mail_body);
$this->mailer()->addAddress($email_full); $this->mailer()->addAddress($email_full);
$this->mailer()->send(); $this->mailer()->send();
} catch (\PHPMailer\PHPMailer\Exception $e) { } catch (\PHPMailer\PHPMailer\Exception $e) {
@@ -290,7 +292,9 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
$_mailerror = false; $_mailerror = false;
try { try {
$this->mailer()->setFrom($admin['email'], User::getCorrectUserSalutation($admin)); $this->mailer()->setFrom(Settings::Get('panel.adminmail'), User::getCorrectUserSalutation($admin));
$this->mailer()->clearReplyTos();
$this->mailer()->addReplyTo($admin['email'], User::getCorrectUserSalutation($admin));
$this->mailer()->Subject = $mail_subject; $this->mailer()->Subject = $mail_subject;
$this->mailer()->AltBody = $mail_body; $this->mailer()->AltBody = $mail_body;
$this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body)); $this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body));

View File

@@ -69,7 +69,7 @@ class EmailDomains extends ApiCommand implements ResourceEntity
$result = []; $result = [];
$query_fields = []; $query_fields = [];
$result_stmt = Database::prepare(" $result_stmt = Database::prepare("
SELECT DISTINCT d.domain, e.domainid, SELECT DISTINCT d.domain, d.domain_ace, e.domainid,
COUNT(e.email) as addresses, COUNT(e.email) as addresses,
IFNULL(SUM(CASE WHEN e.popaccountid > 0 THEN 1 ELSE 0 END), 0) as accounts, IFNULL(SUM(CASE WHEN e.popaccountid > 0 THEN 1 ELSE 0 END), 0) as accounts,
IFNULL(SUM( IFNULL(SUM(

View File

@@ -53,12 +53,14 @@ class Emails extends ApiCommand implements ResourceEntity
* domain-name for the email-address * domain-name for the email-address
* @param float $spam_tag_level * @param float $spam_tag_level
* optional, score which is required to tag emails as spam, default: 7.0 * optional, score which is required to tag emails as spam, default: 7.0
* @param bool $rewrite_subject
* optional, whether to add ***SPAM*** to the email's subject if applicable, default: [antispam.default_spam_rewrite_subject]
* @param float $spam_kill_level * @param float $spam_kill_level
* optional, score which is required to discard emails, default: 14.0 * optional, score which is required to discard emails, default: 14.0
* @param boolean $bypass_spam * @param boolean $bypass_spam
* optional, disable spam-filter entirely, default: no * optional, disable spam-filter entirely, default: [antispam.default_bypass_spam]
* @param boolean $policy_greylist * @param boolean $policy_greylist
* optional, enable grey-listing, default: yes * optional, enable grey-listing, default: [antispam.default_policy_greylist]
* @param boolean $iscatchall * @param boolean $iscatchall
* optional, make this address a catchall address, default: no * optional, make this address a catchall address, default: no
* @param int $customerid * @param int $customerid
@@ -85,17 +87,32 @@ class Emails extends ApiCommand implements ResourceEntity
// parameters // parameters
$spam_tag_level = $this->getParam('spam_tag_level', true, '7.0'); $spam_tag_level = $this->getParam('spam_tag_level', true, '7.0');
$spam_kill_level = $this->getParam('spam_kill_level', true, '14.0'); $spam_kill_level = $this->getUlParam('spam_kill_level', 'spam_kill_level_ul', true, '14.0');
$bypass_spam = $this->getBoolParam('bypass_spam', true, 0);
$policy_greylist = $this->getBoolParam('policy_greylist', true, 1);
$iscatchall = $this->getBoolParam('iscatchall', true, 0); $iscatchall = $this->getBoolParam('iscatchall', true, 0);
$description = $this->getParam('description', true, ''); $description = $this->getParam('description', true, '');
if ((int)Settings::Get('antispam.default_spam_rewrite_subject') <= 2) {
$rewrite_subject = $this->getBoolParam('rewrite_subject', true, (int)Settings::Get('antispam.default_spam_rewrite_subject') == 1 ? 1 : 0);
} else {
$rewrite_subject = (int)Settings::Get('antispam.default_spam_rewrite_subject') == 3 ? 1 : 0;
}
if ((int)Settings::Get('antispam.default_bypass_spam') <= 2) {
$bypass_spam = $this->getBoolParam('bypass_spam', true, (int)Settings::Get('antispam.default_bypass_spam') == 1 ? 1 : 0);
} else {
$bypass_spam = (int)Settings::Get('antispam.default_bypass_spam') == 3 ? 1 : 0;
}
if ((int)Settings::Get('antispam.default_policy_greylist') <= 2) {
$policy_greylist = $this->getBoolParam('policy_greylist', true, (int)Settings::Get('antispam.default_policy_greylist') == 1 ? 1 : 0);
} else {
$policy_greylist = (int)Settings::Get('antispam.default_policy_greylist') == 3 ? 1 : 0;
}
// validation // validation
$idna_convert = new IdnaWrapper();
if (substr($domain, 0, 4) != 'xn--') { if (substr($domain, 0, 4) != 'xn--') {
$idna_convert = new IdnaWrapper();
$domain = $idna_convert->encode(Validate::validate($domain, 'domain', '', '', [], true)); $domain = $idna_convert->encode(Validate::validate($domain, 'domain', '', '', [], true));
} }
$email_part = $idna_convert->encode($email_part);
// check domain and whether it's an email-enabled domain // check domain and whether it's an email-enabled domain
// use internal call because the customer might have 'domains' in customer_hide_options // use internal call because the customer might have 'domains' in customer_hide_options
@@ -103,10 +120,10 @@ class Emails extends ApiCommand implements ResourceEntity
'domainname' => $domain 'domainname' => $domain
], true); ], true);
if ((int)$domain_check['isemaildomain'] == 0) { if ((int)$domain_check['isemaildomain'] == 0) {
Response::standardError('maindomainnonexist', $domain, true); Response::standardError('maindomainnonexist', $idna_convert->decode($domain), true);
} }
if ((int)$domain_check['deactivated'] == 1) { if ((int)$domain_check['deactivated'] == 1) {
Response::standardError('maindomaindeactivated', $domain, true); Response::standardError('maindomaindeactivated', $idna_convert->decode($domain), true);
} }
if (Settings::Get('catchall.catchall_enabled') != '1') { if (Settings::Get('catchall.catchall_enabled') != '1') {
@@ -127,7 +144,7 @@ class Emails extends ApiCommand implements ResourceEntity
// validate it // validate it
if (!Validate::validateEmail($email_full)) { if (!Validate::validateEmail($email_full)) {
Response::standardError('emailiswrong', $email_full, true); Response::standardError('emailiswrong', $idna_convert->decode($email_full), true);
} }
// get needed customer info to reduce the email-address-counter by one // get needed customer info to reduce the email-address-counter by one
@@ -148,14 +165,16 @@ class Emails extends ApiCommand implements ResourceEntity
if ($email_check) { if ($email_check) {
if (strtolower($email_check['email_full']) == strtolower($email_full)) { if (strtolower($email_check['email_full']) == strtolower($email_full)) {
Response::standardError('emailexistalready', $email_full, true); Response::standardError('emailexistalready', $idna_convert->decode($email_full), true);
} elseif ($email_check['email'] == $email) { } elseif ($email_check['email'] == $email) {
Response::standardError('youhavealreadyacatchallforthisdomain', '', true); Response::standardError('youhavealreadyacatchallforthisdomain', '', true);
} }
} }
$spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true); $spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1})?$/', '', [7.0], true);
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true); if ($spam_kill_level > -1) {
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1})?$/', '', [14.0], true);
}
$description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true);
$stmt = Database::prepare(" $stmt = Database::prepare("
@@ -164,6 +183,7 @@ class Emails extends ApiCommand implements ResourceEntity
`email` = :email, `email` = :email,
`email_full` = :email_full, `email_full` = :email_full,
`spam_tag_level` = :spam_tag_level, `spam_tag_level` = :spam_tag_level,
`rewrite_subject` = :rewrite_subject,
`spam_kill_level` = :spam_kill_level, `spam_kill_level` = :spam_kill_level,
`bypass_spam` = :bypass_spam, `bypass_spam` = :bypass_spam,
`policy_greylist` = :policy_greylist, `policy_greylist` = :policy_greylist,
@@ -176,6 +196,7 @@ class Emails extends ApiCommand implements ResourceEntity
"email" => $email, "email" => $email,
"email_full" => $email_full, "email_full" => $email_full,
"spam_tag_level" => $spam_tag_level, "spam_tag_level" => $spam_tag_level,
"rewrite_subject" => $rewrite_subject,
"spam_kill_level" => $spam_kill_level, "spam_kill_level" => $spam_kill_level,
"bypass_spam" => $bypass_spam, "bypass_spam" => $bypass_spam,
"policy_greylist" => $policy_greylist, "policy_greylist" => $policy_greylist,
@@ -226,7 +247,7 @@ class Emails extends ApiCommand implements ResourceEntity
LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON v.`popaccountid` = u.`id` LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON v.`popaccountid` = u.`id`
WHERE v.`customerid` IN (" . implode(", ", $customer_ids) . ") WHERE v.`customerid` IN (" . implode(", ", $customer_ids) . ")
AND " . (is_numeric($params['idea']) ? "v.`id`= :idea" : "(v.`email` = :idea OR v.`email_full` = :idea)" AND " . (is_numeric($params['idea']) ? "v.`id`= :idea" : "(v.`email` = :idea OR v.`email_full` = :idea)"
)); ));
$result = Database::pexecute_first($result_stmt, $params, true, true); $result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) { if ($result) {
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get email address '" . $result['email_full'] . "'"); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get email address '" . $result['email_full'] . "'");
@@ -249,12 +270,14 @@ class Emails extends ApiCommand implements ResourceEntity
* optional, required when called as admin (if $customerid is not specified) * optional, required when called as admin (if $customerid is not specified)
* @param float $spam_tag_level * @param float $spam_tag_level
* optional, score which is required to tag emails as spam, default: 7.0 * optional, score which is required to tag emails as spam, default: 7.0
* @param bool $rewrite_subject
* optional, whether to add ***SPAM*** to the email's subject if applicable, default: [antispam.default_spam_rewrite_subject]
* @param float $spam_kill_level * @param float $spam_kill_level
* optional, score which is required to discard emails, default: 14.0 * optional, score which is required to discard emails, default: 14.0
* @param boolean $bypass_spam * @param boolean $bypass_spam
* optional, disable spam-filter entirely, default: no * optional, disable spam-filter entirely, default: [antispam.default_bypass_spam]
* @param boolean $policy_greylist * @param boolean $policy_greylist
* optional, enable grey-listing, default: yes * optional, enable grey-listing, default: [antispam.default_policy_greylist]
* @param boolean $iscatchall * @param boolean $iscatchall
* optional * optional
* @param string $description * @param string $description
@@ -282,12 +305,26 @@ class Emails extends ApiCommand implements ResourceEntity
// parameters // parameters
$spam_tag_level = $this->getParam('spam_tag_level', true, $result['spam_tag_level']); $spam_tag_level = $this->getParam('spam_tag_level', true, $result['spam_tag_level']);
$spam_kill_level = $this->getParam('spam_kill_level', true, $result['spam_kill_level']); $spam_kill_level = $this->getUlParam('spam_kill_level', 'spam_kill_level_ul', true, $result['spam_kill_level']);
$bypass_spam = $this->getBoolParam('bypass_spam', true, $result['bypass_spam']);
$policy_greylist = $this->getBoolParam('policy_greylist', true, $result['policy_greylist']);
$iscatchall = $this->getBoolParam('iscatchall', true, $result['iscatchall']); $iscatchall = $this->getBoolParam('iscatchall', true, $result['iscatchall']);
$description = $this->getParam('description', true, $result['description']); $description = $this->getParam('description', true, $result['description']);
if ((int)Settings::Get('antispam.default_spam_rewrite_subject') <= 2) {
$rewrite_subject = $this->getBoolParam('rewrite_subject', true, $result['rewrite_subject']);
} else {
$rewrite_subject = (int)Settings::Get('antispam.default_spam_rewrite_subject') == 3 ? 1 : 0;
}
if ((int)Settings::Get('antispam.default_bypass_spam') <= 2) {
$bypass_spam = $this->getBoolParam('bypass_spam', true, $result['bypass_spam']);
} else {
$bypass_spam = (int)Settings::Get('antispam.default_bypass_spam') == 3 ? 1 : 0;
}
if ((int)Settings::Get('antispam.default_policy_greylist') <= 2) {
$policy_greylist = $this->getBoolParam('policy_greylist', true, $result['policy_greylist']);
} else {
$policy_greylist = (int)Settings::Get('antispam.default_policy_greylist') == 3 ? 1 : 0;
}
// if enabling catchall is not allowed by settings, we do not need // if enabling catchall is not allowed by settings, we do not need
// to run update() // to run update()
if ($iscatchall && $result['iscatchall'] == 0 && Settings::Get('catchall.catchall_enabled') != '1') { if ($iscatchall && $result['iscatchall'] == 0 && Settings::Get('catchall.catchall_enabled') != '1') {
@@ -326,13 +363,16 @@ class Emails extends ApiCommand implements ResourceEntity
} }
$spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true); $spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true);
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true); if ($spam_kill_level > -1) {
$spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true);
}
$description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true);
$stmt = Database::prepare(" $stmt = Database::prepare("
UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET
`email` = :email , `email` = :email ,
`spam_tag_level` = :spam_tag_level, `spam_tag_level` = :spam_tag_level,
`rewrite_subject` = :rewrite_subject,
`spam_kill_level` = :spam_kill_level, `spam_kill_level` = :spam_kill_level,
`bypass_spam` = :bypass_spam, `bypass_spam` = :bypass_spam,
`policy_greylist` = :policy_greylist, `policy_greylist` = :policy_greylist,
@@ -343,6 +383,7 @@ class Emails extends ApiCommand implements ResourceEntity
$params = [ $params = [
"email" => $email, "email" => $email,
"spam_tag_level" => $spam_tag_level, "spam_tag_level" => $spam_tag_level,
"rewrite_subject" => $rewrite_subject,
"spam_kill_level" => $spam_kill_level, "spam_kill_level" => $spam_kill_level,
"bypass_spam" => $bypass_spam, "bypass_spam" => $bypass_spam,
"policy_greylist" => $policy_greylist, "policy_greylist" => $policy_greylist,
@@ -396,7 +437,10 @@ class Emails extends ApiCommand implements ResourceEntity
LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`) LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`)
WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit());
Database::pexecute($result_stmt, $query_fields, true, true); Database::pexecute($result_stmt, $query_fields, true, true);
$idna_convert = new IdnaWrapper();
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$row['email'] = $idna_convert->decode($row['email']);
$row['email_full'] = $idna_convert->decode($row['email_full']);
$result[] = $row; $result[] = $row;
} }
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-addresses"); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-addresses");

View File

@@ -288,7 +288,7 @@ class Ftps extends ApiCommand implements ResourceEntity
try { try {
$this->mailer()->Subject = $mail_subject; $this->mailer()->Subject = $mail_subject;
$this->mailer()->AltBody = $mail_body; $this->mailer()->AltBody = $mail_body;
$this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body)); $this->mailer()->Body = str_replace("\n", "<br />", $mail_body);
$this->mailer()->addAddress($customer['email'], User::getCorrectUserSalutation($customer)); $this->mailer()->addAddress($customer['email'], User::getCorrectUserSalutation($customer));
$this->mailer()->send(); $this->mailer()->send();
} catch (\PHPMailer\PHPMailer\Exception $e) { } catch (\PHPMailer\PHPMailer\Exception $e) {

View File

@@ -113,9 +113,9 @@ class Mysqls extends ApiCommand implements ResourceEntity
if (strlen($newdb_params['loginname'] . '_' . $databasename) > Database::getSqlUsernameLength()) { if (strlen($newdb_params['loginname'] . '_' . $databasename) > Database::getSqlUsernameLength()) {
throw new Exception("Database name cannot be longer than " . (Database::getSqlUsernameLength() - strlen($newdb_params['loginname'] . '_')) . " characters.", 406); throw new Exception("Database name cannot be longer than " . (Database::getSqlUsernameLength() - strlen($newdb_params['loginname'] . '_')) . " characters.", 406);
} }
$username = $dbm->createDatabase($newdb_params['loginname'] . '_' . $databasename, $password, $dbserver); $username = $dbm->createDatabase($newdb_params['loginname'] . '_' . $databasename, $password, $dbserver, 0, $newdb_params['loginname']);
} else { } else {
$username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, $newdb_params['mysql_lastaccountnumber']); $username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, $newdb_params['mysql_lastaccountnumber'], $newdb_params['loginname']);
} }
// we've checked against the password in dbm->createDatabase // we've checked against the password in dbm->createDatabase
@@ -184,7 +184,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
try { try {
$this->mailer()->Subject = $mail_subject; $this->mailer()->Subject = $mail_subject;
$this->mailer()->AltBody = $mail_body; $this->mailer()->AltBody = $mail_body;
$this->mailer()->msgHTML(str_replace("\n", "<br />", $mail_body)); $this->mailer()->Body = str_replace("\n", "<br />", $mail_body);
$this->mailer()->addAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo)); $this->mailer()->addAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo));
$this->mailer()->send(); $this->mailer()->send();
} catch (\PHPMailer\PHPMailer\Exception $e) { } catch (\PHPMailer\PHPMailer\Exception $e) {
@@ -541,7 +541,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
// Begin root-session // Begin root-session
Database::needRoot(true, $result['dbserver'], false); Database::needRoot(true, $result['dbserver'], false);
$dbm = new DbManager($this->logger()); $dbm = new DbManager($this->logger());
$dbm->getManager()->deleteDatabase($result['databasename']); $dbm->getManager()->deleteDatabase($result['databasename'], $customer['loginname']);
Database::needRoot(false); Database::needRoot(false);
// End root-session // End root-session

View File

@@ -503,8 +503,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get subdomain '" . $result['domain'] . "'"); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get subdomain '" . $result['domain'] . "'");
return $this->response($result); return $this->response($result);
} }
$key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'"); throw new Exception("Requested subdomain could not be found", 404);
throw new Exception("Subdomain with " . $key . " could not be found", 404);
} }
private function getHasCertValueForDomain(int $domainid, int $parentdomainid): int private function getHasCertValueForDomain(int $domainid, int $parentdomainid): int

View File

@@ -217,6 +217,10 @@ final class ConfigServices extends CliCommand
$_daemons_config['distro'] = $io->choice('Choose distribution', $valid_dists, $os_default); $_daemons_config['distro'] = $io->choice('Choose distribution', $valid_dists, $os_default);
// go through all services and let user check whether to include it or not // go through all services and let user check whether to include it or not
if (empty($_daemons_config['distro']) || !file_exists($config_dir . '/' . $_daemons_config['distro']. ".xml")) {
$output->writeln('<error>Empty or non-existing distribution given.</>');
return self::INVALID;
}
$configfiles = new ConfigParser($config_dir . '/' . $_daemons_config['distro'] . ".xml"); $configfiles = new ConfigParser($config_dir . '/' . $_daemons_config['distro'] . ".xml");
$services = $configfiles->getServices(); $services = $configfiles->getServices();
@@ -352,8 +356,13 @@ final class ConfigServices extends CliCommand
} }
if (!empty($decoded_config)) { if (!empty($decoded_config)) {
$config_dir = Froxlor::getInstallDir() . 'lib/configfiles/'; $config_dir = Froxlor::getInstallDir() . 'lib/configfiles/';
$configfiles = new ConfigParser($config_dir . '/' . $decoded_config['distro'] . ".xml"); if (empty($decoded_config['distro']) || !file_exists($config_dir . '/' . $decoded_config['distro']. ".xml")) {
$output->writeln('<error>Empty or non-existing distribution given. Please login with an admin, go to "System -> Configuration" and select your correct distribution in the top-right corner or specify valid distribution name for "distro" parameter.</>');
return self::INVALID;
}
$configfiles = new ConfigParser($config_dir . '/' . $decoded_config['distro']. ".xml");
$services = $configfiles->getServices(); $services = $configfiles->getServices();
$replace_arr = $this->getReplacerArray(); $replace_arr = $this->getReplacerArray();

View File

@@ -80,6 +80,7 @@ final class MasterCron extends CliCommand
Cronjob::inserttask(TaskId::REBUILD_RSPAMD); Cronjob::inserttask(TaskId::REBUILD_RSPAMD);
Cronjob::inserttask(TaskId::CREATE_QUOTA); Cronjob::inserttask(TaskId::CREATE_QUOTA);
Cronjob::inserttask(TaskId::REBUILD_CRON); Cronjob::inserttask(TaskId::REBUILD_CRON);
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
$jobs[] = 'tasks'; $jobs[] = 'tasks';
} }
define('CRON_IS_FORCED', 1); define('CRON_IS_FORCED', 1);
@@ -214,9 +215,14 @@ final class MasterCron extends CliCommand
if (file_exists($this->lockFile)) { if (file_exists($this->lockFile)) {
$jobinfo = json_decode(file_get_contents($this->lockFile), true); $jobinfo = json_decode(file_get_contents($this->lockFile), true);
$check_pid_return = null; if ($jobinfo === false || !is_array($jobinfo)) {
// get status of process // looks like an invalid lockfile
system("kill -CHLD " . (int)$jobinfo['pid'] . " 1> /dev/null 2> /dev/null", $check_pid_return); $check_pid_return = 1;
} else {
$check_pid_return = null;
// get status of process
system("kill -CHLD " . (int)$jobinfo['pid'] . " 1> /dev/null 2> /dev/null", $check_pid_return);
}
if ($check_pid_return == 1) { if ($check_pid_return == 1) {
// Process does not seem to run, most likely it has died // Process does not seem to run, most likely it has died
$this->unlockJob(); $this->unlockJob();

View File

@@ -28,13 +28,17 @@ namespace Froxlor\Cli;
use Exception; use Exception;
use Froxlor\Froxlor; use Froxlor\Froxlor;
use Froxlor\Install\AutoUpdate; use Froxlor\Install\AutoUpdate;
use Froxlor\Install\Preconfig;
use Froxlor\Install\Update; use Froxlor\Install\Update;
use Froxlor\Settings; use Froxlor\Settings;
use Froxlor\System\Mailer; use Froxlor\System\Mailer;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
final class UpdateCommand extends CliCommand final class UpdateCommand extends CliCommand
{ {
@@ -44,6 +48,8 @@ final class UpdateCommand extends CliCommand
$this->setName('froxlor:update'); $this->setName('froxlor:update');
$this->setDescription('Check for newer version and update froxlor'); $this->setDescription('Check for newer version and update froxlor');
$this->addOption('check-only', 'c', InputOption::VALUE_NONE, 'Only check for newer version and exit') $this->addOption('check-only', 'c', InputOption::VALUE_NONE, 'Only check for newer version and exit')
->addOption('show-update-options', 'o', InputOption::VALUE_NONE, 'Show possible update option parameter for the update if any. Only usable in combination with "check-only".')
->addOption('update-options', 'O', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Parameter list of update options.')
->addOption('database', 'd', InputOption::VALUE_NONE, 'Only run database updates in case updates are done via apt or manually.') ->addOption('database', 'd', InputOption::VALUE_NONE, 'Only run database updates in case updates are done via apt or manually.')
->addOption('mail-notify', 'm', InputOption::VALUE_NONE, 'Additionally inform administrator via email if a newer version was found') ->addOption('mail-notify', 'm', InputOption::VALUE_NONE, 'Additionally inform administrator via email if a newer version was found')
->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for download, extract and database-update, just do it (if not --check-only is set)') ->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for download, extract and database-update, just do it (if not --check-only is set)')
@@ -63,9 +69,13 @@ final class UpdateCommand extends CliCommand
$output->writeln('<info>' . lng('update.dbupdate_required') . '</>'); $output->writeln('<info>' . lng('update.dbupdate_required') . '</>');
if ($input->getOption('check-only')) { if ($input->getOption('check-only')) {
$output->writeln('<comment>Doing nothing because of "check-only" flag.</>'); $output->writeln('<comment>Doing nothing because of "check-only" flag.</>');
$this->askUpdateOptions($input, $output, null, false);
} else { } else {
$yestoall = $input->getOption('yes-to-all') !== false; $yestoall = $input->getOption('yes-to-all') !== false;
$helper = $this->getHelper('question'); $helper = $this->getHelper('question');
$this->askUpdateOptions($input, $output, $helper, $yestoall);
$question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) { if ($yestoall || $helper->ask($input, $output, $question)) {
$result = $this->runUpdate($output, true); $result = $this->runUpdate($output, true);
@@ -101,7 +111,7 @@ final class UpdateCommand extends CliCommand
} }
// there is a new version // there is a new version
if ($input->getOption('check-only')) { if ($input->getOption('check-only')) {
$text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel').' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]);
} else { } else {
$text = lng('admin.newerversionavailable') . ' ' . lng('admin.newerversiondetails', [AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $text = lng('admin.newerversionavailable') . ' ' . lng('admin.newerversiondetails', [AutoUpdate::getFromResult('version'), Froxlor::VERSION]);
} }
@@ -152,6 +162,7 @@ final class UpdateCommand extends CliCommand
// check whether we only wanted to check // check whether we only wanted to check
if ($input->getOption('check-only')) { if ($input->getOption('check-only')) {
//$output->writeln('<comment>Not proceeding as "check-only" is specified</>'); //$output->writeln('<comment>Not proceeding as "check-only" is specified</>');
$this->askUpdateOptions($input, $output, null, false);
return $result; return $result;
} else { } else {
$yestoall = $input->getOption('yes-to-all') !== false; $yestoall = $input->getOption('yes-to-all') !== false;
@@ -174,6 +185,9 @@ final class UpdateCommand extends CliCommand
if ($auex == 0) { if ($auex == 0) {
$output->writeln("<info>Froxlor files updated successfully.</>"); $output->writeln("<info>Froxlor files updated successfully.</>");
$result = self::SUCCESS; $result = self::SUCCESS;
$this->askUpdateOptions($input, $output, $helper, $yestoall);
$question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) { if ($yestoall || $helper->ask($input, $output, $question)) {
$result = $this->runUpdate($output, true); $result = $this->runUpdate($output, true);
@@ -195,12 +209,141 @@ final class UpdateCommand extends CliCommand
return $result; return $result;
} }
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param $helper
* @param bool $yestoall
* @return void
*/
private function askUpdateOptions(InputInterface $input, OutputInterface $output, $helper, bool $yestoall = false)
{
// check for preconfigs
$preconfig = Preconfig::getPreConfig(true);
$show_options_only = $input->getOption('show-update-options') !== false;
if (!is_null($helper) && $show_options_only) {
$output->writeln('<comment>Unsetting "show-update-options" due to not being called with "check-only".</>');
$show_options_only = false;
}
$update_options = [];
// set parameters
$uOptions = $input->getOption('update-options');
if (!empty($uOptions)) {
$options_value = [];
foreach ($uOptions as $givenOption) {
$optVal = explode("=", $givenOption);
if (count($optVal) == 2) {
$options_value[$optVal[0]] = $optVal[1];
}
}
}
if (!empty($preconfig)) {
krsort($preconfig);
foreach ($preconfig as $section) {
if (!$show_options_only) {
$output->writeln("<info>Updater questions for " . $section['title'] . "</>");
}
foreach ($section['fields'] as $update_field => $metainfo) {
if (isset($options_value[$update_field])) {
$output->writeln('Setting given parameter "' . $update_field . '" to "' . $options_value[$update_field] . '"');
$_POST[$update_field] = $options_value[$update_field];
continue;
}
$default = null;
$question_text = html_entity_decode(strip_tags($metainfo['label']), ENT_QUOTES | ENT_IGNORE, "UTF-8");
if ($metainfo['type'] == 'checkbox') {
$default = (int)$metainfo['checked'];
if ($show_options_only) {
$update_options[] = [
'name' => $update_field,
'question' => $question_text,
'default' => $default,
'choices' => '0: No' . PHP_EOL . '1: Yes' . PHP_EOL
];
} else {
$question = new ConfirmationQuestion($question_text . ' [' . ($metainfo['checked'] ? 'yes' : 'no') . '] ', (bool)$metainfo['checked'], '/^(y|j)/i');
}
} elseif ($metainfo['type'] == 'select') {
$default = $metainfo['selected'];
$choices = "";
foreach (array_values($metainfo['select_var'] ?? []) as $index => $choice) {
$choices .= $index . ': ' . $choice . PHP_EOL;
}
if ($show_options_only) {
$update_options[] = [
'name' => $update_field,
'question' => $question_text,
'default' => !empty($default) ? $default : '-',
'choices' => $choices
];
} else {
$question = new ChoiceQuestion(
$question_text,
array_values($metainfo['select_var'] ?? []),
$metainfo['selected']
);
$question->setValidator(function ($answer) use ($metainfo): string {
$key = array_keys($metainfo['select_var'])[(int)$answer] ?? false; // Find the key based on the selected value
if ($key === false) {
throw new \RuntimeException('Invalid selection.');
}
return $key;
});
}
} elseif ($metainfo['type'] == 'text') {
$default = $metainfo['value'] ?? '';
if ($show_options_only) {
$update_options[] = [
'name' => $update_field,
'question' => $question_text,
'default' => $default,
'choices' => PHP_EOL
];
} else {
$question = new Question($question_text . (!empty($metainfo['value']) ? ' [' . $metainfo['value'] . ']' : ''), $default);
$question->setValidator(function (string $answer) use ($metainfo): string {
if (($metainfo['mandatory'] ?? false) && empty($answer)) {
throw new \RuntimeException(
'Answer cannot be empty'
);
}
if (!empty($metainfo['pattern'] ?? "") && !preg_match("/" . $metainfo['pattern'] . "/", $answer)) {
throw new \RuntimeException('Answer does not seem to be in valid format');
}
return $answer;
});
}
} else {
$output->writeln("<error>Unknown type " . $metainfo['type'] . "</error>");
continue;
}
if (!$show_options_only) {
if ($yestoall) {
$_POST[$update_field] = $default;
} else {
$_POST[$update_field] = $helper->ask($input, $output, $question);
}
}
}
}
if ($show_options_only) {
$io = new SymfonyStyle($input, $output);
$io->table(
['Parameter', 'Description', 'Default', 'Choices'],
$update_options
);
}
}
}
private function mailNotify(InputInterface $input, OutputInterface $output) private function mailNotify(InputInterface $input, OutputInterface $output)
{ {
if ($input->getOption('mail-notify')) { if ($input->getOption('mail-notify')) {
$last_check_version = Settings::Get('system.update_notify_last'); $last_check_version = Settings::Get('system.update_notify_last');
if (Update::versionInUpdate($last_check_version, AutoUpdate::getFromResult('version'))) { if (Update::versionInUpdate($last_check_version, AutoUpdate::getFromResult('version'))) {
$text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel').' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]);
$mail = new Mailer(true); $mail = new Mailer(true);
$mail->Body = $text; $mail->Body = $text;
$mail->Subject = "[froxlor] " . lng('update.notify_subject'); $mail->Subject = "[froxlor] " . lng('update.notify_subject');

View File

@@ -69,7 +69,8 @@ class AcmeSh extends FroxlorCron
* run the task * run the task
* *
* @param bool $internal * @param bool $internal
* @return number * @return int
* @throws \Exception
*/ */
public static function run(bool $internal = false) public static function run(bool $internal = false)
{ {
@@ -85,6 +86,9 @@ class AcmeSh extends FroxlorCron
if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) { if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) {
// insert task to generate certificates and vhost-configs // insert task to generate certificates and vhost-configs
Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::REBUILD_VHOST);
if ($renew_froxlor) {
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
}
} }
return 0; return 0;
} }
@@ -217,6 +221,7 @@ class AcmeSh extends FroxlorCron
* check whether we need to issue a new certificate for froxlor itself * check whether we need to issue a new certificate for froxlor itself
* *
* @return boolean * @return boolean
* @throws \Exception
*/ */
private static function issueFroxlorVhost() private static function issueFroxlorVhost()
{ {
@@ -228,9 +233,7 @@ class AcmeSh extends FroxlorCron
"); ");
$froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
// also check for possible existing certificate // also check for possible existing certificate
if (($froxlor_ssl && empty($froxlor_ssl['validtodate'])) if (!$froxlor_ssl || empty($froxlor_ssl['validtodate'])) {
|| (!$froxlor_ssl && !self::checkFsFilesAreNewer(Settings::Get('system.hostname'), date('Y-m-d H:i:s')))
) {
return true; return true;
} }
} }
@@ -321,10 +324,12 @@ EOC;
WHERE WHERE
dom.`customerid` = cust.`customerid` dom.`customerid` = cust.`customerid`
AND cust.deactivated = 0 AND cust.deactivated = 0
AND dom.deactivated = 0
AND dom.`ssl_enabled` = 1 AND dom.`ssl_enabled` = 1
AND dom.`letsencrypt` = 1 AND dom.`letsencrypt` = 1
AND dom.`aliasdomain` IS NULL AND dom.`aliasdomain` IS NULL
AND dom.`iswildcarddomain` = 0 AND dom.`iswildcarddomain` = 0
AND dom.`email_only` = 0
AND domssl.`validtodate` IS NULL AND domssl.`validtodate` IS NULL
"); ");
$customer_ssl = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC); $customer_ssl = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -338,6 +343,7 @@ EOC;
* check whether we need to renew-check the certificate for froxlor itself * check whether we need to renew-check the certificate for froxlor itself
* *
* @return boolean * @return boolean
* @throws \Exception
*/ */
private static function renewFroxlorVhost() private static function renewFroxlorVhost()
{ {
@@ -383,10 +389,13 @@ EOC;
WHERE WHERE
dom.`customerid` = cust.`customerid` dom.`customerid` = cust.`customerid`
AND cust.deactivated = 0 AND cust.deactivated = 0
AND dom.deactivated = 0
AND dom.`ssl_enabled` = 1 AND dom.`ssl_enabled` = 1
AND dom.`letsencrypt` = 1 AND dom.`letsencrypt` = 1
AND dom.`aliasdomain` IS NULL AND dom.`aliasdomain` IS NULL
AND dom.`iswildcarddomain` = 0 AND dom.`iswildcarddomain` = 0
AND dom.`email_only` = 0
AND dom.`ssl_redirect` != 2
"); ");
$renew_certs = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC); $renew_certs = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC);
if ($renew_certs) { if ($renew_certs) {
@@ -535,6 +544,7 @@ EOC;
* @param array $domains * @param array $domains
* @param int $domain_id * @param int $domain_id
* @param FroxlorLogger $cronlog * @param FroxlorLogger $cronlog
* @throws \Exception
*/ */
private static function validateDns(array &$domains, $domain_id, &$cronlog) private static function validateDns(array &$domains, $domain_id, &$cronlog)
{ {
@@ -615,27 +625,47 @@ EOC;
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Successful exit-code returned - storing certificate"); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Successful exit-code returned - storing certificate");
$cert_stored = self::certToDb($certrow, $cronlog, $acme_result); $cert_stored = self::certToDb($certrow, $cronlog, $acme_result);
if ($cert_stored if ($cert_stored && $renew_hook) {
&& $renew_hook self::renewHookConfigs($cronlog);
&& !empty(trim(Settings::Get('system.le_renew_services') ?? "")) }
&& !empty(trim(Settings::Get('system.le_renew_hook') ?? "")) }
) { }
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations"); }
$certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname'))); public static function renewHookConfigs($cronlog)
$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer'); {
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key'); if (!empty(trim(Settings::Get('system.le_renew_services') ?? ""))
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer'); && !empty(trim(Settings::Get('system.le_renew_hook') ?? ""))
) {
if (Settings::IsInList('system.le_renew_services', 'postfix')) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations");
// "postconf -e" for postfix
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain)); $certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname')));
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
} if (empty($certificate_folder)) {
if (Settings::IsInList('system.le_renew_services', 'dovecot')) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No certificate folder for '" . Settings::Get('system.hostname') . "' found");
// custom config for dovecot return;
$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting? }
$ssl_content = <<<EOSSL
$fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer');
$keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key');
$ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer');
if (!file_exists($fullchain) || !file_exists($keyfile) || !file_exists($ca_file)) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "At least one of the required certificate files for '" . Settings::Get('system.hostname') . "' could not be found");
return;
}
$dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting?
if (Settings::IsInList('system.le_renew_services', 'postfix')) {
// "postconf -e" for postfix
FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain));
FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile));
}
if (Settings::IsInList('system.le_renew_services', 'dovecot')) {
// custom config for dovecot
$ssl_content = <<<EOSSL
# Autogenerated configuration by froxlor. # Autogenerated configuration by froxlor.
# Do not manually edit this file as it will be overwritten. # Do not manually edit this file as it will be overwritten.
@@ -643,33 +673,35 @@ ssl = yes
ssl_cert = <{$fullchain} ssl_cert = <{$fullchain}
ssl_key = <{$keyfile} ssl_key = <{$keyfile}
EOSSL; EOSSL;
file_put_contents($dovecot_conf, $ssl_content); file_put_contents($dovecot_conf, $ssl_content);
} } elseif (file_exists($dovecot_conf)) {
if (Settings::IsInList('system.le_renew_services', 'proftpd')) { // safely remove the autogenerated config file
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting? unlink($dovecot_conf);
$rval = false;
// ECC certificate or not?
if (strpos($certificate_folder, '_ecc') === false) {
// comment out ECC related settings
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add RSA directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
} else {
// comment out RSA related settings
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add ECC directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
// reload the services
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
}
} }
if (Settings::IsInList('system.le_renew_services', 'proftpd')) {
$proftpd_conf = '/etc/proftpd/tls.conf'; // @fixme setting?
$rval = false;
// ECC certificate or not?
if (strpos($certificate_folder, '_ecc') === false) {
// comment out ECC related settings
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateFile|# TLSECCertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSECCertificateKeyFile|# TLSECCertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add RSA directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateFile.*|TLSRSACertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSRSACertificateKeyFile.*|TLSRSACertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
} else {
// comment out RSA related settings
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateFile|# TLSRSACertificateFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^TLSRSACertificateKeyFile|# TLSRSACertificateKeyFile|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
// add ECC directives
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateFile.*|TLSECCertificateFile " . $fullchain . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSECCertificateKeyFile.*|TLSECCertificateKeyFile " . $keyfile . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
FileDir::safe_exec("sed -i.bak 's|^#\?\s\?TLSCACertificateFile.*|TLSCACertificateFile " . $ca_file . "|' " . escapeshellarg($proftpd_conf), $rval, ['|', '?']);
}
// reload the services
FileDir::safe_exec(Settings::Get('system.le_renew_hook'));
} }
} }

View File

@@ -55,7 +55,7 @@ class Rspamd
// get all email addresses // get all email addresses
$antispam_stmt = Database::prepare(" $antispam_stmt = Database::prepare("
SELECT email, spam_tag_level, spam_kill_level, bypass_spam, policy_greylist, iscatchall SELECT email, spam_tag_level, rewrite_subject, spam_kill_level, bypass_spam, policy_greylist, iscatchall
FROM `" . TABLE_MAIL_VIRTUAL . "` FROM `" . TABLE_MAIL_VIRTUAL . "`
ORDER BY email ORDER BY email
"); ");
@@ -165,7 +165,7 @@ class Rspamd
$this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating antispam config for ' . $email['email']); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating antispam config for ' . $email['email']);
$email['spam_tag_level'] = floatval($email['spam_tag_level']); $email['spam_tag_level'] = floatval($email['spam_tag_level']);
$email['spam_kill_level'] = floatval($email['spam_kill_level']); $email['spam_kill_level'] = $email['spam_kill_level'] == -1 ? "null" : floatval($email['spam_kill_level']);
$email_id = md5($email['email']); $email_id = md5($email['email']);
$this->frx_settings_file .= '# Email: ' . $email['email'] . "\n"; $this->frx_settings_file .= '# Email: ' . $email['email'] . "\n";
@@ -185,7 +185,9 @@ class Rspamd
$this->frx_settings_file .= ' apply {' . "\n"; $this->frx_settings_file .= ' apply {' . "\n";
$this->frx_settings_file .= ' actions {' . "\n"; $this->frx_settings_file .= ' actions {' . "\n";
$this->frx_settings_file .= ' "add header" = ' . $email['spam_tag_level'] . ';' . "\n"; $this->frx_settings_file .= ' "add header" = ' . $email['spam_tag_level'] . ';' . "\n";
$this->frx_settings_file .= ' rewrite_subject = ' . $email['spam_tag_level'] . ';' . "\n"; if ((int)$email['rewrite_subject'] == 1) {
$this->frx_settings_file .= ' rewrite_subject = ' . ($email['spam_tag_level'] + 0.01) . ';' . "\n";
}
$this->frx_settings_file .= ' reject = ' . $email['spam_kill_level'] . ';' . "\n"; $this->frx_settings_file .= ' reject = ' . $email['spam_kill_level'] . ';' . "\n";
if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) { if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) {
$this->frx_settings_file .= ' greylist = null;' . "\n"; $this->frx_settings_file .= ' greylist = null;' . "\n";

View File

@@ -29,6 +29,7 @@ use Exception;
use Froxlor\Cron\FroxlorCron; use Froxlor\Cron\FroxlorCron;
use Froxlor\Cron\Http\ConfigIO; use Froxlor\Cron\Http\ConfigIO;
use Froxlor\Cron\Http\HttpConfigBase; use Froxlor\Cron\Http\HttpConfigBase;
use Froxlor\Cron\Http\LetsEncrypt\AcmeSh;
use Froxlor\Cron\Mail\Rspamd; use Froxlor\Cron\Mail\Rspamd;
use Froxlor\Cron\TaskId; use Froxlor\Cron\TaskId;
use Froxlor\Database\Database; use Froxlor\Database\Database;
@@ -125,6 +126,12 @@ class TasksCron extends FroxlorCron
*/ */
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']);
Domain::doLetsEncryptCleanUp($row['data']['domain']); Domain::doLetsEncryptCleanUp($row['data']['domain']);
} elseif ($row['type'] == TaskId::UPDATE_LE_SERVICES) {
/**
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
*/
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Updating Let's Encrypt configuration for selected services");
AcmeSh::renewHookConfigs(FroxlorLogger::getInstanceOf());
} }
} }
@@ -334,10 +341,11 @@ class TasksCron extends FroxlorCron
// webserver logs // webserver logs
$logsdir = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $row['data']['loginname']); $logsdir = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $row['data']['loginname']);
if (file_exists($logsdir) && $logsdir != '/' && $logsdir != FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')) && substr($logsdir, 0, strlen(Settings::Get('system.logfiles_directory'))) == Settings::Get('system.logfiles_directory')) { if (file_exists(dirname($logsdir)) && $logsdir != '/' && $logsdir != FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')) && substr($logsdir, 0, strlen(Settings::Get('system.logfiles_directory'))) == Settings::Get('system.logfiles_directory')) {
// build up wildcard for webX-{access,error}.log{*} // build up wildcard for webX-{access,error}.log{*}
$logsdir .= '-*'; $logsdir .= '-*.log';
FileDir::safe_exec('rm -f ' . escapeshellarg($logsdir)); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' .FileDir::makeCorrectFile($logsdir));
FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($logsdir));
} }
} }
} }

View File

@@ -87,6 +87,11 @@ final class TaskId
*/ */
const DELETE_DOMAIN_SSL = 12; const DELETE_DOMAIN_SSL = 12;
/**
* TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate
*/
const UPDATE_LE_SERVICES = 13;
/** /**
* TYPE=20 CUSTUMER DATA DUMP * TYPE=20 CUSTUMER DATA DUMP
*/ */

View File

@@ -133,7 +133,9 @@ class ReportsCron extends FroxlorCron
$_mailerror = false; $_mailerror = false;
$mailerr_msg = ""; $mailerr_msg = "";
try { try {
$mail->SetFrom($row['adminmail'], $row['adminname']); $mail->setFrom(Settings::Get('panel.adminmail'), $row['adminname']);
$mail->clearReplyTos();
$mail->addReplyTo($row['adminmail'], $row['adminname']);
$mail->Subject = $mail_subject; $mail->Subject = $mail_subject;
$mail->AltBody = $mail_body; $mail->AltBody = $mail_body;
$mail->MsgHTML(nl2br($mail_body)); $mail->MsgHTML(nl2br($mail_body));
@@ -405,7 +407,9 @@ class ReportsCron extends FroxlorCron
$_mailerror = false; $_mailerror = false;
$mailerr_msg = ""; $mailerr_msg = "";
try { try {
$mail->SetFrom($row['adminmail'], $row['adminname']); $mail->setFrom(Settings::Get('panel.adminmail'), $row['adminname']);
$mail->clearReplyTos();
$mail->addReplyTo($row['adminmail'], $row['adminname']);
$mail->Subject = $mail_subject; $mail->Subject = $mail_subject;
$mail->AltBody = $mail_body; $mail->AltBody = $mail_body;
$mail->MsgHTML(nl2br($mail_body)); $mail->MsgHTML(nl2br($mail_body));

View File

@@ -102,8 +102,26 @@ class DbManager
$databases[$databases_row['dbserver']][] = $databases_row['databasename']; $databases[$databases_row['dbserver']][] = $databases_row['databasename'];
} }
$customers_sel = Database::query("
SELECT DISTINCT c.loginname
FROM `" . TABLE_PANEL_CUSTOMERS . "` c
LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid
WHERE c.deactivated = '0' AND d.id IS NOT NULL
");
$customers = [];
while ($customer = $customers_sel->fetch(\PDO::FETCH_ASSOC)) {
$customers[] = $customer['loginname'];
}
$dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`"); $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`");
while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) { while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) {
// add all customer loginnames to the $databases array for this database-server to correct
// a possible existing global mysql-user for that customer
foreach ($customers as $customer) {
$databases[$dbserver['dbserver']][] = $customer;
}
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver['dbserver'], false); Database::needRoot(true, $dbserver['dbserver'], false);
@@ -136,6 +154,8 @@ class DbManager
$dbm->getManager()->flushPrivileges(); $dbm->getManager()->flushPrivileges();
Database::needRoot(false); Database::needRoot(false);
unset($databases[$dbserver['dbserver']]);
} }
} }
@@ -149,13 +169,14 @@ class DbManager
* @param ?string $password * @param ?string $password
* @param int $dbserver * @param int $dbserver
* @param int $last_accnumber * @param int $last_accnumber
* @param ?string $global_user
* *
* @return string|bool $username if successful or false of username is equal to the password * @return string|bool $username if successful or false of username is equal to the password
* @throws Exception * @throws Exception
*/ */
public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0) public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0, string $global_user = "")
{ {
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// check whether we shall create a random username // check whether we shall create a random username
if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') { if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') {
@@ -184,6 +205,9 @@ class DbManager
// and give permission to the user on every access-host we have // and give permission to the user on every access-host we have
foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
$this->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host); $this->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host);
if (!empty($global_user)) {
$this->getManager()->grantCreateToDb($global_user, $username, $mysql_access_host);
}
} }
$this->getManager()->flushPrivileges(); $this->getManager()->flushPrivileges();

View File

@@ -110,13 +110,21 @@ class DbManagerMySQL
"password" => $password "password" => $password
]); ]);
// grant privileges // grant privileges
$grants = "ALL";
if ($grant_access_prefix) {
$grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER";
}
$stmt = Database::prepare(" $stmt = Database::prepare("
GRANT ALL ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host GRANT " . $grants . " ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host
"); ");
Database::pexecute($stmt, [ Database::pexecute($stmt, [
"username" => $username, "username" => $username,
"host" => $access_host "host" => $access_host
]); ]);
if ($grant_access_prefix) {
$this->grantCreateToCustomerDbs($username, $access_host);
}
} else { } else {
// set password // set password
if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.6', '<') || version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '10.0.0', '>=')) { if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.6', '<') || version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '10.0.0', '>=')) {
@@ -145,9 +153,10 @@ class DbManagerMySQL
* takes away any privileges from a user to that db * takes away any privileges from a user to that db
* *
* @param string $dbname * @param string $dbname
* @param ?string $global_user
* @throws \Exception * @throws \Exception
*/ */
public function deleteDatabase(string $dbname) public function deleteDatabase(string $dbname, string $global_user = "")
{ {
if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) {
// failsafe if user has been deleted manually (requires MySQL 4.1.2+) // failsafe if user has been deleted manually (requires MySQL 4.1.2+)
@@ -167,11 +176,19 @@ class DbManagerMySQL
} else { } else {
$drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host"); $drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host");
} }
$rev_stmt = Database::prepare("REVOKE ALL PRIVILEGES ON `" . $dbname . "`.* FROM :guser@:host;");
while ($host = $host_res_stmt->fetch(PDO::FETCH_ASSOC)) { while ($host = $host_res_stmt->fetch(PDO::FETCH_ASSOC)) {
Database::pexecute($drop_stmt, [ Database::pexecute($drop_stmt, [
'dbname' => $dbname, 'dbname' => $dbname,
'host' => $host['Host'] 'host' => $host['Host']
], false); ], false);
if (!empty($global_user)) {
Database::pexecute($rev_stmt, [
'guser' => $global_user,
'host' => $host['Host']
], false);
}
} }
$drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`"); $drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`");
@@ -187,21 +204,23 @@ class DbManagerMySQL
*/ */
public function deleteUser(string $username, string $host) public function deleteUser(string $username, string $host)
{ {
if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { if ($this->userExistsOnHost($username, $host)) {
// Revoke privileges (only required for MySQL 4.1.2 - 5.0.1) if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) {
$stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`"); // Revoke privileges (only required for MySQL 4.1.2 - 5.0.1)
Database::pexecute($stmt); $stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`");
Database::pexecute($stmt);
}
// as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+)
if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) {
$stmt = Database::prepare("DROP USER :username@:host");
} else {
$stmt = Database::prepare("DROP USER IF EXISTS :username@:host");
}
Database::pexecute($stmt, [
"username" => $username,
"host" => $host
]);
} }
// as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+)
if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) {
$stmt = Database::prepare("DROP USER :username@:host");
} else {
$stmt = Database::prepare("DROP USER IF EXISTS :username@:host");
}
Database::pexecute($stmt, [
"username" => $username,
"host" => $host
]);
} }
/** /**
@@ -229,8 +248,15 @@ class DbManagerMySQL
{ {
// check whether user exists to avoid errors // check whether user exists to avoid errors
if ($this->userExistsOnHost($username, $host)) { if ($this->userExistsOnHost($username, $host)) {
Database::query('GRANT ALL PRIVILEGES ON `' . $username . ($grant_access_prefix ? '%' : '') . '`.* TO `' . $username . '`@`' . $host . '`'); $grants = "ALL PRIVILEGES";
Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . ($grant_access_prefix ? '%' : '') . '` . * TO `' . $username . '`@`' . $host . '`'); if ($grant_access_prefix) {
$grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER";
}
Database::query('GRANT ' . $grants . ' ON `' . $username . ($grant_access_prefix ? '%' : '') . '`.* TO `' . $username . '`@`' . $host . '`');
Database::query('GRANT ' . $grants . ' ON `' . str_replace('_', '\_', $username) . ($grant_access_prefix ? '%' : '') . '` . * TO `' . $username . '`@`' . $host . '`');
if ($grant_access_prefix) {
$this->grantCreateToCustomerDbs($username, $host);
}
} }
} }
@@ -290,4 +316,51 @@ class DbManagerMySQL
} }
return $allsqlusers; return $allsqlusers;
} }
/**
* grant "CREATE" for prefix user to all existing databases of that customer
*
* @param string $username
* @param string $access_host
* @return void
* @throws \Exception
*/
private function grantCreateToCustomerDbs(string $username, string $access_host)
{
$cus_stmt = Database::prepare("SELECT customerid FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE loginname = :username");
$cust = Database::pexecute_first($cus_stmt, ['username' => $username]);
if ($cust) {
$sel_stmt = Database::prepare("SELECT databasename FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid");
Database::pexecute($sel_stmt, ['cid' => $cust['customerid']]);
while ($dbdata = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
$stmt = Database::prepare("
GRANT ALL ON `" . $dbdata['databasename'] . "`.* TO :username@:host
");
Database::pexecute($stmt, [
"username" => $username,
"host" => $access_host
]);
}
}
}
/**
* grant "CREATE" for prefix user to all existing databases of that customer
*
* @param string $username
* @param string $database
* @param string $access_host
* @return void
* @throws \Exception
*/
public function grantCreateToDb(string $username, string $database, string $access_host)
{
$stmt = Database::prepare("
GRANT ALL ON `" . $database . "`.* TO :username@:host
");
Database::pexecute($stmt, [
"username" => $username,
"host" => $access_host
]);
}
} }

View File

@@ -31,10 +31,10 @@ final class Froxlor
{ {
// Main version variable // Main version variable
const VERSION = '2.2.1'; const VERSION = '2.2.6';
// Database version (YYYYMMDDC where C is a daily counter) // Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202408140'; const DBVERSION = '202412030';
// Distribution branding-tag (used for Debian etc.) // Distribution branding-tag (used for Debian etc.)
const BRANDING = ''; const BRANDING = '';
@@ -316,7 +316,7 @@ final class Froxlor
* @param array|null $arr * @param array|null $arr
* @return void * @return void
*/ */
private static function parseVersionArray(array &$arr = null) private static function parseVersionArray(?array &$arr)
{ {
// -dev or -beta or -rc ? // -dev or -beta or -rc ?
if (stripos($arr[count($arr) - 1], '-') !== false) { if (stripos($arr[count($arr) - 1], '-') !== false) {

View File

@@ -85,27 +85,31 @@ class Preconfig
} }
} }
} }
/** /**
* Function getPreConfig * Function getPreConfig
* *
* outputs various form-field-arrays before the update process * outputs various form-field-arrays before the update process
* can be continued (asks for agreement whatever is being asked) * can be continued (asks for agreement whatever is being asked)
* *
* @param bool $no_check
* @return array * @return array
*/ */
public static function getPreConfig(): array public static function getPreConfig(bool $no_check = false): array
{ {
$preconfig = new self(); $preconfig = new self();
if ($preconfig->hasPreConfig()) { if ($preconfig->hasPreConfig()) {
$agree = [ if (!$no_check) {
'title' => 'Check', $agree = [
'fields' => [ 'title' => 'Check',
'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'], 'fields' => [
'update_preconfig' => ['type' => 'hidden', 'value' => 1] 'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
] 'update_preconfig' => ['type' => 'hidden', 'value' => 1]
]; ]
$preconfig->addToPreConfig($agree); ];
$preconfig->addToPreConfig($agree);
}
return $preconfig->getData(); return $preconfig->getData();
} }
return []; return [];

View File

@@ -104,6 +104,10 @@ class MailLogParser
unset($matches); unset($matches);
$line = fgets($file_handle); $line = fgets($file_handle);
if (strpos($line, 'postfix') === false) {
continue;
}
$timestamp = $this->getLogTimestamp($line); $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) { if ($this->startTime < $timestamp) {
if (preg_match("/postfix\/qmgr.*(?::|\])\s([A-Z\d]+).*from=<?(?:.*\@([a-zA-Z\d\.\-]+))?>?, size=(\d+),/", $line, $matches)) { if (preg_match("/postfix\/qmgr.*(?::|\])\s([A-Z\d]+).*from=<?(?:.*\@([a-zA-Z\d\.\-]+))?>?, size=(\d+),/", $line, $matches)) {
@@ -112,7 +116,7 @@ class MailLogParser
"domainFrom" => strtolower($matches[2]), "domainFrom" => strtolower($matches[2]),
"size" => $matches[3] "size" => $matches[3]
]; ];
} elseif (preg_match("/postfix\/(?:pipe|smtp).*(?::|\])\s([A-Z\d]+).*to=<?(?:.*\@([a-zA-Z\d\.\-]+))?>?,/", $line, $matches)) { } elseif (preg_match("/postfix\/(?:pipe|smtp|lmtp).*(?::|\])\s([A-Z\d]+).*to=<?(?:.*\@([a-zA-Z\d\.\-]+))?>?,/", $line, $matches)) {
// Postfix to // Postfix to
if (array_key_exists($matches[1], $this->mails)) { if (array_key_exists($matches[1], $this->mails)) {
$this->mails[$matches[1]]["domainTo"] = strtolower($matches[2]); $this->mails[$matches[1]]["domainTo"] = strtolower($matches[2]);
@@ -149,7 +153,7 @@ class MailLogParser
private function getLogTimestamp($line) private function getLogTimestamp($line)
{ {
$matches = null; $matches = null;
if (preg_match("/((?:[A-Z]{3}\s{1,2}\d{1,2}|\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2})/i", $line, $matches)) { if (preg_match("/((?:[A-Z]{3}\s{1,2}\d{1,2}|\d{4}-\d{2}-\d{2}).\d{2}:\d{2}:\d{2})/i", $line, $matches)) {
$timestamp = strtotime($matches[1]); $timestamp = strtotime($matches[1]);
if ($timestamp > ($this->startTime + 60 * 60 * 24)) { if ($timestamp > ($this->startTime + 60 * 60 * 24)) {
return strtotime($matches[1] . " -1 year"); return strtotime($matches[1] . " -1 year");
@@ -258,6 +262,10 @@ class MailLogParser
unset($matches); unset($matches);
$line = fgets($file_handle); $line = fgets($file_handle);
if (strpos($line, 'dovecot') === false) {
continue;
}
$timestamp = $this->getLogTimestamp($line); $timestamp = $this->getLogTimestamp($line);
if ($this->startTime < $timestamp) { if ($this->startTime < $timestamp) {
if (preg_match("/dovecot.*(?::|\]) imap\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*(?:in=(\d+) out=(\d+)|bytes=(\d+)\/(\d+))/i", $line, $matches)) { if (preg_match("/dovecot.*(?::|\]) imap\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*(?:in=(\d+) out=(\d+)|bytes=(\d+)\/(\d+))/i", $line, $matches)) {

View File

@@ -125,25 +125,20 @@ class Store
} }
if (count($ids) > 0) { if (count($ids) > 0) {
$defaultips_new = explode(',', $newfieldvalue);
if (!empty($defaultips_old) && !empty($newfieldvalue)) { if (!empty($defaultips_old)) {
$in_value = $defaultips_old . ", " . $newfieldvalue; // Delete the existing mappings linking to default IPs
} elseif (!empty($defaultips_old) && empty($newfieldvalue)) { $del_stmt = Database::prepare("
$in_value = $defaultips_old; DELETE FROM `" . TABLE_DOMAINTOIP . "`
} else { WHERE `id_domain` IN (" . implode(', ', $ids) . ")
$in_value = $newfieldvalue; AND `id_ipandports` IN (" . $defaultips_old . ")
");
Database::pexecute($del_stmt);
} }
// Delete the existing mappings linking to default IPs $defaultips_new = !empty($newfieldvalue) ? explode(",", $newfieldvalue) : [];
$del_stmt = Database::prepare("
DELETE FROM `" . TABLE_DOMAINTOIP . "`
WHERE `id_domain` IN (" . implode(', ', $ids) . ")
AND `id_ipandports` IN (" . $in_value . ")
");
Database::pexecute($del_stmt);
if (count($defaultips_new) > 0) { if (count($defaultips_new) > 0) {
// Insert the new mappings // Insert the new mappings
$ins_stmt = Database::prepare(" $ins_stmt = Database::prepare("
INSERT INTO `" . TABLE_DOMAINTOIP . "` INSERT INTO `" . TABLE_DOMAINTOIP . "`
@@ -166,6 +161,9 @@ class Store
{ {
$defaultips_old = Settings::Get('system.defaultsslip'); $defaultips_old = Settings::Get('system.defaultsslip');
self::cleanIpSelection($defaultips_old);
self::cleanIpSelection($newfieldvalue);
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'defaultsslip') { if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'defaultsslip') {
@@ -175,6 +173,14 @@ class Store
return $returnvalue; return $returnvalue;
} }
private static function cleanIpSelection(&$selection)
{
$selection_arr = array_filter(explode(',', $selection), function ($value) {
return !empty($value);
});
$selection = implode(",", $selection_arr);
}
/** /**
* updates the setting for the default panel-theme * updates the setting for the default panel-theme
* and also the user themes (customers and admins) if * and also the user themes (customers and admins) if
@@ -237,6 +243,17 @@ class Store
return $returnvalue; return $returnvalue;
} }
public static function storeSettingFieldInsertUpdateServicesTask($fieldname, $fielddata, $newfieldvalue)
{
// first save the setting itself
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);
if ($returnvalue !== false) {
Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES);
}
return $returnvalue;
}
public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue) public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue)
{ {
$returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue);

View File

@@ -134,7 +134,7 @@ class Cronjob
INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = :type, `data` = :data INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = :type, `data` = :data
"); ");
if ($type == TaskId::REBUILD_VHOST || $type == TaskId::REBUILD_DNS || $type == TaskId::CREATE_FTP || $type == TaskId::REBUILD_RSPAMD || $type == TaskId::CREATE_QUOTA || $type == TaskId::REBUILD_CRON) { if ($type == TaskId::REBUILD_VHOST || $type == TaskId::REBUILD_DNS || $type == TaskId::CREATE_FTP || $type == TaskId::REBUILD_RSPAMD || $type == TaskId::CREATE_QUOTA || $type == TaskId::REBUILD_CRON || $type == TaskId::UPDATE_LE_SERVICES) {
// 4 = bind -> if bind disabled -> no task // 4 = bind -> if bind disabled -> no task
if ($type == TaskId::REBUILD_DNS && Settings::Get('system.bind_enable') == '0') { if ($type == TaskId::REBUILD_DNS && Settings::Get('system.bind_enable') == '0') {
return; return;
@@ -147,6 +147,10 @@ class Cronjob
if ($type == TaskId::CREATE_QUOTA && Settings::Get('system.diskquota_enabled') == '0') { if ($type == TaskId::CREATE_QUOTA && Settings::Get('system.diskquota_enabled') == '0') {
return; return;
} }
// 13 = let's encrypt for services -> if services empty = no task
if ($type == TaskId::UPDATE_LE_SERVICES && (Settings::Get('system.le_froxlor_enabled') == '0' || Settings::Get('system.le_renew_services') == '')) {
return;
}
// delete previously inserted tasks if they are the same as we only need ONE // delete previously inserted tasks if they are the same as we only need ONE
$del_stmt = Database::prepare(" $del_stmt = Database::prepare("

View File

@@ -68,9 +68,9 @@ class Mailer extends PHPMailer
if (self::ValidateAddress(Settings::Get('panel.adminmail')) !== false) { if (self::ValidateAddress(Settings::Get('panel.adminmail')) !== false) {
// set return-to address and custom sender-name, see #76 // set return-to address and custom sender-name, see #76
$this->SetFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); $this->setFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname'));
if (Settings::Get('panel.adminmail_return') != '') { if (Settings::Get('panel.adminmail_return') != '') {
$this->AddReplyTo(Settings::Get('panel.adminmail_return'), Settings::Get('panel.adminmail_defname')); $this->addReplyTo(Settings::Get('panel.adminmail_return'), Settings::Get('panel.adminmail_defname'));
} }
} }
} }

View File

@@ -121,7 +121,7 @@ class UI
'domain' => self::getCookieHost(), 'domain' => self::getCookieHost(),
'secure' => self::requestIsHttps(), 'secure' => self::requestIsHttps(),
'httponly' => true, 'httponly' => true,
'samesite' => 'Strict' 'samesite' => 'Lax'
]); ]);
session_start(); session_start();

View File

@@ -152,7 +152,7 @@ class User
]); ]);
$customer['emails_used_new'] = (int)$customer_emails['number_emails']; $customer['emails_used_new'] = (int)$customer_emails['number_emails'];
$customer_emails_result_stmt = Database::prepare('SELECT `email`, `email_full`, `destination`, `popaccountid` AS `number_email_forwarders` FROM `' . TABLE_MAIL_VIRTUAL . '` $customer_emails_result_stmt = Database::prepare('SELECT `email`, `email_full`, `destination`, `popaccountid` FROM `' . TABLE_MAIL_VIRTUAL . '`
WHERE `customerid` = :cid'); WHERE `customerid` = :cid');
Database::pexecute($customer_emails_result_stmt, [ Database::pexecute($customer_emails_result_stmt, [
"cid" => $customer['customerid'] "cid" => $customer['customerid']

View File

@@ -16,7 +16,7 @@
<default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default> <default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default>
<default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default> <default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default>
<default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default> <default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default>
<default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/nginx/"></default> <default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/php/"></default>
</defaults> </defaults>
<services> <services>
<!-- HTTP --> <!-- HTTP -->

View File

@@ -16,7 +16,7 @@
<default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default> <default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default>
<default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default> <default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default>
<default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default> <default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default>
<default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/nginx/"></default> <default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/php/"></default>
</defaults> </defaults>
<services> <services>
<!-- HTTP --> <!-- HTTP -->

View File

@@ -16,7 +16,7 @@
<default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default> <default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default>
<default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default> <default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default>
<default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default> <default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default>
<default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/nginx/"></default> <default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/php/"></default>
</defaults> </defaults>
<services> <services>
<!-- HTTP --> <!-- HTTP -->

View File

@@ -16,7 +16,7 @@
<default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default> <default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default>
<default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default> <default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default>
<default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default> <default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default>
<default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/nginx/"></default> <default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/php/"></default>
</defaults> </defaults>
<services> <services>
<!-- HTTP --> <!-- HTTP -->

View File

@@ -16,7 +16,7 @@
<default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default> <default for="nginx" settinggroup="system" varname="apacheconf_htpasswddir" value="/etc/nginx/froxlor-htpasswd/"></default>
<default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default> <default for="nginx" settinggroup="system" varname="apachereload_command" value="service nginx reload"></default>
<default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default> <default for="nginx" settinggroup="system" varname="letsencryptacmeconf" value="/etc/nginx/acme.conf"></default>
<default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/nginx/"></default> <default for="nginx" settinggroup="phpfpm" varname="fastcgi_ipcdir" value="/var/run/php/"></default>
</defaults> </defaults>
<services> <services>
<!-- HTTP --> <!-- HTTP -->

View File

@@ -46,7 +46,7 @@ return [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'mandatory' => true, 'mandatory' => true,
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'admin_password_suggestion' => [ 'admin_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -52,7 +52,7 @@ return [
'admin_password' => [ 'admin_password' => [
'label' => lng('login.password') . '&nbsp;(' . lng('panel.emptyfornochanges') . ')', 'label' => lng('login.password') . '&nbsp;(' . lng('panel.emptyfornochanges') . ')',
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'visible' => $result['adminid'] != $userinfo['userid'], 'visible' => $result['adminid'] != $userinfo['userid'],
'next_to' => [ 'next_to' => [
'admin_password_suggestion' => [ 'admin_password_suggestion' => [

View File

@@ -58,7 +58,7 @@ return [
'new_customer_password' => [ 'new_customer_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'placeholder' => lng('admin.password_default_msg'), 'placeholder' => lng('admin.password_default_msg'),
'next_to' => [ 'next_to' => [
'new_customer_password_suggestion' => [ 'new_customer_password_suggestion' => [

View File

@@ -63,7 +63,7 @@ return [
'new_customer_password' => [ 'new_customer_password' => [
'label' => lng('login.password') . '&nbsp;(' . lng('panel.emptyfornochanges') . ')', 'label' => lng('login.password') . '&nbsp;(' . lng('panel.emptyfornochanges') . ')',
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'new_customer_password_suggestion' => [ 'new_customer_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -193,7 +193,7 @@ return [
'label' => lng('admin.domain_sslenabled'), 'label' => lng('admin.domain_sslenabled'),
'type' => 'checkbox', 'type' => 'checkbox',
'value' => '1', 'value' => '1',
'checked' => !empty($ssl_ipsandports) 'checked' => !empty(Settings::Get('system.defaultsslip'))
], ],
'no_ssl_available_info' => [ 'no_ssl_available_info' => [
'visible' => empty($ssl_ipsandports), 'visible' => empty($ssl_ipsandports),

View File

@@ -213,6 +213,10 @@ return [
'type' => 'hidden', 'type' => 'hidden',
'value' => '0' 'value' => '0'
], ],
'emaildomainverified' => [
'type' => 'hidden',
'value' => '0'
],
] ]
], ],
'section_bssl' => [ 'section_bssl' => [
@@ -433,34 +437,36 @@ return [
'section_d' => [ 'section_d' => [
'title' => lng('admin.nameserversettings'), 'title' => lng('admin.nameserversettings'),
'image' => 'icons/domain_edit.png', 'image' => 'icons/domain_edit.png',
'visible' => Settings::Get('system.bind_enable') == '1' && $userinfo['change_serversettings'] == '1', 'visible' => ($userinfo['change_serversettings'] == '1' && Settings::Get('system.bind_enable') == '1') || ($result['isemaildomain'] == '1' && (Settings::Get('spf.use_spf') == '1' || Settings::Get('dmarc.use_dmarc') == '1' || (Settings::Get('antispam.activated') == '1' && $result['dkim'] == '1' && $result['dkim_pubkey'] != ''))),
'fields' => [ 'fields' => [
'isbinddomain' => [ 'isbinddomain' => [
'visible' => $userinfo['change_serversettings'] == '1' && Settings::Get('system.bind_enable') == '1',
'label' => lng('admin.createzonefile'), 'label' => lng('admin.createzonefile'),
'type' => 'checkbox', 'type' => 'checkbox',
'value' => '1', 'value' => '1',
'checked' => $result['isbinddomain'] 'checked' => $result['isbinddomain']
], ],
'zonefile' => [ 'zonefile' => [
'visible' => $userinfo['change_serversettings'] == '1' && Settings::Get('system.bind_enable') == '1',
'label' => lng('admin.custombindzone'), 'label' => lng('admin.custombindzone'),
'desc' => lng('admin.bindzonewarning'), 'desc' => lng('admin.bindzonewarning'),
'type' => 'text', 'type' => 'text',
'value' => $result['zonefile'] 'value' => $result['zonefile']
], ],
'spf_entry' => [ 'spf_entry' => [
'visible' => (Settings::Get('system.bind_enable') == '0' && Settings::Get('spf.use_spf') == '1' && $result['isemaildomain'] == '1'), 'visible' => (Settings::Get('spf.use_spf') == '1' && $result['isemaildomain'] == '1'),
'label' => lng('antispam.required_spf_dns'), 'label' => lng('antispam.required_spf_dns'),
'type' => 'longtext', 'type' => 'longtext',
'value' => (string)(new \Froxlor\Dns\DnsEntry('@', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent(Settings::Get('spf.spf_entry')))) 'value' => (string)(new \Froxlor\Dns\DnsEntry('@', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent(Settings::Get('spf.spf_entry'))))
], ],
'dmarc_entry' => [ 'dmarc_entry' => [
'visible' => (Settings::Get('system.bind_enable') == '0' && Settings::Get('dmarc.use_dmarc') == '1' && $result['isemaildomain'] == '1'), 'visible' => (Settings::Get('dmarc.use_dmarc') == '1' && $result['isemaildomain'] == '1'),
'label' => lng('antispam.required_dmarc_dns'), 'label' => lng('antispam.required_dmarc_dns'),
'type' => 'longtext', 'type' => 'longtext',
'value' => (string)(new \Froxlor\Dns\DnsEntry('_dmarc', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent(Settings::Get('dmarc.dmarc_entry')))) 'value' => (string)(new \Froxlor\Dns\DnsEntry('_dmarc', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent(Settings::Get('dmarc.dmarc_entry'))))
], ],
'dkim_entry' => [ 'dkim_entry' => [
'visible' => (Settings::Get('system.bind_enable') == '0' && Settings::Get('antispam.activated') == '1' && $result['dkim'] == '1' && $result['dkim_pubkey'] != ''), 'visible' => (Settings::Get('antispam.activated') == '1' && $result['dkim'] == '1' && $result['dkim_pubkey'] != '' && $result['isemaildomain'] == '1'),
'label' => lng('antispam.required_dkim_dns'), 'label' => lng('antispam.required_dkim_dns'),
'type' => 'longtext', 'type' => 'longtext',
'value' => (string)(new \Froxlor\Dns\DnsEntry('dkim' . $result['dkim_id'] . '._domainkey', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent('v=DKIM1; k=rsa; p='.trim($result['dkim_pubkey'])))) 'value' => (string)(new \Froxlor\Dns\DnsEntry('dkim' . $result['dkim_id'] . '._domainkey', 'TXT', \Froxlor\Dns\Dns::encloseTXTContent('v=DKIM1; k=rsa; p='.trim($result['dkim_pubkey']))))

View File

@@ -43,7 +43,7 @@ return [
'email_password' => [ 'email_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'email_password_suggestion' => [ 'email_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -43,7 +43,7 @@ return [
'email_password' => [ 'email_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'mandatory' => true, 'mandatory' => true,
'next_to' => [ 'next_to' => [
'email_password_suggestion' => [ 'email_password_suggestion' => [

View File

@@ -89,60 +89,49 @@ return [
] ]
] ]
], ],
'mail_catchall' => [ 'iscatchall' => [
'visible' => Settings::Get('catchall.catchall_enabled') == '1',
'label' => lng('emails.catchall'), 'label' => lng('emails.catchall'),
'type' => 'label', 'type' => 'checkbox',
'value' => ((int)$result['iscatchall'] == 0 ? lng('panel.no') : lng('panel.yes')), 'value' => '1',
'next_to' => [ 'checked' => (int)$result['iscatchall'],
'add_link' => [ ],
'type' => 'link', 'bypass_spam' => [
'href' => $filename . '?page=' . $page . '&amp;domainid=' . $result['domainid'] . '&amp;action=togglecatchall&amp;id=' . $result['id'], 'visible' => Settings::Get('antispam.activated') == '1' && (int)Settings::Get('antispam.default_bypass_spam') <= 2,
'label' => '<i class="fa-solid fa-arrow-right-arrow-left"></i> ' . lng('panel.toggle'), 'label' => lng('antispam.bypass_spam'),
'classes' => 'btn btn-sm btn-secondary' 'type' => 'checkbox',
] 'value' => '1',
] 'checked' => (int)$result['bypass_spam'],
], ],
'spam_tag_level' => [ 'spam_tag_level' => [
'visible' => Settings::Get('antispam.activated') == '1', 'visible' => Settings::Get('antispam.activated') == '1',
'label' => lng('antispam.spam_tag_level'), 'label' => lng('antispam.spam_tag_level'),
'type' => 'text', 'type' => 'number',
'string_regexp' => '/^\d{1,}(\.\d{1,2})?$/', 'min' => 0,
'value' => $result['spam_tag_level'] 'step' => 0.1,
'value' => $result['spam_tag_level'],
],
'spam_rewrite_subject' => [
'visible' => Settings::Get('antispam.activated') == '1' && (int)Settings::Get('antispam.default_spam_rewrite_subject') <= 2,
'label' => lng('antispam.rewrite_subject'),
'type' => 'checkbox',
'value' => '1',
'checked' => (int)$result['rewrite_subject'],
], ],
'spam_kill_level' => [ 'spam_kill_level' => [
'visible' => Settings::Get('antispam.activated') == '1', 'visible' => Settings::Get('antispam.activated') == '1',
'label' => lng('antispam.spam_kill_level'), 'label' => lng('antispam.spam_kill_level'),
'type' => 'text', 'desc' => lng('panel.use_checkbox_to_disable'),
'string_regexp' => '/^\d{1,}(\.\d{1,2})?$/', 'type' => 'textul',
'step' => 0.1,
'value' => $result['spam_kill_level'] 'value' => $result['spam_kill_level']
], ],
'bypass_spam' => [
'visible' => Settings::Get('antispam.activated') == '1',
'label' => lng('antispam.bypass_spam'),
'type' => 'label',
'value' => ((int)$result['bypass_spam'] == 0 ? lng('panel.no') : lng('panel.yes')),
'next_to' => [
'add_link' => [
'type' => 'link',
'href' => $filename . '?page=' . $page . '&amp;domainid=' . $result['domainid'] . '&amp;action=togglebypass&amp;id=' . $result['id'],
'label' => '<i class="fa-solid fa-arrow-right-arrow-left"></i> ' . lng('panel.toggle'),
'classes' => 'btn btn-sm btn-secondary'
]
]
],
'policy_greylist' => [ 'policy_greylist' => [
'visible' => Settings::Get('antispam.activated') == '1', 'visible' => Settings::Get('antispam.activated') == '1' && (int)Settings::Get('antispam.default_policy_greylist') <= 2,
'label' => lng('antispam.policy_greylist'), 'label' => lng('antispam.policy_greylist'),
'type' => 'label', 'type' => 'checkbox',
'value' => ((int)$result['policy_greylist'] == 0 ? lng('panel.no') : lng('panel.yes')), 'value' => '1',
'next_to' => [ 'checked' => (int)$result['policy_greylist'],
'add_link' => [
'type' => 'link',
'href' => $filename . '?page=' . $page . '&amp;domainid=' . $result['domainid'] . '&amp;action=togglegreylist&amp;id=' . $result['id'],
'label' => '<i class="fa-solid fa-arrow-right-arrow-left"></i> ' . lng('panel.toggle'),
'classes' => 'btn btn-sm btn-secondary'
]
]
], ],
'mail_fwds' => [ 'mail_fwds' => [
'label' => lng('emails.forwarders') . ' (' . $forwarders_count . ')', 'label' => lng('emails.forwarders') . ' (' . $forwarders_count . ')',

View File

@@ -54,7 +54,7 @@ return [
'directory_password' => [ 'directory_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'mandatory' => true, 'mandatory' => true,
'next_to' => [ 'next_to' => [
'directory_password_suggestion' => [ 'directory_password_suggestion' => [

View File

@@ -49,7 +49,7 @@ return [
'directory_password' => [ 'directory_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'directory_password_suggestion' => [ 'directory_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -58,7 +58,7 @@ return [
'ftp_password' => [ 'ftp_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'mandatory' => true, 'mandatory' => true,
'next_to' => [ 'next_to' => [
'ftp_password_suggestion' => [ 'ftp_password_suggestion' => [

View File

@@ -51,7 +51,7 @@ return [
'label' => lng('login.password'), 'label' => lng('login.password'),
'desc' => lng('ftp.editpassdescription'), 'desc' => lng('ftp.editpassdescription'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'ftp_password_suggestion' => [ 'ftp_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -45,7 +45,7 @@ return [
'mysql_password' => [ 'mysql_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'mandatory' => true, 'mandatory' => true,
'next_to' => [ 'next_to' => [
'mysql_password_suggestion' => [ 'mysql_password_suggestion' => [

View File

@@ -52,7 +52,7 @@ return [
'mysql_password' => [ 'mysql_password' => [
'label' => lng('changepassword.new_password_ifnotempty'), 'label' => lng('changepassword.new_password_ifnotempty'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'next_to' => [ 'next_to' => [
'mysql_password_suggestion' => [ 'mysql_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':', 'next_to_prefix' => lng('customer.generated_pwd') . ':',

View File

@@ -34,7 +34,7 @@ return [
'mysql_password' => [ 'mysql_password' => [
'label' => lng('login.password'), 'label' => lng('login.password'),
'type' => 'password', 'type' => 'password',
'autocomplete' => 'off', 'autocomplete' => 'new-password',
'mandatory' => true, 'mandatory' => true,
'next_to' => [ 'next_to' => [
'mysql_password_suggestion' => [ 'mysql_password_suggestion' => [

View File

@@ -61,7 +61,7 @@ function lng(string $identifier, array $arguments = [])
* @param string|null $session * @param string|null $session
* @return mixed|string|null * @return mixed|string|null
*/ */
function old(string $identifier, string $default = null, string $session = null) function old(string $identifier, ?string $default, ?string $session = null)
{ {
if ($session && isset($_SESSION[$session])) { if ($session && isset($_SESSION[$session])) {
return $_SESSION[$session][$identifier] ?? $default; return $_SESSION[$session][$identifier] ?? $default;

View File

@@ -374,7 +374,7 @@ if (CurrentUser::hasSession()) {
'domain' => UI::getCookieHost(), 'domain' => UI::getCookieHost(),
'secure' => UI::requestIsHttps(), 'secure' => UI::requestIsHttps(),
'httponly' => true, 'httponly' => true,
'samesite' => 'Strict' 'samesite' => 'Lax'
]; ];
setcookie(session_name(), $_COOKIE[session_name()], $cookie_params); setcookie(session_name(), $_COOKIE[session_name()], $cookie_params);
} else { } else {

View File

@@ -33,9 +33,9 @@ return [
'title' => lng('menue.email.emailsoverview'), 'title' => lng('menue.email.emailsoverview'),
'icon' => 'fa-solid fa-envelope', 'icon' => 'fa-solid fa-envelope',
'self_overview' => ['section' => 'email', 'page' => 'overview'], 'self_overview' => ['section' => 'email', 'page' => 'overview'],
'default_sorting' => ['d.domain' => 'asc'], 'default_sorting' => ['d.domain_ace' => 'asc'],
'columns' => [ 'columns' => [
'd.domain' => [ 'd.domain_ace' => [
'label' => 'Domain', 'label' => 'Domain',
'field' => 'domain', 'field' => 'domain',
], ],
@@ -56,7 +56,7 @@ return [
], ],
], ],
'visible_columns' => Listing::getVisibleColumnsForListing('emaildomain_list', [ 'visible_columns' => Listing::getVisibleColumnsForListing('emaildomain_list', [
'd.domain', 'd.domain_ace',
'addresses', 'addresses',
'accounts', 'accounts',
'forwarder', 'forwarder',

View File

@@ -504,6 +504,7 @@ return [
'apiguide' => 'API Guide', 'apiguide' => 'API Guide',
'domain_duplicate' => 'Domain duplizieren', 'domain_duplicate' => 'Domain duplizieren',
'domain_duplicate_named' => '%s duplizieren', 'domain_duplicate_named' => '%s duplizieren',
'emaildomainwarning' => '<div id="emaildomainnote" class="invalid-feedback">ACHTUNG: Durch die Änderung dieser Einstellung löschen Sie alle bestehenden E-Mail-Adressen und -Konten unwiderruflich.</div>',
], ],
'apikeys' => [ 'apikeys' => [
'no_api_keys' => 'Keine API Keys gefunden', 'no_api_keys' => 'Keine API Keys gefunden',
@@ -621,6 +622,10 @@ return [
'title' => 'Spam Level', 'title' => 'Spam Level',
'description' => 'Erforderliche Punktzahl zum Markieren einer E-Mail als Spam<br/>Standard: 7.0' 'description' => 'Erforderliche Punktzahl zum Markieren einer E-Mail als Spam<br/>Standard: 7.0'
], ],
'rewrite_subject' => [
'title' => 'Betreff ändern',
'description' => 'Dem E-Mail Betreff <strong>***SPAM***</strong> hinzufügen, sofern zutreffend',
],
'spam_kill_level' => [ 'spam_kill_level' => [
'title' => 'Ablehnungs Level', 'title' => 'Ablehnungs Level',
'description' => 'Erforderliche Punktzahl für das Ablehnen einer E-Mail<br/>Standard: 14.0' 'description' => 'Erforderliche Punktzahl für das Ablehnen einer E-Mail<br/>Standard: 14.0'
@@ -636,6 +641,24 @@ return [
'required_spf_dns' => 'Erforderlicher SPF DNS Eintrag', 'required_spf_dns' => 'Erforderlicher SPF DNS Eintrag',
'required_dmarc_dns' => 'Erforderlicher DMARC DNS Eintrag', 'required_dmarc_dns' => 'Erforderlicher DMARC DNS Eintrag',
'required_dkim_dns' => 'Erforderlicher DKIM DNS Eintrag', 'required_dkim_dns' => 'Erforderlicher DKIM DNS Eintrag',
'default_select' => [
'on_changeable' => 'Aktiviert, einstellbar',
'off_changeable' => 'Deaktiviert, einstellbar',
'on_unchangeable' => 'Aktiviert, nicht einstellbar',
'off_unchangeable' => 'Deaktiviert, nicht einstellbar',
],
'default_bypass_spam' => [
'title' => 'Standardwert: Spamfilter umgehen',
'description' => 'Wählen, ob bei neuen E-Mail-Konten "Spamfilter umgehen" standardmäßig aktiviert ist und ob diese Einstellung vom Kunden angepasst werden kann.<br/>Standard: Deaktiviert, einstellbar'
],
'default_spam_rewrite_subject' => [
'title' => 'Standardwert: Betreff ändern',
'description' => 'Wählen, ob bei neuen E-Mail-Konten "Betreff ändern" standardmäßig aktiviert ist und ob diese Einstellung vom Kunden angepasst werden kann.<br/>Standard: Aktiviert, einstellbar'
],
'default_policy_greylist' => [
'title' => 'Standardwert: Verwende greylisting',
'description' => 'Wählen, ob bei neuen E-Mail-Konten "Verwende greylisting" standardmäßig aktiviert ist und ob diese Einstellung vom Kunden angepasst werden kann.<br/>Standard: Aktiviert, einstellbar'
],
], ],
'dns' => [ 'dns' => [
'destinationip' => 'Domain-IP-Adresse(n)', 'destinationip' => 'Domain-IP-Adresse(n)',
@@ -765,6 +788,8 @@ return [
'mydocumentroot' => '\'Documentroot\'', 'mydocumentroot' => '\'Documentroot\'',
'loginnameexists' => 'Der Login-Name "%s" existiert bereits.', 'loginnameexists' => 'Der Login-Name "%s" existiert bereits.',
'emailiswrong' => 'Die E-Mail-Adresse "%s" enthält ungültige Zeichen oder ist nicht vollständig.', 'emailiswrong' => 'Die E-Mail-Adresse "%s" enthält ungültige Zeichen oder ist nicht vollständig.',
'emailexists' => 'Die E-Mail-Adresse "%s" wird bereits von einem anderen Admin verwendet',
'emailexistsanon' => 'Die E-Mail-Adresse "%s" wird bereits verwendet',
'alternativeemailiswrong' => 'Die angegebene alternative E-Mail Adresse "%s", an welche die Zugangsdaten geschickt werden soll, scheint ungültig zu sein.', 'alternativeemailiswrong' => 'Die angegebene alternative E-Mail Adresse "%s", an welche die Zugangsdaten geschickt werden soll, scheint ungültig zu sein.',
'loginnameiswrong' => 'Der Login-Name "%s" enthält ungültige Zeichen.', 'loginnameiswrong' => 'Der Login-Name "%s" enthält ungültige Zeichen.',
'loginnameiswrong2' => 'Der Login-Name enthält zu viele Zeichen, es sind maximal %s Zeichen erlaubt.', 'loginnameiswrong2' => 'Der Login-Name enthält zu viele Zeichen, es sind maximal %s Zeichen erlaubt.',
@@ -952,6 +977,7 @@ return [
'invalidpgppublickey' => 'Der angegebene PGP Public Key ist ungültig', 'invalidpgppublickey' => 'Der angegebene PGP Public Key ist ungültig',
'invalid_validtime' => 'Wert der valid_time in Sekunden muss zwischen 10 und 120 liegen.', 'invalid_validtime' => 'Wert der valid_time in Sekunden muss zwischen 10 und 120 liegen.',
'customerphpenabledbutnoconfig' => 'Kunde hat PHP aktiviert aber keine PHP-Konfiguration wurde gewählt.', 'customerphpenabledbutnoconfig' => 'Kunde hat PHP aktiviert aber keine PHP-Konfiguration wurde gewählt.',
'emaildomainstillhasaddresses' => 'Maildomain-Flag kann nicht deaktiviert werden, da für diese Domain noch E-Mail-Adressen vorhanden sind.',
], ],
'extras' => [ 'extras' => [
'description' => 'Hier können Sie zusätzliche Extras einrichten, wie zum Beispiel einen Verzeichnisschutz.<br />Die Änderungen sind erst nach einer kurzen Zeit wirksam.', 'description' => 'Hier können Sie zusätzliche Extras einrichten, wie zum Beispiel einen Verzeichnisschutz.<br />Die Änderungen sind erst nach einer kurzen Zeit wirksam.',
@@ -1262,6 +1288,7 @@ Vielen Dank, Ihr Administrator',
'upload_import' => 'Hochladen und importieren', 'upload_import' => 'Hochladen und importieren',
'profile' => 'Mein Profil', 'profile' => 'Mein Profil',
'use_checkbox_for_unlimited' => 'Der Wert "0" deaktiviert die Resource. Die Checkbox rechts erlaubt "unlimitierte" Nutzung.', 'use_checkbox_for_unlimited' => 'Der Wert "0" deaktiviert die Resource. Die Checkbox rechts erlaubt "unlimitierte" Nutzung.',
'use_checkbox_to_disable' => 'Zum Deaktivieren, klicke die Checkbox auf der rechten Seite des Eingabefeldes',
], ],
'phpfpm' => [ 'phpfpm' => [
'vhost_httpuser' => 'Lokaler Benutzer für PHP-FPM (Froxlor-Vhost)', 'vhost_httpuser' => 'Lokaler Benutzer für PHP-FPM (Froxlor-Vhost)',
@@ -2169,6 +2196,7 @@ Vielen Dank, Ihr Administrator',
'CREATE_CUSTOMER_DATADUMP' => 'Daten-Export für Kunde %s', 'CREATE_CUSTOMER_DATADUMP' => 'Daten-Export für Kunde %s',
'DELETE_DOMAIN_PDNS' => 'Lösche Domain %s von PowerDNS Datenbank', 'DELETE_DOMAIN_PDNS' => 'Lösche Domain %s von PowerDNS Datenbank',
'DELETE_DOMAIN_SSL' => 'Lösche SSL Dateien von Domain %s', 'DELETE_DOMAIN_SSL' => 'Lösche SSL Dateien von Domain %s',
'UPDATE_LE_SERVICES' => 'Aktualisiere Systemdienste für Let\'s Encrypt',
], ],
'terms' => 'AGB', 'terms' => 'AGB',
'traffic' => [ 'traffic' => [
@@ -2264,6 +2292,7 @@ Vielen Dank, Ihr Administrator',
'critical_error' => 'Kritischer Fehler', 'critical_error' => 'Kritischer Fehler',
'suggestions' => 'Nicht notwendig, aber emfohlen', 'suggestions' => 'Nicht notwendig, aber emfohlen',
'phpinfosuccess' => 'Auf dem System ist PHP %s installiert', 'phpinfosuccess' => 'Auf dem System ist PHP %s installiert',
'suggestionsnote' => 'Es liegen keine kritische Fehler vor, die eine Installation verhindern, allerdings beachten Sie bitte die Empfehlungen weiter unten für ein optimales Erlebnis.',
'phpinfowarn' => 'Die genutzte PHP Version ist kleiner als die geforderte Version %s', 'phpinfowarn' => 'Die genutzte PHP Version ist kleiner als die geforderte Version %s',
'phpinfoupdate' => 'Aktualisierung von PHP Version %s auf %s oder höher notwendig', 'phpinfoupdate' => 'Aktualisierung von PHP Version %s auf %s oder höher notwendig',
'start_installation' => 'Installation starten', 'start_installation' => 'Installation starten',

View File

@@ -29,6 +29,7 @@ return [
'de' => 'German', 'de' => 'German',
'en' => 'English', 'en' => 'English',
'fr' => 'French', 'fr' => 'French',
'hu' => 'Hungarian',
'it' => 'Italian', 'it' => 'Italian',
'nl' => 'Dutch', 'nl' => 'Dutch',
'pt' => 'Portuguese', 'pt' => 'Portuguese',
@@ -519,6 +520,7 @@ return [
'backups' => [ 'backups' => [
'backups' => 'Backups', 'backups' => 'Backups',
], ],
'emaildomainwarning' => '<div id="emaildomainnote" class="invalid-feedback">WARNING: By changing this setting you will delete all existing e-mail addresses and -accounts permanently.</div>',
], ],
'apcuinfo' => [ 'apcuinfo' => [
'clearcache' => 'Clear APCu cache', 'clearcache' => 'Clear APCu cache',
@@ -670,6 +672,10 @@ return [
'title' => 'Spam tag level', 'title' => 'Spam tag level',
'description' => 'Score that is required to mark an email as spam<br/>Default: 7.0' 'description' => 'Score that is required to mark an email as spam<br/>Default: 7.0'
], ],
'rewrite_subject' => [
'title' => 'Rewrite subject',
'description' => 'Whether to add <strong>***SPAM***</strong> to the email subject if applicable',
],
'spam_kill_level' => [ 'spam_kill_level' => [
'title' => 'Spam kill level', 'title' => 'Spam kill level',
'description' => 'Score that is required to discard an email entirely<br/>Default: 14.0' 'description' => 'Score that is required to discard an email entirely<br/>Default: 14.0'
@@ -685,6 +691,24 @@ return [
'required_spf_dns' => 'Required SPF DNS entry', 'required_spf_dns' => 'Required SPF DNS entry',
'required_dmarc_dns' => 'Required DMARC DNS entry', 'required_dmarc_dns' => 'Required DMARC DNS entry',
'required_dkim_dns' => 'Required DKIM DNS entry', 'required_dkim_dns' => 'Required DKIM DNS entry',
'default_select' => [
'on_changeable' => 'Activated, adjustable',
'off_changeable' => 'Deactivated, adjustable',
'on_unchangeable' => 'Activated, not adjustable',
'off_unchangeable' => 'Deactivated, not adjustable',
],
'default_bypass_spam' => [
'title' => 'Bypass spamfilter default value',
'description' => 'Whether new email accounts have "Bypass spamfilter" activated by default and whether this setting is adjustable by the customer.<br/>Default: Deactivated, adjustable'
],
'default_spam_rewrite_subject' => [
'title' => 'Rewrite subject default value',
'description' => 'Whether new email accounts have "Rewrite subject" activated by default and whether this setting is adjustable by the customer.<br/>Default: Activated, adjustable'
],
'default_policy_greylist' => [
'title' => 'Use greylisting default value',
'description' => 'Whether new email accounts have "Use greylisting" activated by default and whether this setting is adjustable by the customer.<br/>Default: Activated, adjustable'
],
], ],
'dns' => [ 'dns' => [
'destinationip' => 'Domain IP(s)', 'destinationip' => 'Domain IP(s)',
@@ -836,6 +860,8 @@ return [
'mydocumentroot' => '\'Documentroot\'', 'mydocumentroot' => '\'Documentroot\'',
'loginnameexists' => 'Loginname %s already exists', 'loginnameexists' => 'Loginname %s already exists',
'emailiswrong' => 'Email-address %s contains invalid characters or is incomplete', 'emailiswrong' => 'Email-address %s contains invalid characters or is incomplete',
'emailexists' => 'Email-address %s already in use by another admin',
'emailexistsanon' => 'Email-address %s already in use.',
'alternativeemailiswrong' => 'The given alternative email address %s to send the credentials to seems to be invalid', 'alternativeemailiswrong' => 'The given alternative email address %s to send the credentials to seems to be invalid',
'loginnameiswrong' => 'Loginname "%s" contains illegal characters.', 'loginnameiswrong' => 'Loginname "%s" contains illegal characters.',
'loginnameiswrong2' => 'Loginname contains too many characters. Only %s characters are allowed.', 'loginnameiswrong2' => 'Loginname contains too many characters. Only %s characters are allowed.',
@@ -1024,6 +1050,7 @@ return [
'invalidpgppublickey' => 'The PGP Public Key is not valid', 'invalidpgppublickey' => 'The PGP Public Key is not valid',
'invalid_validtime' => 'Valid time in seconds can only be between 10 and 120', 'invalid_validtime' => 'Valid time in seconds can only be between 10 and 120',
'customerphpenabledbutnoconfig' => 'Customer has PHP activated but no PHP-configuration was selected.', 'customerphpenabledbutnoconfig' => 'Customer has PHP activated but no PHP-configuration was selected.',
'emaildomainstillhasaddresses' => 'Cannot deactivate mail-domain flag, as there are still email-addresses for this domain.',
], ],
'extras' => [ 'extras' => [
'description' => 'Here you can add some extras, for example directory protection.<br />The system will need some time to apply the new settings after every change.', 'description' => 'Here you can add some extras, for example directory protection.<br />The system will need some time to apply the new settings after every change.',
@@ -1377,6 +1404,7 @@ Yours sincerely, your administrator',
'upload_import' => 'Upload and import', 'upload_import' => 'Upload and import',
'profile' => 'My profile', 'profile' => 'My profile',
'use_checkbox_for_unlimited' => 'The value "0" deactivates this resource. The checkbox on the right allows "unlimited" usage.', 'use_checkbox_for_unlimited' => 'The value "0" deactivates this resource. The checkbox on the right allows "unlimited" usage.',
'use_checkbox_to_disable' => 'To disable, activate the checkbox on the right of the input field',
], ],
'phpfpm' => [ 'phpfpm' => [
'vhost_httpuser' => 'Local user to use for PHP-FPM (Froxlor vHost)', 'vhost_httpuser' => 'Local user to use for PHP-FPM (Froxlor vHost)',
@@ -2303,6 +2331,7 @@ Yours sincerely, your administrator',
'CREATE_CUSTOMER_DATADUMP' => 'Data export job for customer %s', 'CREATE_CUSTOMER_DATADUMP' => 'Data export job for customer %s',
'DELETE_DOMAIN_PDNS' => 'Delete domain %s from PowerDNS database', 'DELETE_DOMAIN_PDNS' => 'Delete domain %s from PowerDNS database',
'DELETE_DOMAIN_SSL' => 'Delete ssl files of domain %s', 'DELETE_DOMAIN_SSL' => 'Delete ssl files of domain %s',
'UPDATE_LE_SERVICES' => 'Updating system services for Let\'s Encrypt',
], ],
'terms' => 'Terms of use', 'terms' => 'Terms of use',
'traffic' => [ 'traffic' => [
@@ -2400,6 +2429,7 @@ Yours sincerely, your administrator',
'critical_error' => 'Critical error', 'critical_error' => 'Critical error',
'suggestions' => 'Not required but recommended', 'suggestions' => 'Not required but recommended',
'phpinfosuccess' => 'Your system is running with PHP %s', 'phpinfosuccess' => 'Your system is running with PHP %s',
'suggestionsnote' => 'There are no critical errors that prevent installation, but please follow the recommendations below for an optimal experience.',
'phpinfowarn' => 'Your system is running a lower version than PHP %s', 'phpinfowarn' => 'Your system is running a lower version than PHP %s',
'phpinfoupdate' => 'Update your current PHP version from %s to %s or higher', 'phpinfoupdate' => 'Update your current PHP version from %s to %s or higher',
'start_installation' => 'Start installation', 'start_installation' => 'Start installation',

2504
lng/hu.lng.php Normal file

File diff suppressed because it is too large Load Diff

2035
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,23 +6,23 @@
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.7.4", "axios": "^1.8.1",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"chart.js": "^4.4.0", "chart.js": "^4.4.8",
"jquery": "^3.6.1", "jquery": "^3.7.1",
"jquery-validation": "^1.20.0", "jquery-validation": "^1.21.0",
"laravel-vite-plugin": "^0.8.0", "laravel-vite-plugin": "^1.2.0",
"lodash": "^4.17.19", "lodash": "^4.17.21",
"postcss": "^8.1.14", "postcss": "^8.5.3",
"resolve-url-loader": "^5.0.0", "resolve-url-loader": "^5.0.0",
"sass": "^1.69.3", "sass": "^1.85.1",
"vite": "^4.5.3", "vite": "^6.2.0",
"vue": "^3.2.37" "vue": "^3.5.13"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=22"
} }
} }

View File

@@ -67,6 +67,38 @@ export default function () {
}); });
} }
// show warning if emaildomain option is set to disabled but was enabled
if ($('input[name=emaildomainverified]')) {
$('input[name=isemaildomain]').on('click', function () {
$('#emaildomainnote').remove();
$('#isemaildomain').removeClass('is-invalid');
$('#emaildomainverified').val(0);
const cFileName = window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1);
$.ajax({
url: cFileName + "?page=overview&action=jqEmaildomainNote",
type: "POST",
data: {
id: $('input[name=id]').val(), newval: +$('#isemaildomain').is(':checked')
},
dataType: "json",
async: false,
beforeSend: function (request) {
request.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
},
success: function (json) {
if (json.changed) {
$('#isemaildomain').addClass('is-invalid');
$('#isemaildomain').parent().append(json.info);
$('#emaildomainverified').val(1);
}
},
error: function (a, b) {
console.log(a, b);
}
});
});
}
/** /**
* email only domain - hide unnecessary/unused sections * email only domain - hide unnecessary/unused sections
*/ */

View File

@@ -0,0 +1,33 @@
export default function () {
$(function () {
/**
* bypass spam - hide unnecessary/unused sections
*/
if ($('#id') && $('#bypass_spam').is(':checked')) {
$('#spam_tag_level').closest('.row').addClass('d-none');
$('#spam_rewrite_subject').closest('.row').addClass('d-none');
$('#spam_kill_level').closest('.row').addClass('d-none');
$('#policy_greylist').closest('.row').addClass('d-none');
}
/**
* toggle show/hide of sections in case of bypass spam flag
*/
$('#bypass_spam').on('click', function () {
if ($(this).is(':checked')) {
// hide unnecessary sections
$('#spam_tag_level').closest('.row').addClass('d-none');
$('#spam_rewrite_subject').closest('.row').addClass('d-none');
$('#spam_kill_level').closest('.row').addClass('d-none');
$('#policy_greylist').closest('.row').addClass('d-none');
} else {
// show sections
$('#spam_tag_level').closest('.row').removeClass('d-none');
$('#spam_rewrite_subject').closest('.row').removeClass('d-none');
$('#spam_kill_level').closest('.row').removeClass('d-none');
$('#policy_greylist').closest('.row').removeClass('d-none');
}
})
});
}

View File

@@ -19,6 +19,8 @@
{% endif %} {% endif %}
{% if field.label.description is defined and field.label.description is not empty %}<br><small>{{ field.label.description|raw }}</small> {% if field.label.description is defined and field.label.description is not empty %}<br><small>{{ field.label.description|raw }}</small>
{% endif %} {% endif %}
{% if field.desc is defined and field.desc is not empty %}<br><small>{{ field.desc|raw }}</small>
{% endif %}
{% if field.requires_reconf is defined and field.requires_reconf is not empty %} {% if field.requires_reconf is defined and field.requires_reconf is not empty %}
<div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert"> <div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p> <i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p>
@@ -170,7 +172,7 @@
{% if field.next_to is defined %} {% if field.next_to is defined %}
<div class="input-group"> <div class="input-group">
{% endif %} {% endif %}
<input type="{{ field.type }}" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} {% if field.type == 'number' and field.min is defined %} min="{{ field.min }}" {% endif %} {% if field.type == 'number' and field.max is defined %} max="{{ field.max }}" {% endif %} {% if field.type != 'number' and field.maxlength is defined %} maxlength="{{ field.maxlength }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.type == 'file' and field.accept is defined %} accept="{{ field.accept }}" {% endif %} {% if field.pattern is defined %} pattern="{{ field.pattern }}" {% endif %}/> <input type="{{ field.type }}" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} {% if field.type == 'number' and field.min is defined %} min="{{ field.min }}" {% endif %} {% if field.type == 'number' and field.max is defined %} max="{{ field.max }}" {% endif %} {% if field.type == 'number' and field.step is not empty %} step="{{ field.step }}" {% endif %} {% if field.type != 'number' and field.maxlength is defined %} maxlength="{{ field.maxlength }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.type == 'file' and field.accept is defined %} accept="{{ field.accept }}" {% endif %} {% if field.pattern is defined %} pattern="{{ field.pattern }}" {% endif %}/>
{% if field.type == 'hidden' and field.display is defined %} {% if field.type == 'hidden' and field.display is defined %}
<input type="text" readonly class="form-control-plaintext" value="{{ field.display|raw }}"> <input type="text" readonly class="form-control-plaintext" value="{{ field.display|raw }}">
{% endif %} {% endif %}
@@ -207,7 +209,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div class="input-group"> <div class="input-group">
<input type="number" min="0" {% if max is not empty %} max="{{ max }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{% if field.value >= 0 %}{{ field.value }}{% endif %}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %}/> <input type="number" min="0" {% if max is not empty %} max="{{ max }}" {% endif %}{% if field.step is not empty %} step="{{ field.step }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{% if field.value >= 0 %}{{ field.value }}{% endif %}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %}/>
<div class="input-group-text"> <div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" name="{{ id }}_ul" value="1" {% if field.value == -1 %} checked="checked" {% endif %}> <input class="form-check-input mt-0" type="checkbox" name="{{ id }}_ul" value="1" {% if field.value == -1 %} checked="checked" {% endif %}>
</div> </div>

View File

@@ -71,6 +71,9 @@
<p class="lead {{ preflight.criticals ? 'text-danger' : preflight.suggestions ? 'text-warning' : 'text-success'}}"> <p class="lead {{ preflight.criticals ? 'text-danger' : preflight.suggestions ? 'text-warning' : 'text-success'}}">
<i class="{{ preflight.criticals ? 'fa-solid fa-triangle-exclamation' : preflight.suggestions ? 'fa-solid fa-circle-info' : 'far fa-circle-check' }}"></i> <i class="{{ preflight.criticals ? 'fa-solid fa-triangle-exclamation' : preflight.suggestions ? 'fa-solid fa-circle-info' : 'far fa-circle-check' }}"></i>
{{ preflight.text }} {{ preflight.text }}
{% if preflight.criticals is empty and preflight.suggestions %}
<br>{{ lng('install.suggestionsnote') }}
{% endif %}
</p> </p>
{% if preflight.criticals %} {% if preflight.criticals %}

View File

@@ -55,7 +55,7 @@
<div class="progress-bar bg-success"></div> <div class="progress-bar bg-success"></div>
</div> </div>
<div class="progress" role="progressbar" <div class="progress" role="progressbar"
style="width: {{ 100 - apcuinfo.num_misses_percentage }}%" style="width: {{ apcuinfo.num_misses_percentage }}%"
aria-valuenow="{{ apcuinfo.num_misses }}" aria-valuemin="0" aria-valuenow="{{ apcuinfo.num_misses }}" aria-valuemin="0"
aria-valuemax="{{ apcuinfo.num_hits_and_misses }}"> aria-valuemax="{{ apcuinfo.num_hits_and_misses }}">
<div class="progress-bar bg-danger"></div> <div class="progress-bar bg-danger"></div>

View File

@@ -279,7 +279,7 @@ class AdminsTest extends TestCase
// add test reseller // add test reseller
$data = [ $data = [
'new_loginname' => 'resellertest', 'new_loginname' => 'resellertest',
'email' => 'testreseller@froxlor.org', 'email' => 'testreseller2@froxlor.org',
'name' => 'Testreseller' 'name' => 'Testreseller'
]; ];